在做Saas應用時,多租戶解析往往是很重要的組成部分,也是使用者訪問網站最先處理的邏輯。
文前介紹:
多租戶的資料庫實現方式主要有三種:
- 單一資料庫實現,每條資料標識租戶Id進行識別資料屬於哪個租戶
- 一租戶一個資料庫,能夠做到完全的資料隔離
- 混合模式,部分資料在一張表上,主要是一些基礎資料;其他業務資料分庫儲存。
無論是哪種方式都要知道租戶是誰才能查詢資料庫。
獲取租戶的方式也可以有多種:
- 根據域名或者子域名不同可以獲知租戶,上面的所有場景都適合使用;(必須要有域名和dns服務)
- 根據使用者id獲取租戶資訊,從而存入前端的cookie中或者header中,只適用於上面的1、3方式,因為需要不同租戶的使用者都存入一種表中,方便查詢對應的租戶資訊;
下文中的例子只是簡單的一個例子,沒有進一步的業務場景,實現功能如下:
使用者根據不同的域名進入系統;
後臺拿刀http傳入的域名並解析;
查詢租戶資料庫,查詢出租戶名稱返回顯示。
新建解決方案,並其下新增三個專案:
try_MultiTenantApi:webapi專案,專案的啟動專案,為了方方便,租戶的Service和IService放入了這個專案中,實際應用時要放入業務層;
MultiTenantApi.Models:類庫專案,存放租戶物件;
MultiTenantApi.Data:資料層,使用efcore,存放上下文DbContext和遷移檔案
MultiTenantApi.Models中新增實體Tenant
public class Tenant { public int Id { get; set; } public string Identifier { get; set; } // 租戶識別符號,例如域名 public string Name { get; set; } public string ConnectionString { get; set; } // 每個租戶的獨立資料庫連線字串 }
MultiTenantApi.Data:
引入nuget包:要注意跟你專案的.net版本相同
這些包的具體說明:
-
Microsoft.EntityFrameworkCore
- 功能: Entity Framework Core 是一個現代的物件-關係對映器(ORM),用於 .NET。它支援 LINQ 查詢、變更跟蹤、更新和模式遷移。EF Core 可以與多種資料庫一起工作,包括 SQL Server、Azure SQL Database、SQLite 和 Azure Cosmos DB。
- 作用: 提供了核心的 ORM 功能,用於在 .NET 應用程式中與資料庫進行互動。
-
Microsoft.EntityFrameworkCore.Design
- 功能: 提供了 Entity Framework Core 工具的共享設計時元件。這些元件包括用於建立和管理遷移的工具。
- 作用: 用於在設計時(如執行遷移命令時)提供必要的工具支援,幫助開發者更高效地管理和生成資料庫遷移。
-
Microsoft.EntityFrameworkCore.Proxies
- 功能: 為 Entity Framework Core 提供延遲載入代理。這些代理用於在需要時載入相關物件,從而減少記憶體使用和提高效能。
- 作用: 透過代理機制實現延遲載入,提高應用程式的效能和響應速度。
-
Microsoft.EntityFrameworkCore.SqlServer
- 功能: 提供了針對 Microsoft SQL Server 的資料庫提供程式。這使得 Entity Framework Core 能夠與 SQL Server 資料庫進行互動。
- 作用: 為 SQL Server 資料庫提供特定的資料庫提供程式,確保 EF Core 能夠正確地與 SQL Server 互動。
-
Microsoft.EntityFrameworkCore.Tools
- 功能: 提供了 Entity Framework Core 工具,用於 NuGet Package Manager Console 中的 Visual Studio。
- 作用: 透過 Visual Studio 的 NuGet Package Manager Console 提供 EF Core 工具,幫助開發者更方便地管理和使用 EF Core 的設計時工具。
新增資料上下文:
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Tenant> Tenants { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Tenant>().HasData( new Tenant { Id = 1, Identifier = "tenant1", Name = "Tenant 1", ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=TenantDb1;Trusted_Connection=True;" }, new Tenant { Id = 2, Identifier = "tenant2", Name = "Tenant 2", ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=TenantDb2;Trusted_Connection=True;" } ); } }
注意實際應用中上面的資料庫連線字串根據你的具體情況變動一下,因為本例子沒有用到這個欄位所以想要嘗試可以不用改。
try_MultiTenantApi:
引入nuget包:
新建類TenantService:
public interface ITenantService { Task<Tenant> GetTenantByIdentifierAsync(string identifier); } public class TenantService : ITenantService { private readonly ApplicationDbContext _context; public TenantService(ApplicationDbContext context) { _context = context; } public async Task<Tenant> GetTenantByIdentifierAsync(string identifier) { return await _context.Tenants.FirstOrDefaultAsync(t => t.Identifier == identifier); } }
新建一箇中介軟體TenantMiddleware:
用於每次的請求解析出租戶資訊
namespace try_MultiTenantApi { public class TenantMiddleware { private readonly RequestDelegate _next; public TenantMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ITenantService tenantService) { var subdomain = GetSubdomain(context.Request.Host.Value); if (!string.IsNullOrEmpty(subdomain)) { var tenant = await tenantService.GetTenantByIdentifierAsync(subdomain); if (tenant != null) { context.Items["Tenant"] = tenant; } } await _next(context); } private string GetSubdomain(string host) { var parts = host.Split('.'); return parts.Length > 2 ? parts[0] : null; } } } public static class TenantMiddlewareExtensions { public static IApplicationBuilder UseTenantMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<TenantMiddleware>(); } }
配置appsetting檔案,配置租戶資料庫連線字串:
"ConnectionStrings": { "DefaultConnection": "Data Source=localhost;Initial Catalog=try_MultiTenant;User ID=sa;Password=******;Encrypt=False;" },
修改Program:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped<ITenantService, TenantService>(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseTenantMiddleware(); // 使用租戶解析中介軟體 app.MapControllers(); app.Run();
新建一個controller:ValuesController
[ApiController] [Route("[controller]")] public class ValuesController : ControllerBase { private readonly ITenantService _tenantService; public ValuesController(ITenantService tenantService) { _tenantService = tenantService; } [HttpGet(nameof(Get1))] public async Task<IActionResult> Get1() { var tenant = HttpContext.Items["Tenant"] as Tenant; if (tenant == null) { return NotFound("Tenant not found"); } return Ok(new { Message = $"Hello from {tenant.Name}" }); } }
OK,至此專案就編碼完成,接下來就是進行資料遷移。因為前面上下文中已經設定了出事測試資料
add-migration init
update-database
到此,租戶的資料庫已經建立,並且也有了初始的測試資料:
為了測試方便咱們在launch啟動檔案中配置兩個租戶的訪問地址:
"profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "http://localhost:5252;http://tenant1.Paas.JBWL.com:5252", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, }
接下來設定try_MultiTenantApi專案為啟動專案,啟來後出swagger頁面:
頁面地址是預設第一個http://localhost:5252/
下面我們就可以修改一下本地的host檔案對映兩個域名,可以使用swichhost,也可以找到本地的host檔案,增加地址對映
127.0.0.1 tenant1.Paas.JBWL.com 127.0.0.1 tenant2.Paas.JBWL.com
啟用後訪問
tenant1.Paas.JBWL.com:5252
出現前面一模一樣的頁面,訪問一下測試介面看是否解析成功:
成功!
接下來試一下tenant2.paas.jbwl.com:5252
至此,這個測試demo也就完成了,只要解析出租戶資訊,接下來Saas的租戶資料就能獲取了。