前言
寫這篇文章的原因,其實由於我寫EF core 實現多租戶的時候,遇到的問題。
具體文章的鏈接:
Asp.net core下利用EF core實現從數據實現多租戶(1)
Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離 (主要關聯文章)
這里我遇到的最主要問題是:由於多租戶的表使用的是同一個數據庫。由於這個原因,無法通過 Database.EnsureCreated() 自動創建多個結構相同但名字不同的表。
所以我在文中提到,需要自己跑腳本去創建多有的表。
雖然我依然認為在多租戶的情況下使用sql管理表是更可靠的方案,但如果可以利用EF core原生提供的Migration機制,在運行時自動創建和更新數據表結構,那更加友好。
實現的思路
其實我們都知道,EF core (code first) 會在數據庫中生成唯一一個 __EFMigrationHistory 表,數據庫的版本記錄在這里。
在我們文章的場景下,由於有多個租戶同時使用,同一個表結構(Products)會出現多次,那么意思就是一個 __EFMigrationHistory 無法同時記錄多個租戶的數據表版本。
好了,既然問題的關鍵已經知道了,我們可以在這里先把答案揭曉,在下問在詳細說明實現方法:
圖中可以看到,我們自定義MigrationHistory表,並且在一個數據下,同時出現了store1和store2的 MigrationHistory 表。
實施
項目介紹
這是一個多租戶系統,具體來說就是根據不同的租戶,創建相同的所有數據表。
項目依賴:
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.
4. EF core design, Microsoft.EntityFrameworkCore.Design, 版本 3.1.1
5. dotnet-ef tool, 版本 3.1.1
關鍵的對象:
1. MigrationsAssembly, 利用此類去實現創建對應的Migration單元。
2. Migration files, 這里指的是一批Migration相關的文件,利用執行dotnet-ef 命令生成具體的文件,從而真正地去創建和更新數據庫。
實施步驟
1. 運行dotnet-ef命令,生成Migration files
命令:
1 dotnet-ef migrations add init
執行后,會在項目中的Migrations文件夾下生成多個*.cs文件,其實他們也是可執行C#對象
機構如下:
這3個文件中,主要起作用的是*_init.cs這個文件
打開之后我們需要對他進行修改

1 using Microsoft.EntityFrameworkCore.Metadata; 2 using Microsoft.EntityFrameworkCore.Migrations; 3 4 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations 5 { 6 public partial class init : Migration 7 { 8 private readonly string prefix; 9 public init(string prefix) 10 { 11 if (string.IsNullOrEmpty(prefix)) 12 { 13 throw new System.ArgumentNullException(); 14 } 15 this.prefix = prefix; 16 } 17 18 protected override void Up(MigrationBuilder migrationBuilder) 19 { 20 migrationBuilder.CreateTable( 21 name: prefix + "_Products", 22 columns: table => new 23 { 24 Id = table.Column<int>(nullable: false) 25 .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), 26 Name = table.Column<string>(maxLength: 50, nullable: false), 27 Category = table.Column<string>(maxLength: 50, nullable: true), 28 Price = table.Column<double>(nullable: true) 29 }, 30 constraints: table => 31 { 32 table.PrimaryKey("PK__Products", x => x.Id); 33 }); 34 } 35 36 protected override void Down(MigrationBuilder migrationBuilder) 37 { 38 migrationBuilder.DropTable( 39 name: prefix + "_Products"); 40 } 41 } 42 }
這里修改的主要是:
1.1 新增構造函數,並且在里面添加一個 prefix 參數。
1.2 在Up方法中,對table Name進行修改,把prefix變量加在_Product前面(第21行)
1.3 在Down方法中,對table Name進行修改,把prefix變量加在_Product前面 (第39行)
2. 創建 MigrationByTenantAssembly 文件。
由於上一步講Migration file的構造函數修改了,理論上EF core已經五法通過默認的方式成功執行改Migration file了

1 using System; 2 using System.Reflection; 3 using Microsoft.EntityFrameworkCore; 4 using Microsoft.EntityFrameworkCore.Diagnostics; 5 using Microsoft.EntityFrameworkCore.Infrastructure; 6 using Microsoft.EntityFrameworkCore.Migrations; 7 using Microsoft.EntityFrameworkCore.Migrations.Internal; 8 9 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 10 { 11 public class MigrationByTenantAssembly : MigrationsAssembly 12 { 13 private readonly DbContext context; 14 15 public MigrationByTenantAssembly(ICurrentDbContext currentContext, 16 IDbContextOptions options, IMigrationsIdGenerator idGenerator, 17 IDiagnosticsLogger<DbLoggerCategory.Migrations> logger) 18 : base(currentContext, options, idGenerator, logger) 19 { 20 context = currentContext.Context; 21 } 22 23 public override Migration CreateMigration(TypeInfo migrationClass, 24 string activeProvider) 25 { 26 if (activeProvider == null) 27 throw new ArgumentNullException($"{nameof(activeProvider)} argument is null"); 28 29 var hasCtorWithSchema = migrationClass 30 .GetConstructor(new[] { typeof(string) }) != null; 31 32 if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext) 33 { 34 var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.TenantInfo?.Name); 35 instance.ActiveProvider = activeProvider; 36 return instance; 37 } 38 39 return base.CreateMigration(migrationClass, activeProvider); 40 } 41 } 42 }
這個類中沒有什么特別的,關鍵在於29~37行。首先需要判斷目標 Migration 對象的是否有一個構造函數的參數有且僅有一個string 類型
判斷DbContext是否有實現ITenantDbContext接口。
利用 Activator 創建 Migration 實例(把tenant Name傳進構造函數)
3. 在 MultipleTenancyExtension 類的AddDatabase方法中,添加自定義MigrationHistory表名
1 var dbOptionBuilder = options.UseMySql(resolver.GetConnection(), builder => 2 { 3 if (option.Type == ConnectionResolverType.ByTabel) 4 { 5 builder.MigrationsHistoryTable(${tenantInfo.Name}__EFMigrationsHistory"); 6 } 7 }); 8 9 10 dbOptionBuilder.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IMigrationsAssembly, MigrationByTenantAssembly>();
最關鍵的一點是第5行,調用 MigrationsHistoryTable 設置MigrationHistory表名
另外一點是第10行,用 MigrationByTenantAssembly 類替換 EF core 中默認的實現(IMigrationsAssembly接口)
4. 在ProductController的構造函數中,修改成如下
Database.Migrate 的作用主要是在運行時可以執行數據庫的創建和更新
1 public ProductController(StoreDbContext storeDbContext) 2 { 3 this.storeDbContext = storeDbContext; 4 this.storeDbContext.Database.Migrate(); 5 }
查看效果
調用接口
跟系列文章一樣,我們先調用創建product的接口分別在store1和store2中添加記錄。
下面是store1 的查詢結果
store2的查詢結果
查看數據庫驗證數據
數據庫的表結構
store1_Products 表數據
store2_Products 表數據
總結
本文中我們介紹了ef core 的code first模式下是如何更新數據庫的,並且通過添加 Migration 對象的構造函數 ,自行添加了必要參數。
通過替換EF core中默認的 IMigrationsAssembly 實現, MigrationByTenantAssembly 中自定對Migration對象實例化。
替換EF core中默認的MigrationHistory最終實現需求。
本文雖然只是一個示例,但是卻可以在真實項目中使用相同的手段以實現需求。不過還是那句話,對於多租戶情況下,我推薦使用db first模式。
關於代碼
代碼已經傳上github,請查看EF_code_first的分支的代碼。
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/EF_code_first
參考文章
Custom Migrations History Table