前言
隨着互聯網的的高速發展,大多數的公司由於一開始使用的傳統的硬件/軟件架構,導致在業務不斷發展的同時,系統也逐漸地逼近傳統結構的極限。
於是,系統也急需進行結構上的升級換代。
在服務端,系統的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 });
系列文章目錄
主線文章
Asp.net core下利用EF core實現從數據實現多租戶(1) (本文)
Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離
Asp.net core下利用EF core實現從數據實現多租戶(3): 按Schema分離 附加:EF Migration 操作
附加文章
EF core (code first) 通過自定義 Migration History 實現多租戶使用同一數據庫時更新數據庫結構
EF core (code first) 通過自動遷移實現多租戶數據分離 :按Schema分離數據
實施
項目介紹
這個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, 版本3.1.1。這里必須要用3.1的,因為ef core3.0是面向.net standard 2.1.
項目中必須對象是什么:
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 }
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 }
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 }
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 }
在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 }

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 }
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 }
驗證效果
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