多租户
builder.Services.AddLedonMultiTenancy();
app.UseLedonMultiTenancy();
租户解析
四种解析来源(o.Source):
| 来源 | 配置 | 示例 |
|---|---|---|
Header(默认) |
o.HeaderName |
X-Tenant-Id: tenant-a |
QueryString |
o.QueryStringName |
?tenantId=tenant-a |
Claim |
o.ClaimType(默认 tenant_id) |
令牌里的租户声明 |
Host |
o.HostSuffix |
tenant-a.example.com → tenant-a |
// 子域名多租户
builder.Services.AddLedonMultiTenancy(o =>
{
o.Source = TenantSource.Host;
o.HostSuffix = ".example.com";
});
Claim来源要求认证先于租户解析:app.UseAuthentication(); app.UseLedonMultiTenancy();Host来源下 apex 域名(example.com)与无关域名按单租户处理- 任何来源未解析到租户标识时按单租户处理(
TenantId == null,IsMultiTenant == false),记 Debug 日志 - 解析结果经
ITenantContext暴露给请求管线内的任意服务:
app.MapGet("/whoami", (ITenantContext tenant) => new { tenant.TenantId, tenant.IsMultiTenant });
后台任务的环境租户穿透
Web 请求外的后台工作(队列任务、定时任务)默认拿不到请求上下文的租户。用 AsyncLocal 环境租户穿透:
// 入队时捕获当前环境租户
await queue.EnqueueAsync(async (scope, token) =>
{
var tenant = scope.ServiceProvider.GetRequiredService<ITenantContext>(); // 仍能看到 tenant-a
await ProcessAsync(tenant.TenantId, token);
});
数据隔离:约定显式
框架提供上下文与解析,不提供隐式查询过滤。每个查询显式带上租户条件:
db.Orders.Where(o => o.TenantId == tenant.TenantId);
理由:隐式全局过滤会让"跨租户统计"这类需求变得困难,且容易在 join 场景产生意外遗漏。约定显式,隔离责任在数据层清晰可见。
需要全局过滤时:ApplyTenantFilters
实体实现 IMultiTenant(TenantId 属性),并在 OnModelCreating 里显式启用过滤(与软删除过滤同一风格):
public class Order : IMultiTenant
{
public int Id { get; set; }
public string TenantId { get; set; } = string.Empty;
}
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
// 每请求/每作用域设置当前租户(Web 中间件解析后写入;后台任务 SetAmbient 穿透后写入)
public string? CurrentTenantId { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.ApplyTenantFilters(this, context => context.CurrentTenantId);
}
- 过滤表达式引用上下文实例成员(EF 文档的查询过滤模式),EF 每次查询按当前上下文实例重新求值;
- 不要传闭包捕获 DbContext 字段的
Func——EF 按上下文类型缓存模型与编译后的查询计划,Func 结果会被首个查询固化; - 无租户上下文时默认不过滤(宿主/后台穿透视角可见全部行,单租户应用不受影响);
filterWhenNoTenant: true则改为过滤为空(严格隔离)。
边界
路由值解析(/tenants/{tenantId}/...)未内置,可在 TenantResolutionMiddleware 基础上自行扩展。后台任务的环境租户靠入队时捕获,不会随每次队列派发重新解析——跨租户复用的 worker 进程需显式 SetAmbient。