HotChocolate GraphQL多租户场景下如何传递租户ID生成动态DbContext连接串
解决方案
1. 存储租户ID到请求生命周期上下文
首先定义Scoped作用域的租户访问器,用于在单次请求范围内传递租户ID:
public class TenantAccessor { public string? TenantId { get; set; } }
在服务注册时将其标记为Scoped:builder.Services.AddScoped<TenantAccessor>();
然后实现HotChocolate的HTTP请求拦截器,从请求头/认证Claims/路由参数中提取租户ID,存入访问器:
public class TenantHttpRequestInterceptor : IHttpRequestInterceptor { public async ValueTask OnRequestAsync(IHttpContextAccessor httpContextAccessor, IRequestExecutor requestExecutor, IQueryRequestBuilder requestBuilder, CancellationToken cancellationToken) { var httpContext = httpContextAccessor.HttpContext; // 示例从请求头X-Tenant-Id提取,可根据实际场景替换为从认证信息/路由提取 if (httpContext?.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantId) == true) { var tenantAccessor = httpContext.RequestServices.GetRequiredService<TenantAccessor>(); tenantAccessor.TenantId = tenantId.FirstOrDefault(); } // 可在此处增加租户ID非空校验,直接返回错误避免后续逻辑执行 } }
将拦截器注册到GraphQL服务:
builder.Services.AddGraphQLServer() .AddHttpRequestInterceptor<TenantHttpRequestInterceptor>();
2. 动态构造租户对应的DbContext
不要在启动时注册固定连接字符串的DbContext,改用Scoped工厂模式动态构造:
// 注册TenantStructure库的DbContext builder.Services.AddScoped<TenantStructureDbContext>(sp => { var tenantAccessor = sp.GetRequiredService<TenantAccessor>(); if (string.IsNullOrWhiteSpace(tenantAccessor.TenantId)) { throw new InvalidOperationException("缺少有效租户ID"); } // 从配置读取带占位符的连接字符串模板 var connTemplate = sp.GetRequiredService<IConfiguration>().GetConnectionString("TenantStructureTemplate"); var finalConnStr = string.Format(connTemplate, tenantAccessor.TenantId); return new TenantStructureDbContext(new DbContextOptionsBuilder<TenantStructureDbContext>() .UseSqlServer(finalConnStr) .Options); }); // TenantData库的DbContext按照同样逻辑注册即可
该方式会在单次请求内首次用到DbContext时完成构造,同一个请求内多次查询复用同一个DbContext实例,无额外性能开销。
3. Resolver中直接使用DbContext
不需要在GraphQL查询参数中声明租户ID,直接注入对应DbContext即可使用:
[QueryType] public static class BusinessQueries { public static async Task<List<MyBusinessData>> GetBusinessListAsync( [Service] TenantDataDbContext dataDb, CancellationToken ct) { return await dataDb.MyBusinessData.ToListAsync(ct); } }
注意事项
- 租户ID提取后建议增加格式校验,避免拼接连接字符串时出现注入风险
- 若有跨租户操作需求,可以单独注册Transient的DbContext工厂,手动传入租户ID构造对应实例
- 可以配合HotChocolate的全局中间件做租户权限校验,避免非法租户ID访问
内容的提问来源于stack exchange,提问作者Holger
相关产品推荐
相关产品推荐

