具有 ASP.NET Core 3.0 和 EF Core 的多租户应用程序

Posted

技术标签:

【中文标题】具有 ASP.NET Core 3.0 和 EF Core 的多租户应用程序【英文标题】:Multi tenant application with ASP.NET Core 3.0 and EF Core 【发布时间】:2020-05-16 16:14:20 【问题描述】:

我正在开发一个基于多租户的应用程序,该应用程序将为每个学校/租户提供一个单独的数据库。每个模式将彼此相同。这个想法是使用 .NET Core 3 和 EF Core 拥有一个数据库上下文。

例如,客户端导航到 school1.gov.uk,然后使用存储在“school1”下的 appsettings.json 中的连接字符串来实例化 SchoolContext。

目前,我必须针对添加的每个新上下文运行 add-migration。任何人都可以想出一个解决方案,在单一上下文(即 SchoolContext )下运行一次迁移吗?

主要上下文

public class SchoolContext : DbContext
     protected readonly IConfiguration _configuration;
      private readonly IHttpContextAccessor _httpContextAccessor;

    public DbSet<Student> Students  get; set; 
    public DbSet<Course> Courses  get; set; 

    public SchoolContext(IConfiguration configuration, IHttpContextAccessor httpContextAccessor) 
    
        _configuration = configuration;
        _httpContextAccessor = httpContextAccessor;
    

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    
            var subdomain = _httpContextAccessor.HttpContext.Request.Host.Host;
            var connectionString = _configuration.GetConnectionString(subdomain);
            optionsBuilder.UseSqlServer(connectionString);
    

 

租户上下文 1

public class School1Context : SchoolContext

    public Schoo1Context()
    

    

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    
         var connectionString = _configuration.GetConnectionString("School1");
         optionsBuilder.UseSqlServer(connectionString);
    

 

租户上下文 2

public class School2Context : SchoolContext

    public School2Context()
    

    

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    
         var connectionString = _configuration.GetConnectionString("School2");
         optionsBuilder.UseSqlServer(connectionString);
    

 

程序.cs

         using (var scope = host.Services.CreateScope())
         
             var school1Context = scope.ServiceProvider.GetService<School1Context>();
             school1Context.Database.Migrate();
             var school2Context = scope.ServiceProvider.GetService<School2Context>();
             school2Context.Database.Migrate();
         

Appsettings.json

"ConnectionStrings": 
    "School1": "Persist Security Info=true;Data Source=.\\SQLEXPRESS;Initial Catalog=School1;User ID=;Password=;",
    "School2": "Persist Security Info=true;Data Source=.\\SQLEXPRESS;Initial Catalog=School2;User ID=;Password=;",

   

【问题讨论】:

codewithmukesh.com/blog/multitenancy-in-aspnet-core 【参考方案1】:

我遇到了与您几乎相同的问题。我有一个 TenantDbContext。

public class TenantDbContext : DbContext

    private readonly Tenant _tenant;

    public DbSet<Branch> Branches  get; set; 

    public TenantDbContext(DbContextOptions<TenantDbContext> options, IHttpContextAccessor httpContextAccessor)
        : base(options)
    
        if (httpContextAccessor.HttpContext != null)
        
            _tenant = (Tenant)httpContextAccessor.HttpContext.Items["TENANT"];
        
    

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    
        if (_tenant != null)
        
            optionsBuilder.UseSqlServer(_tenant.ConnectionString);
        

        base.OnConfiguring(optionsBuilder);
    

然后我发现使用包管理器控制台很难“添加迁移”,因为连接字符串是动态的,所以我在 Startup.ConfigureServices() 中为连接字符串创建了一个占位符(“Dev”)

var devConnectionString = _config.GetConnectionString("Dev");
services.AddDbContext<TenantDbContext>(opt => opt.UseSqlServer(devConnectionString));

要知道,配置一个DbContext有两种方式,分别如上面两个代码sn-ps所示。如果两者都提供,则首先调用 AddDbContext(),然后再调用 OnConfiguring()。

当您运行“Add-Migration”命令时,将在带有占位符连接字符串的 DbContext 中生成迁移,尽管没有指定租户。当您运行应用程序时,您只需添加一个中间件并调用 Migrate() 将这些迁移应用到您的租户数据库,然后再转到 MVC 中间件。

public class TenantIdentifier

    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantIdentifier(RequestDelegate next, IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
    
        _next = next;
        _configuration = configuration;
        _httpContextAccessor = httpContextAccessor;
    

    public async Task Invoke(HttpContext httpContext, HostDbContext hostDbContext)
    
        // Get tenant id from token
        var tenantId = httpContext.User.FindFirst(Constants.TENANT)?.Value;

        // Set tenant id to httpContext.Items
        if (!string.IsNullOrWhiteSpace(tenantId))
        
            var tenant = hostDbContext.Tenants.SingleOrDefault(t => t.Id.ToString() == tenantId);
            httpContext.Items["TENANT"] = tenant;

            using(var tenantDbContext = new TenantDbContext(new DbContextOptions<TenantDbContext>(), _httpContextAccessor))
            
                tenantDbContext.Database.Migrate();
            
        

        await _next.Invoke(httpContext);
    

然后,您只需要运行一次“Add-Migration”,根本不需要创建占位符数据库。

【讨论】:

【参考方案2】:

我在您的代码中看到的最大问题是您应该有 1 个上下文(因为它们都是相同的架构,对吗?)。如果它们有不同的架构,您将需要不同的上下文。

应该使用学校的特定连接字符串来实例化该单一上下文。

这样,您可以针对 1 个上下文(不是多个)添加迁移。

你大部分时间都在那里,只需删除 School1Context 和 School2Context,因为这里:

SchoolContext.cs

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

   var subdomain = _httpContextAccessor.HttpContext.Request.Host.Host;
   var connectionString = _configuration.GetConnectionString(subdomain); // <--- this is tenanted already
   optionsBuilder.UseSqlServer(connectionString);

您已经获取连接字符串并连接到不同的数据库。

您在原始问题中陈述了答案:

这个想法是使用 .NET Core 3 和 EF 拥有一个数据库上下文 核心。

轻微插入我喜欢和每天使用的框架:ServiceStack 内置多租户 - ServiceStack Multitenancy

【讨论】:

根据我从 OP 那里得到的信息,他需要他的应用程序来引用所有 学校 中的所有数据库 这不是我的理解。此语句:“例如,客户端导航到 school1.gov.uk,然后使用存储在 appsettings.json 中‘school1’下的连接字符串来实例化 SchoolContext。”这告诉我,当人们访问 school1.gov.uk 时,他只需要访问 School 1 的数据库。也许我错过了什么? 拥有多个部署配置文件和配置转换,同时为所有主机保持应用完全相同(使用相同的 connectionString 键)不是更好吗?

以上是关于具有 ASP.NET Core 3.0 和 EF Core 的多租户应用程序的主要内容,如果未能解决你的问题,请参考以下文章

ef在asp.net中删除报值不能为空的原因

从 ASP.NET Core 2.2 升级到 3.0

.NET Core 1.0ASP.NET Core 1.0和EF Core 1.0简介

出站请求在一段时间后挂在ASP.NET Core 3.0应用程序中

.NET Core 3.0及ASP.NET Core 3.0 前瞻

ASP.NET Core 3.0 System.Text.Json骆驼案例序列化