許多企業營運應用程式的設計目的是要與多個客戶合作。 請務必保護數據,讓客戶數據不會「外泄」或由其他客戶和潛在競爭對手看到。 這些應用程式會分類為「多租戶」,因為每個客戶都被視為應用程式的租戶,擁有自己的資料集。
警告
本文使用本機資料庫,其不需要使用者進行驗證。 實際執行應用程式應該使用可用的最安全驗證流程。 如需已部署測試與實際執行應用程式驗證的詳細資訊,請參閱安全驗證流程。
重要
本文件提供現狀的範例和解決方案。這些不是「最佳做法」,而是供您參考的「運作方式」。
提示
您可以在 GitHub 上檢視此 範例的原始程式碼
支援多租戶
在應用程式中實作多租戶架構的方法有很多種。 其中一個常見的方法(有時是需求),就是將每個客戶的數據保留在個別的資料庫中。 架構相同,但數據是客戶特定的。 另一種方法是依客戶分割現有資料庫中的數據。 這可以藉由在資料表中使用一個欄位,或在多個架構中為每個租戶建立的架構中包含資料表來完成。
方法 | 租戶的欄位? | 每個租用戶的架構? | 多個資料庫? | EF Core 支援 |
---|---|---|---|---|
判別器(欄位) | 是 | 無 | 無 | 全域查詢篩選 |
每個租戶一個資料庫 | 無 | 無 | 是 | 組態 |
租戶架構 | 無 | 是 | 無 | 不支援 |
對於每個租戶使用一個資料庫的方式,切換至正確的資料庫就像提供正確的連接字串一樣簡單。 當數據儲存在單一資料庫中時, 全域查詢篩選 可用來依租使用者標識碼自動篩選數據列,確保開發人員不會不小心撰寫程式代碼來存取其他客戶的數據。
這些範例應該在大部分的應用程式模型中正常運作,包括控制台、WPF、WinForms 和 ASP.NET Core 應用程式。 Blazor Server 應用程式需要特別考慮。
Blazor Server 應用程式和處理站的生活
在 Blazor 應用程式中使用 Entity Framework Core 的建議模式是註冊 DbContextFactory,然後呼叫它來建立每個作業的新實例DbContext
。 根據預設,工廠是單例,因此應用程式的所有使用者共享一個實例。 這通常沒問題,因為雖然共用處理站,但個別 DbContext
實例則不是。
不過,針對多租戶環境,連接字串可能因使用者而變更。 因為處理站會快取具有相同存留期的組態,這表示所有用戶都必須共用相同的組態。 因此,存留期應該變更為 Scoped
。
Blazor WebAssembly 應用程式中不會發生此問題,因為單例的範圍限定為使用者。 另一方面,Blazor Server 應用程式提出了獨特的挑戰。 雖然應用程式是 Web 應用程式,但使用 SignalR 進行即時通訊會「保持運作」。 每個使用者會建立會話,並持續超過初始要求。 應為每位使用者提供一個新的工廠,以允許新的設定。 此特殊工廠的存留期已設定範圍,而且會為每個用戶會話創建新的實例。
範例解決方案 (單一資料庫)
可能的解決方法是建立一個簡單的 ITenantService
服務,以處理使用者當前租戶的設定。 它會提供回呼功能,讓程式碼在租用戶變更時收到通知。 實施過程(為了使說明更清楚而省略了回呼)可能看起來像這樣:
namespace Common
{
public interface ITenantService
{
string Tenant { get; }
void SetTenant(string tenant);
string[] GetTenants();
event TenantChangedEventHandler OnTenantChanged;
}
}
DbContext
接著,可以管理多重租戶。 方法取決於您的資料庫策略。 如果您要將所有租使用者儲存在單一資料庫中,您可能會使用查詢篩選器。
ITenantService
會透過相依性注入傳遞至建構函式,並用來解析及儲存租戶識別符。
public ContactContext(
DbContextOptions<ContactContext> opts,
ITenantService service)
: base(opts) => _tenant = service.Tenant;
使用覆寫方法 OnModelCreating
來指定查詢篩選條件:
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<MultitenantContact>()
.HasQueryFilter(mt => mt.Tenant == _tenant);
這可確保每個查詢都會針對每個要求篩選至租戶。 應用程式程式代碼中不需要篩選,因為會自動套用全域篩選。
在應用程式啟動時,配置租戶提供者和 DbContextFactory
,如下所示,以 Sqlite 作為範例:
builder.Services.AddDbContextFactory<ContactContext>(
opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);
請注意,服務 存留期 是使用 ServiceLifetime.Scoped
設定的。 這使其能夠依賴於租戶提供者。
注意
相依性必須一律流向單一。 這表示 Scoped
服務可以相依於另一 Scoped
個 Singleton
服務或服務,但 Singleton
服務只能相依於其他 Singleton
服務: Transient => Scoped => Singleton
。
多個架構
警告
EF Core 並不直接支援此案例,而且不是建議的解決方案。
在不同的方法中,相同的資料庫可以使用資料表架構來處理 tenant1
和 tenant2
。
-
Tenant1 -
tenant1.CustomerData
-
Tenant2 -
tenant2.CustomerData
如果您未使用 EF Core 來處理帶有遷移功能的資料庫更新,且已經有多模式的資料表,則可以像這樣覆寫DbContext
OnModelCreating
中的結構(資料表CustomerData
的結構設為租賃方):
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);
多個資料庫和 連接字串
藉由為每位租戶傳遞不同的連接字串來實作多租戶資料庫版本。 這可以在啟動時設定,方法是解析服務提供者,並使用它來建置 連接字串。 在租戶區段中,連接字串已新增至appsettings.json
組態檔。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"TenantA": "Data Source=tenantacontacts.sqlite",
"TenantB": "Data Source=tenantbcontacts.sqlite"
},
"AllowedHosts": "*"
}
服務和組態都會被注入到DbContext
中。
public ContactContext(
DbContextOptions<ContactContext> opts,
IConfiguration config,
ITenantService service)
: base(opts)
{
_tenantService = service;
_configuration = config;
}
接著使用租用戶來查閱OnConfiguring
中的連接字串:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenant = _tenantService.Tenant;
var connectionStr = _configuration.GetConnectionString(tenant);
optionsBuilder.UseSqlite(connectionStr);
}
這適用於大多數情況,除非用戶可以在同一會話期間切換租戶。
切換租戶
在針對多個資料庫的先前設定中,選項會被快取在Scoped
層級。 這表示如果使用者變更租使用者,則不會重新評估選項,因此租用戶變更不會反映在查詢中。
當租戶可以變更時,這個問題的簡單解決方案是將存留期設置為 Transient.
。這可確保每次DbContext
請求時,租戶與連接字串都會被重新評估。 用戶可以隨時切換租戶。 下表可協助您選擇最適合您工廠的使用壽命。
案例 | 單一資料庫 | 多個資料庫 |
---|---|---|
使用者處於單一租戶環境中 | Scoped |
Scoped |
用戶可以切換租戶 | Scoped |
Transient |
如果您的資料庫不採用用戶範圍相依性,預設值 Singleton
仍然有意義。
效能注意事項
EF Core 的設計目的是讓 DbContext
實例能以盡可能少的額外負荷快速具現化。 因此,為每個作業建立新 DbContext
通常應該沒問題。 如果此方法會影響應用程式的效能,請考慮使用 DbContext 共用。
結論
這是在 EF Core 應用程式中實現多租戶功能的實作指引。 如果您有進一步的範例或案例,或想要提供意見反應,請 開啟問題 並參考這份檔。