多租戶解析與Demo

中阳里士發表於2024-12-10

在做Saas應用時,多租戶解析往往是很重要的組成部分,也是使用者訪問網站最先處理的邏輯。

文前介紹:

多租戶的資料庫實現方式主要有三種:

  1. 單一資料庫實現,每條資料標識租戶Id進行識別資料屬於哪個租戶
  2. 一租戶一個資料庫,能夠做到完全的資料隔離
  3. 混合模式,部分資料在一張表上,主要是一些基礎資料;其他業務資料分庫儲存。

無論是哪種方式都要知道租戶是誰才能查詢資料庫。

獲取租戶的方式也可以有多種:

  1. 根據域名或者子域名不同可以獲知租戶,上面的所有場景都適合使用;(必須要有域名和dns服務)
  2. 根據使用者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版本相同

這些包的具體說明:

  1. Microsoft.EntityFrameworkCore

    • 功能: Entity Framework Core 是一個現代的物件-關係對映器(ORM),用於 .NET。它支援 LINQ 查詢、變更跟蹤、更新和模式遷移。EF Core 可以與多種資料庫一起工作,包括 SQL Server、Azure SQL Database、SQLite 和 Azure Cosmos DB。
    • 作用: 提供了核心的 ORM 功能,用於在 .NET 應用程式中與資料庫進行互動。
  2. Microsoft.EntityFrameworkCore.Design

    • 功能: 提供了 Entity Framework Core 工具的共享設計時元件。這些元件包括用於建立和管理遷移的工具。
    • 作用: 用於在設計時(如執行遷移命令時)提供必要的工具支援,幫助開發者更高效地管理和生成資料庫遷移。
  3. Microsoft.EntityFrameworkCore.Proxies

    • 功能: 為 Entity Framework Core 提供延遲載入代理。這些代理用於在需要時載入相關物件,從而減少記憶體使用和提高效能。
    • 作用: 透過代理機制實現延遲載入,提高應用程式的效能和響應速度。
  4. Microsoft.EntityFrameworkCore.SqlServer

    • 功能: 提供了針對 Microsoft SQL Server 的資料庫提供程式。這使得 Entity Framework Core 能夠與 SQL Server 資料庫進行互動。
    • 作用: 為 SQL Server 資料庫提供特定的資料庫提供程式,確保 EF Core 能夠正確地與 SQL Server 互動。
  5. 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的租戶資料就能獲取了。

相關文章