如何在Entity Framework Core中配置基于域名/UniqueID的动态连接字符串
实现方案
核心逻辑为按请求范围解析租户信息,在业务DbContext实例化前完成对应租户连接字符串的读取,再为该请求的所有数据操作分配对应连接的DbContext实例。
步骤1:定义租户实体与租户解析服务
首先单独定义主库(存储租户配置的默认DB)的DbContext,专门用于租户配置查询,避免和业务DbContext产生循环依赖:
// 主库DbContext,仅用于查询租户配置 public class MasterDbContext : DbContext { public MasterDbContext(DbContextOptions<MasterDbContext> options) : base(options) {} // 租户配置表,存储UniqueID、域名、连接字符串等信息 public DbSet<TenantInfo> TenantInfos { get; set; } } // 租户信息实体 public class TenantInfo { public Guid Id { get; set; } public string UniqueId { get; set; } public string DomainSuffix { get; set; } public string ConnectionString { get; set; } }
定义租户解析服务接口与实现,按请求范围注入,每次请求触发时解析当前请求对应的租户:
public interface ITenantResolver { TenantInfo CurrentTenant { get; } } public class TenantResolver : ITenantResolver { private readonly IHttpContextAccessor _httpContextAccessor; private readonly MasterDbContext _masterDbContext; private readonly IMemoryCache _cache; private TenantInfo _currentTenant; public TenantResolver(IHttpContextAccessor httpContextAccessor, MasterDbContext masterDbContext, IMemoryCache cache) { _httpContextAccessor = httpContextAccessor; _masterDbContext = masterDbContext; _cache = cache; } public TenantInfo CurrentTenant { get { if (_currentTenant != null) return _currentTenant; var context = _httpContextAccessor.HttpContext; if (context == null) return null; // 设计时/无请求场景返回空 // 优先读取iframe携带的UniqueID参数 if (context.Request.Query.TryGetValue("uniqueId", out var uniqueId)) { _currentTenant = _cache.GetOrCreate($"tenant_{uniqueId}", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return _masterDbContext.TenantInfos.FirstOrDefault(t => t.UniqueId == uniqueId); }); return _currentTenant; } // 没有UniqueID时按域名后缀匹配 var host = context.Request.Host.Host; var suffix = host.Split('.').Last(); _currentTenant = _cache.GetOrCreate($"tenant_domain_{suffix}", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return _masterDbContext.TenantInfos.FirstOrDefault(t => t.DomainSuffix == suffix); }); return _currentTenant; } } }
步骤2:修改业务DbContext配置
修改你用于业务操作的DbContext,在构造函数注入租户解析服务,动态配置连接字符串,同时兼容设计时(迁移、工具调用)场景:
public class BusinessDbContext : DbContext { private readonly ITenantResolver _tenantResolver; // 设计时创建实例会调用这个构造函数 public BusinessDbContext(DbContextOptions<BusinessDbContext> options) : base(options) {} // 运行时请求进来会调用这个构造函数注入租户解析服务 public BusinessDbContext(DbContextOptions<BusinessDbContext> options, ITenantResolver tenantResolver) : base(options) { _tenantResolver = tenantResolver; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 已经配置过(设计时场景)就不需要重复配置 if (optionsBuilder.IsConfigured) return; // 运行时拿到租户连接字符串再配置 var connStr = _tenantResolver?.CurrentTenant?.ConnectionString; if (string.IsNullOrEmpty(connStr)) { // 无有效租户时抛出异常或者使用默认测试连接,按需调整 throw new UnauthorizedAccessException("无效租户访问"); } // 替换为你使用的数据库Provider,比如UseMySQL、UsePostgreSQL等 optionsBuilder.UseSqlServer(connStr); } // 你的业务DbSet定义... }
步骤3:Startup(.NET 5)服务注册与管道配置
public class Startup { public void ConfigureServices(IServiceCollection services) { // 注册HttpContext访问器,用于获取当前请求信息 services.AddHttpContextAccessor(); // 注册内存缓存,用于缓存租户配置 services.AddMemoryCache(); // 注册主库DbContext,连接字符串写在appsettings.json里即可 services.AddDbContext<MasterDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MasterDb"))); // 注册租户解析服务为Scoped,每个请求一个实例 services.AddScoped<ITenantResolver, TenantResolver>(); // 注册业务DbContext为Scoped,不要写死连接字符串,留到OnConfiguring动态配置 services.AddDbContext<BusinessDbContext>(); // 其他服务注册... services.AddControllersWithViews(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 其他中间件... app.UseRouting(); app.UseAuthorization(); // 可选:添加租户校验中间件,在请求进入业务逻辑前判断租户是否有效 app.Use(async (context, next) => { var tenantResolver = context.RequestServices.GetRequiredService<ITenantResolver>(); if (tenantResolver.CurrentTenant == null) { context.Response.StatusCode = 400; await context.Response.WriteAsync("无效租户访问"); return; } await next(); }); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } }
异常问题修复
你之前遇到的实例初始化前调用异常,是因为程序启动时(无请求上下文)框架会尝试初始化DbContext实例,此时还没有租户信息,通过两个方式解决:
- 给BusinessDbContext加无租户解析服务的构造函数,供设计时工具调用
- 在OnConfiguring中加
optionsBuilder.IsConfigured判断,设计时使用默认配置的连接字符串即可,运行时才会走动态配置逻辑
iframe场景适配
你给租户提供的iframe地址按https://你的服务域名/对应页面路径?uniqueId=租户唯一ID格式拼接即可,租户解析服务会优先读取uniqueId参数匹配对应租户,无需依赖域名后缀判断。
内容的提问来源于stack exchange,提问作者Deep Soni
相关产品推荐
相关产品推荐

