共用方式為


多租戶架構

許多企業營運應用程式的設計目的是要與多個客戶合作。 請務必保護數據,讓客戶數據不會「外泄」或由其他客戶和潛在競爭對手看到。 這些應用程式會分類為「多租戶」,因為每個客戶都被視為應用程式的租戶,擁有自己的資料集。

警告

本文使用本機資料庫,其不需要使用者進行驗證。 實際執行應用程式應該使用可用的最安全驗證流程。 如需已部署測試與實際執行應用程式驗證的詳細資訊,請參閱安全驗證流程

重要

本文件提供現狀的範例和解決方案。這些不是「最佳做法」,而是供您參考的「運作方式」。

提示

您可以在 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 服務可以相依於另一 ScopedSingleton 服務或服務,但 Singleton 服務只能相依於其他 Singleton 服務: Transient => Scoped => Singleton

多個架構

警告

EF Core 並不直接支援此案例,而且不是建議的解決方案。

在不同的方法中,相同的資料庫可以使用資料表架構來處理 tenant1tenant2

  • Tenant1 - tenant1.CustomerData
  • Tenant2 - tenant2.CustomerData

如果您未使用 EF Core 來處理帶有遷移功能的資料庫更新,且已經有多模式的資料表,則可以像這樣覆寫DbContextOnModelCreating中的結構(資料表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 應用程式中實現多租戶功能的實作指引。 如果您有進一步的範例或案例,或想要提供意見反應,請 開啟問題 並參考這份檔。