在 ASP.NET Core 中执行租户服务
本博文翻譯自:
http://gunnarpeipman.com/2017/08/tenant-providers/
在我之前關于?Entity Framework core 2.0 全局查詢過濾器的文章中,我提出了一個想法,當構建模型時,如何自動地將查詢過濾器應用到所有的領域實體中,也就是說領域實體總是來自同一租戶。這篇文章更深入地介紹了在 ASP.NET Core 應用程序中檢測當前租戶的可能解決方案,并建議一些租戶提供者將為實際應用程序中提供多租戶的支持作為出發點。
注意!?請閱讀我之前在Entity Framework core 2.0 全局查詢過濾器中的文章,這篇文章將繼續下去,并期待讀者熟悉我為多租戶提供的解決方案。另外,將多租戶規則應用到所有領域實體的方法是從我以前的全局查詢過濾器中獲取的,而不是在這里復制的。
如何檢測當前租戶?
情況是這樣的。數據上下文是在請求傳入和構建模型全局查詢過濾器時構建的。其中一個過濾器是關于當前租戶的。在代碼中還需要租戶ID,但模型還沒有準備好。同一時間,租戶ID只能在數據庫中使用。我們該怎么辦?
一些想法:
在數據上下文中使用數據庫連接,并對租戶表進行直接查詢
為租戶的信息和操作使用單獨的數據上下文
保持租戶信息在云存儲上可用
使用域名的哈希值作為租戶ID
注意!?在本文中,我希望在web應用程序中通過host的header檢測租戶。
我在這篇文章中使用的租戶表如下圖所示。
注意!?依賴于解決方案的租戶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,那么更改將分布在整個數據庫中??蛻艨赡芟M麖姆兆詣犹峁┑淖远x主機名開始,然后使用他們自己的子域名。
這里是作為主機名的租戶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中真正的去利用全局查詢過濾器。雖然這里所展示的代碼是簡單的而不我們實際運用場景所需要的,但在構建真正的解決方案之前,它們仍然是很好的例子。我盡量讓解決方案盡可能的接近完美的架構原則。我認為讀者他們自己的多租戶應用程序可以在這里提供的解決方案中獲得幫助。
相關文章:?
.NET Core 2.0 正式發布信息匯總
.NET Standard 2.0 特性介紹和使用指南
.NET Core 2.0 的dll實時更新、https、依賴包變更問題及解決
.NET Core 2.0 特性介紹和使用指南
Entity Framework Core 2.0 新特性
體驗 PHP under .NET Core
.NET Core 2.0使用NLog
升級項目到.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社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的在 ASP.NET Core 中执行租户服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET平台微服务项目汇集
- 下一篇: 基于.NET CORE微服务框架 -谈谈