Asp.net core下利用EF core實現從資料實現多租戶(1)
前言
隨著網際網路的的高速發展,大多數的公司由於一開始使用的傳統的硬體/軟體架構,導致在業務不斷髮展的同時,系統也逐漸地逼近傳統結構的極限。
於是,系統也急需進行結構上的升級換代。
在服務端,系統的I/O是很大的瓶頸。其中資料庫的I/O最容易成為限制系統效率的一環。在優化資料庫I/O這一環中,可以從優化系統呼叫資料庫效率、資料庫自身效率等多方面入手。
一般情況下,通過升級資料庫伺服器的硬體是最容易達到的。但是伺服器資源不可能無限擴大,於是從呼叫資料庫的效率方面入手是目前主流的優化方向。
於是讀寫分離、分庫分表成為了軟體系統的重要一環。並且需要在傳統的系統架構下,是需要做強入侵性的修改。
什麼是多租戶:
多租戶的英文是Multiple Tenancy,很多人把多租戶和Saas劃上等號,其實這還是有區別的。我們今天不討論Sass這種如此廣泛的議題。
現在很多的系統都是to B的,它們面向的是組織、公司和商業機構等。每個機構會有獨立的組織架構,獨立的訂單結構,獨立的服務等級和收費。
這就造成了各個機構間的資料是天然獨立的,特別是部分的公司對資料的獨立和安全性會有較高要求,往往資料是需要獨立儲存的。
由於多租戶資料的天然獨立,造成了系統可以根據機構的不同進行分庫分表。所以這裡討論的多租戶,僅限於資料層面的!
寫這篇文章原因
其實由於一個群的朋友問到了相關的問題,由於當時我並沒有dotnet環境,所以簡單地寫了幾句程式碼,我本身是不知道程式碼是否正確的。
在我有空的時候,試了一下原來是可實施的。我貼上當時隨手寫的核心程式碼,其中connenctionResolver是需要自己建立的。
這程式碼是能用的,如果對於asp.net core很熟悉的人,把這段程式碼放入到ConfigureServices方法內即可。
但是我還是強烈建議大家跟著我的介紹逐步實施。
1 services.AddDbContext<MyContext>((serviceProvider, options)=> 2 { 3 var connenctionResolver = serviceProvider.GetService<IConnectionResolver>(); 4 options.UseSqlServer(connenctionResolver.ConnectionString); 5 });
實施
專案介紹
這個Demo,主要通過根據http request header來獲取不同的租戶的標識,從而達到區分租戶最終實現資料的隔離。
專案依賴:
1. .net core app 3.1。在機器上安裝好.net core SDK, 版本3.1
2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包
3. EF core,Microsoft.EntityFrameworkCore
專案中必須物件是什麼:
1. DbContext和對應資料庫物件
2. ConnenctionResolver, 用於獲取連線字串
3. TenantInfo, 用於表示租戶資訊
4. TenantInfoMiddleware,用於在asp.net core管道根據http的內容從而解析出TenantInfo
5. Controller, 用於實施對應的
實施步驟
1. 建立TenanInfo 和 TenantInfoMiddleware. TenanInfo 作為租戶的資訊,通過IOC建立,並且在TenantInfoMiddleware通過解析http request header,修改TenantInfo
1 using System; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 4 { 5 public class TenantInfo 6 { 7 public string Name { get; set; } 8 } 9 }
1 using System; 2 using System.Threading.Tasks; 3 using Microsoft.AspNetCore.Http; 4 using Microsoft.Extensions.DependencyInjection; 5 6 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 7 { 8 public class TenantInfoMiddleware 9 { 10 private readonly RequestDelegate _next; 11 12 public TenantInfoMiddleware(RequestDelegate next) 13 { 14 _next = next; 15 } 16 17 public async Task InvokeAsync(HttpContext context) 18 { 19 var tenantInfo = context.RequestServices.GetRequiredService<TenantInfo>(); 20 var tenantName = context.Request.Headers["Tenant"]; 21 22 if (string.IsNullOrEmpty(tenantName)) 23 tenantName = "default"; 24 25 tenantInfo.Name = tenantName; 26 27 // Call the next delegate/middleware in the pipeline 28 await _next(context); 29 } 30 } 31 }TenantInfoMiddleware
2. 建立HttpHeaderSqlConnectionResolver並且實現ISqlConnectionResolver介面。這裡要做的事情很簡單,直接同TenantInfo取值,並且在配置檔案查詢對應的connectionString。
其實這個實現類在正常的業務場景是需要包含邏輯的,但是在Demo裡為了簡明扼要,就使用最簡單的方式實現了。
1 using System; 2 using Microsoft.Extensions.Configuration; 3 4 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 5 { 6 public interface ISqlConnectionResolver 7 { 8 string GetConnection(); 9 10 } 11 12 public class HttpHeaderSqlConnectionResolver : ISqlConnectionResolver 13 { 14 private readonly TenantInfo tenantInfo; 15 private readonly IConfiguration configuration; 16 17 public HttpHeaderSqlConnectionResolver(TenantInfo tenantInfo, IConfiguration configuration) 18 { 19 this.tenantInfo = tenantInfo; 20 this.configuration = configuration; 21 } 22 public string GetConnection() 23 { 24 var connectionString = configuration.GetConnectionString(this.tenantInfo.Name); 25 if(string.IsNullOrEmpty(connectionString)){ 26 throw new NullReferenceException("can not find the connection"); 27 } 28 return connectionString; 29 } 30 } 31 }ConnectionResolver
3. 建立類MultipleTenancyExtension,裡面包含最重要的配置資料庫連線字串的方法。其中裡面的DbContext並沒有使用泛型,是為了更加簡明點
1 using kiwiho.Course.MultipleTenancy.EFcore.Api.DAL; 2 using Microsoft.Extensions.Configuration; 3 using Microsoft.Extensions.DependencyInjection; 4 using Microsoft.EntityFrameworkCore; 5 6 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 7 { 8 public static class MultipleTenancyExtension 9 { 10 public static IServiceCollection AddConnectionByDatabase(this IServiceCollection services) 11 { 12 services.AddDbContext<StoreDbContext>((serviceProvider, options)=> 13 { 14 var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>(); 15 16 options.UseMySql(resolver.GetConnection()); 17 }); 18 19 return services; 20 } 21 } 22 }MultipleTenancyExtension
4. 在Startup類中配置依賴注入和把TenantInfoMiddleware加入到管道中。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddScoped<TenantInfo>(); 4 services.AddScoped<ISqlConnectionResolver, HttpHeaderSqlConnectionResolver>(); 5 services.AddConnectionByDatabase(); 6 services.AddControllers(); 7 }ConfigureServices
在Configure內,在UseRouting前把TenantInfoMiddleware加入到管道
1 app.UseMiddleware<TenantInfoMiddleware>();
5. 配置好DbContext和對應的資料庫物件
1 using Microsoft.EntityFrameworkCore; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL 4 { 5 public class StoreDbContext : DbContext 6 { 7 public DbSet<Product> Products { get; set; } 8 public StoreDbContext(DbContextOptions options) : base(options) 9 { 10 } 11 } 12 13 }StoreDbContext
1 using System.ComponentModel.DataAnnotations; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL 4 { 5 public class Product 6 { 7 [Key] 8 public int Id { get; set; } 9 10 [StringLength(50), Required] 11 public string Name { get; set; } 12 13 [StringLength(50)] 14 public string Category { get; set; } 15 16 public double? Price { get; set; } 17 } 18 }Product
6. 建立ProductController, 並且在裡面新增3個方法,分別是建立,查詢所有,根據id查詢。在建構函式通過EnsureCreated以達到在資料庫不存在是自動建立資料庫。
1 using System; 2 using System.Collections.Generic; 3 using System.Threading.Tasks; 4 using kiwiho.Course.MultipleTenancy.EFcore.Api.DAL; 5 using Microsoft.AspNetCore.Mvc; 6 using Microsoft.EntityFrameworkCore; 7 8 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Controllers 9 { 10 [ApiController] 11 [Route("api/Products")] 12 public class ProductController : ControllerBase 13 { 14 private readonly StoreDbContext storeDbContext; 15 16 public ProductController(StoreDbContext storeDbContext){ 17 this.storeDbContext = storeDbContext; 18 this.storeDbContext.Database.EnsureCreated(); 19 } 20 21 [HttpPost("")] 22 public async Task<ActionResult<Product>> Create(Product product){ 23 var rct = await this.storeDbContext.Products.AddAsync(product); 24 25 await this.storeDbContext.SaveChangesAsync(); 26 27 return rct?.Entity; 28 29 } 30 31 [HttpGet("{id}")] 32 public async Task<ActionResult<Product>> Get([FromRoute] int id){ 33 34 var rct = await this.storeDbContext.Products.FindAsync(id); 35 36 return rct; 37 38 } 39 40 [HttpGet("")] 41 public async Task<ActionResult<List<Product>>> Search(){ 42 var rct = await this.storeDbContext.Products.ToListAsync(); 43 return rct; 44 } 45 } 46 }ProductController
驗證效果
1. 啟動專案
2. 通過postman在store1中建立一個Orange,在store2中建立一個cola。要注意的是Headers仲的Tenant:store1是必須的。
圖片就只截了store1的例子
3. 分別在store1,store2中查詢所有product
store1:只查到了Orange
store2: 只查到了cola
4. 通過查詢資料庫驗證資料是否已經隔離。可能有人會覺得為什麼2個id都是1。是因為Product的Id使用 [Key] ,資料庫的id是自增長的。
其實這是故意為之的,為的是更好的展示這2個物件是在不同的資料庫
store1:
store2:
總結
這是一個很簡單的例子,似乎把前言讀完就已經能實現,那麼為什麼還要花費那麼長去介紹呢。
這其實是一個系列文章,這裡只做了最簡單的介紹。具體來說,它真的是一個Demo。
接下來要做什麼:
在很多實際場景中,其實一個機構一個數據庫,這種模式似乎太重了,而且每個機構都需要部署資料庫伺服器和例項好像很難自動化。
並且,大多數的機構,其實完全沒有必要獨立一個數據庫的。可以通過分表,分Schema實現資料隔離。
所以接下來我會介紹怎麼利用EFCore的現有介面實施。並且最終把核心程式碼做成類庫,並且結合MySql,SqlServer做成擴充套件
關於程式碼
文章中的程式碼並非全部程式碼,如果僅僅拷貝文章的程式碼可能還不足以實施。但是關鍵程式碼已經全部貼出
程式碼全部放到github上了。這是part1,請checkout分支part1. 或者在master分支上的part1資料夾內。
可以檢視master上commit tag是part1 的部分
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part1
&n