在 ASP.NET Core 中執行租戶服務

不定時更新翻譯系列,此係列更新毫無時間規律,文筆菜翻譯菜求各位看官老爺們輕噴,如以爲我翻譯有問題請挪步原博客地址javascript

本博文翻譯自:
http://gunnarpeipman.com/2017/08/tenant-providers/html

在我以前關於 Entity Framework core 2.0 全局查詢過濾器的文章中,我提出了一個想法,當構建模型時,如何自動地將查詢過濾器應用到全部的領域實體中,也就是說領域實體老是來自同一租戶。這篇文章更深刻地介紹了在 ASP.NET Core 應用程序中檢測當前租戶的可能解決方案,並建議一些租戶提供者將爲實際應用程序中提供多租戶的支持做爲出發點。java

注意! 請閱讀我以前在Entity Framework core 2.0 全局查詢過濾器中的文章,這篇文章將繼續下去,並期待讀者熟悉我爲多租戶提供的解決方案。另外,將多租戶規則應用到全部領域實體的方法是從我之前的全局查詢過濾器中獲取的,而不是在這裏複製的。web

如何檢測當前租戶?

狀況是這樣的。數據上下文是在請求傳入和構建模型全局查詢過濾器時構建的。其中一個過濾器是關於當前租戶的。在代碼中還須要租戶ID,但模型尚未準備好。同一時間,租戶ID只能在數據庫中使用。咱們該怎麼辦?sql

一些想法:數據庫

  • 在數據上下文中使用數據庫鏈接,並對租戶表進行直接查詢
  • 爲租戶的信息和操做使用單獨的數據上下文
  • 保持租戶信息在雲存儲上可用
  • 使用域名的哈希值做爲租戶ID

注意! 在本文中,我但願在web應用程序中經過host的header檢測租戶。json

我在這篇文章中使用的租戶表以下圖所示。緩存

ef-core-tenants-table

注意! 依賴於解決方案的租戶ID也能夠是其餘的,而不是像上圖所示的int類型。架構

使用數據上下文鏈接數據庫

這多是最輕量級的解決方案了,由於不須要添加額外的類,也再也不須要租戶提供程序。並且使用IHttpContextAccessor很容易得到當前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 }

上面的代碼是基於數據上下文所持有的數據庫鏈接建立命令,並運行sql命令,以經過host的header來獲取租戶ID。

這個解決方案的代碼量是比較少的,可是它會用主機名檢測內部細節的方法來污染數據上下文。

爲租戶使用單獨的數據上下文

第二種方法是使用單獨的web應用程序訪問特定的租戶上下文。能夠編寫租戶提供程序(請參閱個人Entity Framework core 2.0 全局查詢過濾器),並將其注入到主數據上下文

讓咱們從文章開頭提到的租戶表開始。


public class Tenant { public int Id { get; set; } public string Name { get; set; } public string Host { get; set; } }

如今,讓咱們構建租戶數據上下文。這個上下文不依賴於其餘有依賴關係的自定義接口和類。它只使用租戶模型。請注意,租戶集是私有的,其餘類只能經過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; } }

如今是時候回到ITenantProvider並編寫使用租戶數據上下文的實現了。這個提供程序包含檢測host的header和獲取租戶ID的全部邏輯,在實際應用中它將更加複雜,可是在這裏我將使用簡單的版本。


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; } }

如今,須要檢查租戶並找到它的ID,由於已經到了從新編寫主數據上下文的時候了,因此它使用新的租戶提供程序。


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應用程序的啓動類中,必須在ConfigureServices()方法中 爲框架級定義的全部依賴項進行依賴注入。


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>(); }

這個解決方案更優雅,由於它將與租戶相關的功能從主數據上下文中移出。ITenantProvider是主數據上下文惟一必須知道的東西,如今它也能夠在其餘不必定是web應用程序的項目中使用。

將租戶信息存儲在雲存儲中

我如今說的是,租戶並非一直都在使用,而不是租戶提供程序查詢數據庫,在須要的時候能夠緩存租戶信息,並在須要時更新它。考慮到雲的場景,最好讓租戶信息在web應用程序的多個實例中均可以訪問。個人選擇是雲存儲。

讓咱們從json格式的簡單的租戶文件開始,讓咱們指望它是一些內部應用程序或後臺任務的職責,以使這個文件保持最新。這是我使用的樣本文件。


[
  {
    "Id": 2, "Name": "Local host", "Host": "localhost:30172" }, { "Id": 3, "Name": "Customer X", "Host": "localhost:3331" }, { "Id": 4, "Name": "Customer Y", "Host": "localhost:33111" } ]

要讀取雲存儲應用程序中的文件,須要瞭解存儲賬戶鏈接字符串、容器名稱和雲名稱。Blob是租戶文件。我再次使用ITenantProvider接口,併爲Azure 雲存儲建立了一個新的實現。我把它叫作BlobStorageTenantProvider。它很簡單,不須要考慮不少實際的方面,好比刷新租戶信息和處理鎖。


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; } }

提供者的代碼可能不是很好,可是它比之前的代碼好,由於不須要額外的數據庫調用,並且租戶id是由內存服務的。

用host的header的哈希值做爲租戶ID

第三種方法是最簡單的方法,但這意味着租戶ID與host的 header相同,或者從它派生而來。我不喜歡這種作法,由於若是客戶想要更改host的 header,那麼更改將分佈在整個數據庫中。客戶可能但願從服務自動提供的自定義主機名開始,而後使用他們本身的子域名。

這裏是做爲主機名的租戶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代替主機的名稱,但它不會改變主機的問題。

總結

這篇文章是關於在Entity Framework Core 2.0中真正的去利用全局查詢過濾器。雖然這裏所展現的代碼是簡單的而不咱們實際運用場景所須要的,但在構建真正的解決方案以前,它們仍然是很好的例子。我儘可能讓解決方案儘量的接近完美的架構原則。我認爲讀者他們本身的多租戶應用程序能夠在這裏提供的解決方案中得到幫助。

歡迎轉載,轉載請註明翻譯原文出處(本文章),原文出處(原博客地址),而後謝謝觀看

若是以爲個人翻譯對您有幫助,請點擊推薦支持:)

相關文章
相關標籤/搜索