You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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实例,此时还没有租户信息,通过两个方式解决:

  1. 给BusinessDbContext加无租户解析服务的构造函数,供设计时工具调用
  2. 在OnConfiguring中加optionsBuilder.IsConfigured判断,设计时使用默认配置的连接字符串即可,运行时才会走动态配置逻辑

iframe场景适配

你给租户提供的iframe地址按https://你的服务域名/对应页面路径?uniqueId=租户唯一ID格式拼接即可,租户解析服务会优先读取uniqueId参数匹配对应租户,无需依赖域名后缀判断。


内容的提问来源于stack exchange,提问作者Deep Soni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 17:15:03