C# .net Core 2、EF和多租户-基于用户的Dbcontext切换
我有(几乎)最糟糕的多租户。我正在构建一个asp.net核心网站,我正在将一堆小小的内部网站移植到这个网站上。每个子网站将是一个asp.net区域。我有一个C# .net Core 2、EF和多租户-基于用户的Dbcontext切换,c#,entity-framework,asp.net-core,dbcontext,C#,Entity Framework,Asp.net Core,Dbcontext,我有(几乎)最糟糕的多租户。我正在构建一个asp.net核心网站,我正在将一堆小小的内部网站移植到这个网站上。每个子网站将是一个asp.net区域。我有一个IdentityContext用于标识内容。我有多个供应商数据库副本,每个副本都有多个租户。ApplicationUser类有一个OrgCode属性,我想用它来切换db上下文 我可以看到自己需要将User.OrgCode和Area映射到连接字符串的东西 有许多关于堆栈溢出的部分示例。经过一个下午的阅读,我感到很困惑。其核心是: 从构造函数参
IdentityContext
用于标识内容。我有多个供应商数据库副本,每个副本都有多个租户。ApplicationUser
类有一个OrgCode属性,我想用它来切换db上下文
我可以看到自己需要将User.OrgCode和Area映射到连接字符串的东西
有许多关于堆栈溢出的部分示例。经过一个下午的阅读,我感到很困惑。其核心是:
- 从构造函数参数中删除DI dbcontext ref
- 在控制器构造函数中实例化dbcontext
- 像前面一样使用dbcontext
编辑2020/07/09 不幸的是,这已变得更加紧迫 身份数据库是租户不可知的。标识中的每个用户都有一个组织代码标识符。(自定义用户属性) 每台服务器都通过使用“成本中心”内置了多租户。服务器在每台服务器上都有一个名为相同的数据库集合
- DbContext
- CustLogsContext
public virtual ServerEnum Server { get { return ServerEnum.None; } } DbSet (etc)
- CustLogsServerAContext
public override ServerEnum Server { get { return ServerEnum.ServerA; } }
- CustLogsServerBContext(etc)
- custlogsservercontext(etc)
- CustLogsServerDContext(etc)
- CustLogsServerAContext
- 卖方上下文
- 供应商服务器文本
- 供应商服务器上下文(etc)
- 供应商服务器上下文(etc)
- 供应商服务器上下文(etc)
- CustLogsContext
目前认为我需要一个收集上下文的类,该类将拥有一个
字典
,并注册为服务。非常确定,我需要为每个继承的dbcontext创建一个对象版本,除非有人知道我可以使用的多态技巧,否则我已经创建了一个多租户实现,如下所示(在理论上可以无限扩展)。创建一个多租户数据库(比如tenantdb)。容易的。但诀窍是为每个租户(您的目标数据库)存储connectionstring详细信息。除了你的用户组织代码等
我可以看到自己需要将User.OrgCode和Area映射到连接字符串的东西
因此,在代码中映射它的方法是将dbcontext与目标租户连接字符串(从租户数据库中获取)相匹配。因此,您需要为您的租户数据库创建一个更好的dbcontext。因此,首先调用您的租户DB,通过使用您的用户组织代码进行筛选来获取正确的租户连接字符串。然后使用它创建一个新的目标dbcontext
我们的梦想是继续在每个页面中使用DI,并根据需要传递上下文。然后,上下文将足够智能,可以根据用户名的组织代码选择正确的底层连接详细信息
我和DI一起工作
我为这个租户数据库的crud操作创建了UI元素,所以我可以更新删除添加连接字符串详细信息和其他需要的数据。密码在保存时加密,在传递到目标dbcontext之前在get上解密
因此,我的配置文件中有两个连接字符串。一个用于租户数据库,一个用于默认目标数据库。它可以是空的/伪的,因为如果没有DI代码,您可能会遇到DI代码引发的应用程序启动错误,因为它很可能会自动搜索connectionstring
我也有开关代码。这是用户可以切换到其他租户的地方。因此,在这里,用户可以从其拥有权限的所有租户中进行选择(是权限存储在tenantdb中)。这将再次触发上述代码步骤
干杯
作为我的出发点
这样,您就可以拥有非常糟糕的耦合目标数据库。唯一的重叠可能是用户ID(甚至是来自Azure、Google、AWS等的一些令牌)
启动
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddDbContext<TenantContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("TenantContext")));
//your dummy (empty) target context.
services.AddDbContext<TargetContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("TargetContext")));
}
默认字符串
{“AllowedHosts”:“*”,
“连接字符串”:{
“租户上下文”:“服务器=(localdb)\mssqllocaldb;数据库=租户上下文;受信任的\u连接=True;MultipleActiveResultSets=True”,
“TargetContext”:“服务器=(localdb)\mssqllocaldb;数据库=TargetContext;受信任的\u连接=True;多个”
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.TenantContext _context;
private ContosoUniversity.Data.TargetContext _targetContext;
public IndexModel(ContosoUniversity.Data.TenantContext context, ContosoUniversity.Data.TargetContext targetContext)
{
_context = context;
//set as default targetcontext -> dummy/empty one.
_targetContext = targetContext;
}
public TenantContext Context => _context;
public TargetContext TargetContext { get => _targetContext; set => _targetContext = value; }
public async Task OnGetAsync()
{
//get data from default target.
var student1 = _targetContext.Students.First();
//or
//switch tenant
//lets say you login and have the users ID as guid.
//then return list of tenants for this user from tenantusers.
var ut = await _context.TenantUser.FindAsync("9245fe4a-d402-451c-b9ed-9c1a04247482");
//now get the tenant(s) for this user.
var SelectedTentant = await _context.Tenants.FindAsync(ut.TenantID);
DbContextOptionsBuilder<TargetContext> Builder = new DbContextOptionsBuilder<TargetContext>();
Builder.UseSqlServer(SelectedTentant.ConnectionString);
_targetContext = new TargetContext(Builder.Options);
//now get data from the switched to database.
var student2 = _targetContext.Students.First();
}
}
public class Tenant
{
public int TenantID { get; set; }
public string Name { get; set; }
//probably could slice up the connenctiing string into props.
public string ConnectionString { get; set; }
public ICollection<TenantUser> TenantUsers { get; set; }
}
public class TenantUser
{
[Key]
public Guid UserID { get; set; }
public string TenantID { get; set; }
}
// In Startup.ConfigureServices
services.AddScoped<ApplicationUser>((serviceProvider) =>
{
// something to return the active user however you're normally doing it.
});
services.AddTransient<CustLogsContext>((serviceProvider) =>
{
ApplicationUser currentUser = serviceProvider.GetRequiredService<ApplicationUser>();
// Use your OrgToServerMapping to create a data context
// with the correct connection
return CreateDataContextFromOrganization(currentUser.OrgCode);
});
// In Startup.ConfigureServices
services.AddScoped<ApplicationUser>((serviceProvider) =>
{
// something to return the active user however you're normally doing it.
});
services.AddTransient<CustLogsContextWrapper>();
// In its own file somewhere
public class CustLogsContextWrapper
{
private ApplicationUser currentUser;
public CustLogsContextWrapper(ApplicationUser currentUser)
{
this.currentUser = currentUser;
}
public CustLogsContext GetContext()
{
// use your OrgToServerMapping to create a data context with the correct connection;
return CreateDataContextFromOrganization(user.OrgCode);
}
}