.net Core 2、EF 和多租户 - 基于用户的 Dbcontext 切换

Hec*_*res 11 c# entity-framework dbcontext asp.net-core

我有(几乎)最糟糕的多租户。我正在构建一个 asp.net 核心网站,我正在将一堆 pokey 小 Intranet 网站移植到该网站。每个子站点将是一个 asp.net 区域。我有一个IdentityContext身份的东西。我有多个供应商数据库副本,每个副本都有多个租户。该ApplicationUser班有,我想用切换分贝范围内的OrgCode财产。

我可以看到自己需要将 User.OrgCode 和 Area 映射到连接字符串的东西

Stack Overflow 上有很多这方面的部分示例。一个下午的阅读后,我很困惑。它的核心接缝是:

  • 从构造函数参数中删除 DI dbcontext ref。
  • 在控制器构造函数中实例化 dbcontext。
  • 像以前一样使用 dbcontext。

我在正确的轨道上吗?

有什么连贯的例子吗?


编辑 2020/07/09

不幸的是,这变得更加紧迫。

身份数据库与租户无关。Identity 中的每个用户都有一个 OrgCode 标识符。(自定义用户属性)。

每个服务器都通过使用“成本中心”内置了多租户。服务器在每台服务器上都有一组名称相同的数据库。

  1. 核心供应商数据库
  2. 我们存储扩展的自定义数据库
  3. 用于我们的作业输出的日志数据库

还有一些小型应用程序特定数据库已经使用组织代码来识别用户

服务器 A - 1 组织代码

服务器 B - 4 个组织代码

服务器 C - 3 个组织代码参与项目,50+ 还没有(大部分很小)

服务器 D - 目前没有使用组织代码。服务器上 80+。(很快)

不可能将所有组织合并到一台服务器上。存在法律和技术后果。每个服务器都有数百个远程转发器向它们报告需要更新。这些提供的数据是我们的自定义作业使用的数据。

梦想是继续在每个页面中使用 DI,根据需要传入上下文。然后上下文将足够智能,可以根据用户名的 OrgCode 选择正确的底层连接详细信息。

我对使用代理这个词犹豫不决,因为它在这个空间中似乎负载很重。

地狱,如果我知道把它放在哪里,即使使用 switch 语句也很好

预期效果来自 Org XYZ 的用户加载需要供应商数据库的页面,他们从 XYZ 映射到的服务器获取该页面。

编辑 2020/07/13

为了整理引用,我已将 OrgCode 和服务器切换到 Enums。上下文继承如下

  • 数据库上下文
    • 客户日志上下文

         public virtual ServerEnum Server 
         { 
             get 
             { 
                 return ServerEnum.None; 
             }
         }
      
         DbSet (etc)
      
      Run Code Online (Sandbox Code Playgroud)
      • CustLogsServerAContext

             public override ServerEnum Server 
             { 
                 get 
                 { 
                     return ServerEnum.ServerA; 
                 }
             }
        
        Run Code Online (Sandbox Code Playgroud)
      • CustLogsServerBContext(等)

      • CustLogsServerCContext(等)

      • CustLogsServerDContext(等)

    • 供应商上下文

      • 供应商服务器上下文
      • VendorServerBContext(等)
      • VendorServerCContext(等)
      • VendorServerDContext(等)

我还创建了一个静态类 OrgToServerMapping,其中包含一个将 OrgCodes 映射到服务器的字典。当前硬编码,最终将更改为从配置加载,并添加重新加载方法。

目前认为我需要一个收集上下文的类 将有一个Dictionary<serverEnum, dbcontext>并注册为服务。很确定我需要每个继承的 dbcontext 的对象版本,除非有人知道我可以使用的多态技巧

Jar*_*lps 7

我在一个具有数千个数据库的类似系统上工作,但使用 LinqToSql 而不是 EF(我知道......)。希望一般的想法都能翻译。有连接池碎片的问题,你有,如果你结了许多数据库抗衡,但只是你的四个数据库,你将不必关于担心。

我喜欢这两种方法 - 它们都假设您可以设置ApplicationUser通过 DI 注入的电流。

方法#1:在 Startup 中,配置返回数据上下文的 DI 以获取当前用户,然后使用该用户构建正确的数据上下文。像这样的东西:

// 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);
});
Run Code Online (Sandbox Code Playgroud)

方法#2:不是直接注入 CustLogsContext,而是注入依赖于负责构建数据上下文的活动用户的服务:

// 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我个人更喜欢后一种方法,因为它避免了在 Startup 中调用服务定位器,而且我喜欢封装有关如何创建数据上下文的细节。但是如果我已经有一堆直接用 DI 获取数据上下文的代码,第一个就可以了。


Igo*_*gor 2

我创建了一个多租户实现,如下所示(理论上可以无限扩展)。创建一个多租户数据库(例如tenantdb)。简单的。但诀窍是存储每个租户(您的目标数据库)的连接字符串详细信息。与您的用户组织代码等一起。

我可以看到自己需要将 User.OrgCode 和 Area 映射到连接字符串的东西

因此,将其映射到代码中的方法是将您从租户数据库获取的目标租户连接字符串提供给您的 dbcontext。因此,您需要为租户数据库提供另一个数据库上下文。因此,首先调用您的tenantdb,通过使用您的用户组织代码进行过滤来获取正确的租户连接字符串。然后用它创建一个新的目标dbcontext。

梦想是继续在每个页面中使用 DI,根据需要传递上下文。然后,上下文将足够智能,可以根据用户名的 OrgCode 选择正确的底层连接详细信息。

我正在与 DI 合作。

我为此tenantdb 创建了用于增删改查操作的UI 元素,因此我可以更新、删除、添加连接字符串详细信息和其他所需数据。密码在保存时加密,在获取时解密,然后传递到目标 dbcontext。

所以我的配置文件中有两个连接字符串。一种用于租户数据库,另一种用于默认目标数据库。这可以是一个空/虚拟的,因为如果您没有 DI 代码,您可能会遇到应用程序启动错误,因为它很可能会自动搜索连接字符串。

我也有开关代码。用户可以在此处切换到另一个租户。因此,在这里用户可以从它有权的所有租户中进行选择(是的,权限存储在tenantdb中)。这将再次触发上述代码步骤。

干杯。

本 Razor Pages 教程作为我的起点。

这样你就可以拥有耦合度非常低的目标数据库。唯一的重叠可能是用户 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")));
    }
Run Code Online (Sandbox Code Playgroud)

IndexModel(租户页面)。

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

租户。

 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; }
}
Run Code Online (Sandbox Code Playgroud)

租户用户。

public class TenantUser
{
    [Key]
    public Guid UserID { get; set; }
    public string TenantID { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

默认连接字符串。

{ "AllowedHosts": "*", "ConnectionStrings": { "TenantContext": "服务器=(localdb)\mssqllocaldb;Database=TenantContext;Trusted_Connection=True;MultipleActiveResultSets=true", "TargetContext": "服务器=(localdb) \mssqllocaldb;数据库=TargetContext;Trusted_Connection=True;MultipleActiveResultSets=true" }