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

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映射到连接字符串的东西 有许多关于堆栈溢出的部分示例。经过一个下午的阅读,我感到很困惑。其核心是: 从构造函数参

我有(几乎)最糟糕的多租户。我正在构建一个asp.net核心网站,我正在将一堆小小的内部网站移植到这个网站上。每个子网站将是一个asp.net区域。我有一个
IdentityContext
用于标识内容。我有多个供应商数据库副本,每个副本都有多个租户。
ApplicationUser
类有一个OrgCode属性,我想用它来切换db上下文

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

有许多关于堆栈溢出的部分示例。经过一个下午的阅读,我感到很困惑。其核心是:

  • 从构造函数参数中删除DI dbcontext ref
  • 在控制器构造函数中实例化dbcontext
  • 像前面一样使用dbcontext
我走对了吗?

有连贯的例子吗?


编辑2020/07/09 不幸的是,这已变得更加紧迫

身份数据库是租户不可知的。标识中的每个用户都有一个组织代码标识符。(自定义用户属性)

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

  • 核心供应商数据库
  • 存储扩展的自定义数据库
  • 记录作业输出的数据库
  • 还有一些特定于应用程序的小型数据库已经使用组织代码来识别用户

    服务器A-1组织代码

    服务器B-4组织代码

    服务器C-3参与项目的组织代码,50+尚未完成(大部分较小)

    服务器D-到目前为止没有使用组织代码。服务器上有80多个。(很快)

    不可能将所有组织整合到一台服务器上。有法律和技术方面的影响。每个服务器都有数百个需要更新的远程转发器向它们报告。这些提供的数据就是我们的定制作业所使用的数据

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

    我不太愿意使用proxy这个词,因为它在这个空间中似乎负载很重

    该死,如果我知道该放在哪里,即使使用switch语句也可以

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

    编辑2020/07/13 为了整理引用,我将OrgCode和服务器切换到Enum。上下文继承如下所示

    • 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)

      • 卖方上下文

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


    目前认为我需要一个收集上下文的类,该类将拥有一个
    字典
    ,并注册为服务。非常确定,我需要为每个继承的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);
        }
    }