ASP.NET Core 3.0 和 EF Core 的多租户应用程序
Multi tenant application with ASP.NET Core 3.0 and EF Core
我正在开发一个基于多租户的应用程序,每个 school/tenant 都有一个单独的数据库。每个模式将彼此相同。这个想法是使用 .NET Core 3 和 EF Core 拥有一个数据库上下文。
例如,客户端导航到school1.gov.uk,然后使用'school1'下appsettings.json中存储的连接字符串实例化SchoolContext。
目前,我必须 运行 针对添加的每个新上下文添加迁移。任何人都可以在单一上下文(即 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);
}
}
Program.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=;",
}
我在您的代码中看到的最大问题是您应该有 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);
}
您已经获取连接字符串并连接到不同的数据库。
您在原问题中陈述了答案:
The idea is to have one database context using .NET Core 3 and EF
Core.
稍微插入我喜欢并每天使用的框架:ServiceStack 内置多租户 - ServiceStack Multitenancy
我遇到了和你几乎一样的问题。我有一个 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);
}
}
然后我发现很难"Add-Migration"使用包管理器控制台,因为连接字符串是动态的,所以我为连接字符串创建了一个占位符("Dev"),在Startup.ConfigureServices()
var devConnectionString = _config.GetConnectionString("Dev");
services.AddDbContext<TenantDbContext>(opt => opt.UseSqlServer(devConnectionString));
众所周知,配置DbContext有两种方式,分别如上两段代码所示。如果两者都提供,将首先调用 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" 一次,根本不需要创建占位符 DB。
我正在开发一个基于多租户的应用程序,每个 school/tenant 都有一个单独的数据库。每个模式将彼此相同。这个想法是使用 .NET Core 3 和 EF Core 拥有一个数据库上下文。
例如,客户端导航到school1.gov.uk,然后使用'school1'下appsettings.json中存储的连接字符串实例化SchoolContext。
目前,我必须 运行 针对添加的每个新上下文添加迁移。任何人都可以在单一上下文(即 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);
}
}
Program.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=;",
}
我在您的代码中看到的最大问题是您应该有 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);
}
您已经获取连接字符串并连接到不同的数据库。
您在原问题中陈述了答案:
The idea is to have one database context using .NET Core 3 and EF Core.
稍微插入我喜欢并每天使用的框架:ServiceStack 内置多租户 - ServiceStack Multitenancy
我遇到了和你几乎一样的问题。我有一个 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);
}
}
然后我发现很难"Add-Migration"使用包管理器控制台,因为连接字符串是动态的,所以我为连接字符串创建了一个占位符("Dev"),在Startup.ConfigureServices()
var devConnectionString = _config.GetConnectionString("Dev");
services.AddDbContext<TenantDbContext>(opt => opt.UseSqlServer(devConnectionString));
众所周知,配置DbContext有两种方式,分别如上两段代码所示。如果两者都提供,将首先调用 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" 一次,根本不需要创建占位符 DB。