在 ASP.NET Core 中执行租户服务
本博文翻譯自:
http://gunnarpeipman.com/2017/08/tenant-providers/
在我之前關(guān)于?Entity Framework core 2.0 全局查詢過濾器的文章中,我提出了一個(gè)想法,當(dāng)構(gòu)建模型時(shí),如何自動地將查詢過濾器應(yīng)用到所有的領(lǐng)域?qū)嶓w中,也就是說領(lǐng)域?qū)嶓w總是來自同一租戶。這篇文章更深入地介紹了在 ASP.NET Core 應(yīng)用程序中檢測當(dāng)前租戶的可能解決方案,并建議一些租戶提供者將為實(shí)際應(yīng)用程序中提供多租戶的支持作為出發(fā)點(diǎn)。
注意!?請閱讀我之前在Entity Framework core 2.0 全局查詢過濾器中的文章,這篇文章將繼續(xù)下去,并期待讀者熟悉我為多租戶提供的解決方案。另外,將多租戶規(guī)則應(yīng)用到所有領(lǐng)域?qū)嶓w的方法是從我以前的全局查詢過濾器中獲取的,而不是在這里復(fù)制的。
如何檢測當(dāng)前租戶?
情況是這樣的。數(shù)據(jù)上下文是在請求傳入和構(gòu)建模型全局查詢過濾器時(shí)構(gòu)建的。其中一個(gè)過濾器是關(guān)于當(dāng)前租戶的。在代碼中還需要租戶ID,但模型還沒有準(zhǔn)備好。同一時(shí)間,租戶ID只能在數(shù)據(jù)庫中使用。我們該怎么辦?
一些想法:
在數(shù)據(jù)上下文中使用數(shù)據(jù)庫連接,并對租戶表進(jìn)行直接查詢
為租戶的信息和操作使用單獨(dú)的數(shù)據(jù)上下文
保持租戶信息在云存儲上可用
使用域名的哈希值作為租戶ID
注意!?在本文中,我希望在web應(yīng)用程序中通過host的header檢測租戶。
我在這篇文章中使用的租戶表如下圖所示。
注意!?依賴于解決方案的租戶ID也可以是其他的,而不是像上圖所示的int類型。
使用數(shù)據(jù)上下文連接數(shù)據(jù)庫
這可能是最輕量級的解決方案了,因?yàn)椴恍枰砑宇~外的類,也不再需要租戶提供程序。而且使用IHttpContextAccessor很容易獲得當(dāng)前host的header。
public class PlaylistContext : DbContext{ ?
? ?private int _tenantId; ?
?? ?private string _tenantHost; public DbSet<Playlist> Playlists { get; set; }
????public DbSet<Song> Songs { get; set; }
????public PlaylistContext(DbContextOptions<PlaylistContext> options, ? ? ? ? ? ? ? ? ? ? ? ? ? IHttpContextAccessor accessor) ? ? ? ?: base(options) ? ?{_tenantHost = accessor.HttpContext.Request.Host.Value;}
????protected override void OnModelCreating(ModelBuilder modelBuilder) ?
? ?{ ? ? ?
????var connection = Database.GetDbConnection(); ??
????using (var command = connection.CreateCommand()){connection.Open();command.CommandText = "select ID from Tenants where Host=@Host";command.CommandType = CommandType.Text; var param = command.CreateParameter();param.ParameterName = "@Host";param.Value = _tenantHost;command.Parameters.Add(param);_tenantId = (int)command.ExecuteScalar();connection.Close();} foreach (var type in GetEntityTypes()) ? ? ? ?{ ? ?
? ? ? ?var method = SetGlobalQueryMethod.MakeGenericMethod(type);method.Invoke(this, new object[] { modelBuilder });} base.OnModelCreating(modelBuilder);} // Other methods follow}
上面的代碼是基于數(shù)據(jù)上下文所持有的數(shù)據(jù)庫連接創(chuàng)建命令,并運(yùn)行sql命令,以通過host的header來獲取租戶ID。
這個(gè)解決方案的代碼量是比較少的,但是它會用主機(jī)名檢測內(nèi)部細(xì)節(jié)的方法來污染數(shù)據(jù)上下文。
為租戶使用單獨(dú)的數(shù)據(jù)上下文
第二種方法是使用單獨(dú)的web應(yīng)用程序訪問特定的租戶上下文。可以編寫租戶提供程序(請參閱我的Entity Framework core 2.0 全局查詢過濾器),并將其注入到主數(shù)據(jù)上下文
讓我們從文章開頭提到的租戶表開始。
public class Tenant{ ?
? ?public int Id { get; set; } ?
???public string Name { get; set; } ?
???public string Host { get; set; } }
現(xiàn)在,讓我們構(gòu)建租戶數(shù)據(jù)上下文。這個(gè)上下文不依賴于其他有依賴關(guān)系的自定義接口和類。它只使用租戶模型。請注意,租戶集是私有的,其他類只能通過host的header查詢租戶ID。
public class TenantsContext : DbContext{ ?
? ?private DbSet<Tenant> Tenants { get; set; } public TenantsContext(DbContextOptions<TenantsContext> options) ? ? ? ?: base(options) ? ?{}
? ?protected override void OnModelCreating(ModelBuilder modelBuilder) ? ?{modelBuilder.Entity<Tenant>().HasKey(e => e.Id);}
? ?public int GetTenantId(string host) ? ?{ ? ? ??var tenant = Tenants.FirstOrDefault(t => t.Host == host); ? ? ? ?if(tenant == null){ ? ? ? ? ? ?return 0;} return tenant.Id;} }
現(xiàn)在是時(shí)候回到ITenantProvider并編寫使用租戶數(shù)據(jù)上下文的實(shí)現(xiàn)了。這個(gè)提供程序包含檢測host的header和獲取租戶ID的所有邏輯,在實(shí)際應(yīng)用中它將更加復(fù)雜,但是在這里我將使用簡單的版本。
public class WebTenantProvider : ITenantProvider{
? ?private int _tenantId; public WebTenantProvider(IHttpContextAccessor accessor, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?TenantsContext context) ? ?{ ?
? ?? ? ?var host = accessor.HttpContext.Request.Host.Value;_tenantId = context.GetTenantId(host);} public int GetTenantId() ? ?{ ?
? ?? ? ?? ? ?return _tenantId;} }
現(xiàn)在,需要檢查租戶并找到它的ID,因?yàn)橐呀?jīng)到了重新編寫主數(shù)據(jù)上下文的時(shí)候了,所以它使用新的租戶提供程序。
public class PlaylistContext : DbContext{ ?
? ?private int _tenantId; public DbSet<Playlist> Playlists { get; set; } ?
? ?
? ?public DbSet<Song> Songs { get; set; }
? ?public PlaylistContext(DbContextOptions<PlaylistContext> options, ? ? ? ? ? ? ? ? ? ? ? ? ? ITenantProvider tenantProvider) ? ? ? ?: base(options) ? ?{_tenantId = tenantProvider.GetTenantId();}
? ?protected override void OnModelCreating(ModelBuilder modelBuilder) ? ?{ ? ? ? ?foreach (var type in GetEntityTypes()) ? ?
? ??{ ?var method = SetGlobalQueryMethod.MakeGenericMethod(type);method.Invoke(this, new object[] { modelBuilder });} base.OnModelCreating(modelBuilder);} // Other methods follow}
在web應(yīng)用程序的啟動類中,必須在ConfigureServices()方法中 為框架級定義的所有依賴項(xiàng)進(jìn)行依賴注入。
public void ConfigureServices(IServiceCollection services){services.AddMvc(); var connection = Configuration["ConnectionString"];services.AddEntityFrameworkSqlServer();services.AddDbContext<PlaylistContext>(options => options.UseSqlServer(connection));services.AddDbContext<TenantsContext>(options => options.UseSqlServer(connection));services.AddScoped<ITenantProvider, WebTenantProvider>();services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); }
這個(gè)解決方案更優(yōu)雅,因?yàn)樗鼘⑴c租戶相關(guān)的功能從主數(shù)據(jù)上下文中移出。ITenantProvider是主數(shù)據(jù)上下文唯一必須知道的東西,現(xiàn)在它也可以在其他不一定是web應(yīng)用程序的項(xiàng)目中使用。
將租戶信息存儲在云存儲中
我現(xiàn)在說的是,租戶并不是一直都在使用,而不是租戶提供程序查詢數(shù)據(jù)庫,在需要的時(shí)候可以緩存租戶信息,并在需要時(shí)更新它。考慮到云的場景,最好讓租戶信息在web應(yīng)用程序的多個(gè)實(shí)例中都可以訪問。我的選擇是云存儲。
讓我們從json格式的簡單的租戶文件開始,讓我們期望它是一些內(nèi)部應(yīng)用程序或后臺任務(wù)的職責(zé),以使這個(gè)文件保持最新。這是我使用的樣本文件。
[{"Id": 2,"Name": "Local host","Host": "localhost:30172"},{"Id": 3,"Name": "Customer X","Host": "localhost:3331"},{"Id": 4,"Name": "Customer Y","Host": "localhost:33111"} ]
要讀取云存儲應(yīng)用程序中的文件,需要了解存儲帳戶連接字符串、容器名稱和云名稱。Blob是租戶文件。我再次使用ITenantProvider接口,并為Azure 云存儲創(chuàng)建了一個(gè)新的實(shí)現(xiàn)。我把它叫做BlobStorageTenantProvider。它很簡單,不需要考慮很多實(shí)際的方面,比如刷新租戶信息和處理鎖。
public class BlobStorageTenantProvider : ITenantProvider{ ? ?
private static IList<Tenant> _tenants; private int _tenantId = 0;
public BlobStorageTenantProvider(IHttpContextAccessor accessor, IConfiguration conf) ?
?{ ? ? ?if(_tenants == null){LoadTenants(conf["StorageConnectionString"], conf["TenantsContainerName"], conf["TenantsBlobName"]);} var host = accessor.HttpContext.Request.Host.Value; ? ??
var tenant = _tenants.FirstOrDefault(t => t.Host.ToLower() == host.ToLower()); ? ? ?
if(tenant != null){_tenantId = tenant.Id;}}
private void LoadTenants(string connStr, string containerName, string blobName) ? ?{ ? ? ?
var storageAccount = CloudStorageAccount.Parse(connStr); ?
? ? ? ? ?var blobClient = storageAccount.CreateCloudBlobClient(); ? ?
? ? ? ? ?var container = blobClient.GetContainerReference(containerName); ? ? ? ?var blob = container.GetBlobReference(blobName);blob.FetchAttributesAsync().GetAwaiter().GetResult(); var fileBytes = new byte[blob.Properties.Length]; using (var stream = blob.OpenReadAsync().GetAwaiter().GetResult()) ? ? ? ? ? using (var textReader = new StreamReader(stream)) ?
? ? ? ?using (var reader = new JsonTextReader(textReader)){_tenants = JsonSerializer.Create().Deserialize<List<Tenant>>(reader);}}
? ? ? ?public int GetTenantId() ? ?{ ? ?
? ? ? ?return _tenantId;} }
提供者的代碼可能不是很好,但是它比以前的代碼好,因?yàn)椴恍枰~外的數(shù)據(jù)庫調(diào)用,而且租戶id是由內(nèi)存服務(wù)的。
用host的header的哈希值作為租戶ID
第三種方法是最簡單的方法,但這意味著租戶ID與host的 header相同,或者從它派生而來。我不喜歡這種做法,因?yàn)槿绻蛻粝胍膆ost的 header,那么更改將分布在整個(gè)數(shù)據(jù)庫中。客戶可能希望從服務(wù)自動提供的自定義主機(jī)名開始,然后使用他們自己的子域名。
這里是作為主機(jī)名的租戶ID的代碼。
public class PlaylistContext : DbContext{ ? ?
? private string _tenantId; public DbSet<Playlist> Playlists { get; set; }
? ?public DbSet<Song> Songs { get; set; }
? ?public PlaylistContext(DbContextOptions<PlaylistContext> options, ? ? ? ? ? ? ? ? ? ? ? ? ? ?IHttpContextAccessor accessor) ? ? ? ?: base(options) ? ?{_tenantId = accessor.HttpContext.Request.Host.Value;}
? ?protected override void OnModelCreating(ModelBuilder modelBuilder) ? ?{ ? ? ? ?foreach (var type in GetEntityTypes()) ?
? ? ?{ var method = SetGlobalQueryMethod.MakeGenericMethod(type);method.Invoke(this, new object[] { modelBuilder });} base.OnModelCreating(modelBuilder);} // Other methods follow}
可以使用MD5代替主機(jī)的名稱,但它不會改變主機(jī)的問題。
總結(jié)
這篇文章是關(guān)于在Entity Framework Core 2.0中真正的去利用全局查詢過濾器。雖然這里所展示的代碼是簡單的而不我們實(shí)際運(yùn)用場景所需要的,但在構(gòu)建真正的解決方案之前,它們?nèi)匀皇呛芎玫睦印N冶M量讓解決方案盡可能的接近完美的架構(gòu)原則。我認(rèn)為讀者他們自己的多租戶應(yīng)用程序可以在這里提供的解決方案中獲得幫助。
相關(guān)文章:?
.NET Core 2.0 正式發(fā)布信息匯總
.NET Standard 2.0 特性介紹和使用指南
.NET Core 2.0 的dll實(shí)時(shí)更新、https、依賴包變更問題及解決
.NET Core 2.0 特性介紹和使用指南
Entity Framework Core 2.0 新特性
體驗(yàn) PHP under .NET Core
.NET Core 2.0使用NLog
升級項(xiàng)目到.NET Core 2.0,在Linux上安裝Docker,并成功部署
解決Visual Studio For Mac Restore失敗的問題
ASP.NET Core 2.0 特性介紹和使用指南
Entity Framework Core 2.0 全局查詢過濾器
Entity Framework Core 2.0 特性介紹和使用指南
原文地址:http://www.cnblogs.com/chen-jie/p/tenant-providers.html
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的在 ASP.NET Core 中执行租户服务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET平台微服务项目汇集
- 下一篇: 基于.NET CORE微服务框架 -谈谈