C# 如何在ASP.NET 5 Web API和Entity Framework 7中设置多租户?

C# 如何在ASP.NET 5 Web API和Entity Framework 7中设置多租户?,c#,asp.net-web-api,asp.net-core,entity-framework-core,C#,Asp.net Web Api,Asp.net Core,Entity Framework Core,我正在使用ASP.NET5的WebAPI和EF7构建一个后端服务,以设置多租户数据库结构。要求如下: public class TenantContextFactory : ITenantContextFactory { /// <summary> /// The tenant configurations store. /// </summary> private IDictionary<string, DbContextOptio

我正在使用ASP.NET5的WebAPI和EF7构建一个后端服务,以设置多租户数据库结构。要求如下:

public class TenantContextFactory : ITenantContextFactory
{
    /// <summary>
    /// The tenant configurations store.
    /// </summary>
    private IDictionary<string, DbContextOptions> tenants;

    /// <summary>
    /// Creates a new TenantContextFactory
    /// </summary>
    public TenantContextFactory()
    {
        tenants = new Dictionary<string, DbContextOptions>();
    }

    /// <summary>
    /// Registers a tenant configuration with the store.
    /// </summary>
    /// <param name="id">The tenant id.</param>
    /// <param name="options">The context options.</param>
    public void RegisterTenant(string id, DbContextOptions options)
    {
        if (!tenants.ContainsKey(id))
        {
            tenants.Add(id, options);
        }
    }

    /// <summary>
    /// Creates a DbContext instance for the specified tenant.
    /// </summary>
    /// <typeparam name="T">The type of DbContext to create.</typeparam>
    /// <param name="id">The tenant id.</param>
    /// <returns>A new instance of the desired DbContext</returns>
    public T GetTenantContext<T>(string id) where T : DbContext
    {
        DbContextOptions options;
        if (tenants.TryGetValue(id, out options))
        {
            // get the type of the desired DbContext and return a new instance
            // with the DbContextOptions as the constructor parameter
            return (T)Activator.CreateInstance(typeof(T), options);
        }

        return null;
    }
}
  • API端点对于所有租户都是相同的
  • 每个租户都有自己的数据库,其中包含特定于租户的数据
  • 所有租户使用相同的数据库结构,因此可以在所有租户之间使用相同的DbContext类,但连接字符串不同
  • 每个租户数据库都包含基于ASP.NET标识进行身份验证的用户信息
目前,我面临以下挑战:

  • DbContext实例不是线程安全的,因此它们的生命周期应该很短。这意味着我不能简单地将DbContext的实例存储在某个地方,而是必须在需要时动态创建和处置实例
  • 必须能够动态添加或删除租户,最好不用重新启动服务
  • EF7迁移需要工作
为了使服务能够动态添加或删除租户,我当前的实现基于一个JSON配置文件,该文件包含键值对中的所有租户连接字符串,如下所示:

{
   "Tenants": [
      { "Tenant1": "Server=.\\SQLEXPRESS;Database=Tenant1;integrated security=True;" },
      { "Tenant2": "Server=.\\SQLEXPRESS;Database=Tenant2;integrated security=True;" }
   ]
}
    public static class ExtensionMethods
{
    /// <summary>
    /// Adds multi tenancy to the service.
    /// </summary>
    /// <param name="services">The service collection</param>
    /// <param name="config">The configuration object</param>
    public static void AddMultiTenancy(this IServiceCollection services, IConfiguration config)
    {
        var tenantContextFactory = new TenantContextFactory();

        // get the information from the JSON file
        var tenants = config.GetSection("Tenants");
        var values = tenants.GetChildren();
        foreach (var key in values)
        {
            foreach (var item in key.GetChildren())
            {
                // get the correct name of the config node
                var tenantId = item.Key.Split(':').Last();

                // and the connection string
                var connectionString = item.Value;

                // create the OptionsBuilder and configure it to use SQL server with the connection string
                var builder = new DbContextOptionsBuilder();
                builder.UseSqlServer(connectionString);

                // and register it with the factory
                tenantContextFactory.RegisterTenant(tenantId, builder.Options);
            }   
        }

        // register the factory with the DI container
        services.AddInstance(typeof(ITenantContextFactory), tenantContextFactory);
    }
}
然后使用此配置设置ContextFactory。此工厂使用DbContextOptions存储,以便在需要时动态创建DbContext实例,从而实现必要的短生命周期。工厂定义如下:

public class TenantContextFactory : ITenantContextFactory
{
    /// <summary>
    /// The tenant configurations store.
    /// </summary>
    private IDictionary<string, DbContextOptions> tenants;

    /// <summary>
    /// Creates a new TenantContextFactory
    /// </summary>
    public TenantContextFactory()
    {
        tenants = new Dictionary<string, DbContextOptions>();
    }

    /// <summary>
    /// Registers a tenant configuration with the store.
    /// </summary>
    /// <param name="id">The tenant id.</param>
    /// <param name="options">The context options.</param>
    public void RegisterTenant(string id, DbContextOptions options)
    {
        if (!tenants.ContainsKey(id))
        {
            tenants.Add(id, options);
        }
    }

    /// <summary>
    /// Creates a DbContext instance for the specified tenant.
    /// </summary>
    /// <typeparam name="T">The type of DbContext to create.</typeparam>
    /// <param name="id">The tenant id.</param>
    /// <returns>A new instance of the desired DbContext</returns>
    public T GetTenantContext<T>(string id) where T : DbContext
    {
        DbContextOptions options;
        if (tenants.TryGetValue(id, out options))
        {
            // get the type of the desired DbContext and return a new instance
            // with the DbContextOptions as the constructor parameter
            return (T)Activator.CreateInstance(typeof(T), options);
        }

        return null;
    }
}
公共类租户ContextFactory:ITenantContextFactory
{
/// 
///租户配置存储。
/// 
私人词典租户;
/// 
///创建新的租户ContextFactory
/// 
公共租户联合工厂()
{
租户=新字典();
}
/// 
///向存储注册租户配置。
/// 
///租户id。
///上下文选项。
公共无效注册表项(字符串id、DbContextOptions选项)
{
如果(!tenants.ContainsKey(id))
{
租户。添加(id、选项);
}
}
/// 
///为指定租户创建DbContext实例。
/// 
///要创建的DbContext的类型。
///租户id。
///所需DbContext的新实例
公共T GetTenantContext(字符串id),其中T:DbContext
{
DbContextOptions选项;
if(租户.TryGetValue(id,输出选项))
{
//获取所需DbContext的类型并返回一个新实例
//使用DbContextOptions作为构造函数参数
return(T)Activator.CreateInstance(typeof(T),options);
}
返回null;
}
}
在配置阶段,使用如下扩展方法向ContextFactory填充租户信息:

{
   "Tenants": [
      { "Tenant1": "Server=.\\SQLEXPRESS;Database=Tenant1;integrated security=True;" },
      { "Tenant2": "Server=.\\SQLEXPRESS;Database=Tenant2;integrated security=True;" }
   ]
}
    public static class ExtensionMethods
{
    /// <summary>
    /// Adds multi tenancy to the service.
    /// </summary>
    /// <param name="services">The service collection</param>
    /// <param name="config">The configuration object</param>
    public static void AddMultiTenancy(this IServiceCollection services, IConfiguration config)
    {
        var tenantContextFactory = new TenantContextFactory();

        // get the information from the JSON file
        var tenants = config.GetSection("Tenants");
        var values = tenants.GetChildren();
        foreach (var key in values)
        {
            foreach (var item in key.GetChildren())
            {
                // get the correct name of the config node
                var tenantId = item.Key.Split(':').Last();

                // and the connection string
                var connectionString = item.Value;

                // create the OptionsBuilder and configure it to use SQL server with the connection string
                var builder = new DbContextOptionsBuilder();
                builder.UseSqlServer(connectionString);

                // and register it with the factory
                tenantContextFactory.RegisterTenant(tenantId, builder.Options);
            }   
        }

        // register the factory with the DI container
        services.AddInstance(typeof(ITenantContextFactory), tenantContextFactory);
    }
}
公共静态类扩展方法
{
/// 
///向服务添加多租户。
/// 
///服务收集
///配置对象
公共静态void addMultitenance(此IServiceCollection服务,IConfiguration配置)
{
var tenantContextFactory=新的tenantContextFactory();
//从JSON文件中获取信息
var tenants=config.GetSection(“租户”);
var values=tenants.GetChildren();
foreach(变量输入值)
{
foreach(key.GetChildren()中的var项)
{
//获取配置节点的正确名称
var tenantId=item.Key.Split(“:”).Last();
//和连接字符串
var connectionString=item.Value;
//创建OptionBuilder并将其配置为使用SQL server和连接字符串
var builder=new DbContextOptionsBuilder();
使用SQLServer(connectionString);
//并在工厂登记
RegisterTenant(tenantId、builder.Options);
}   
}
//使用DI容器注册工厂
服务.附加(类型为(ITenantContextFactory),租户ContextFactory);
}
}
然后,工厂可以作为服务注入任何需要它的控制器或服务,并正确实例化所需的上下文

到目前为止还不错。以下问题仍然存在:

如何集成EF7迁移?(已解决)

尝试添加迁移时出现以下错误:

System.InvalidOperationException:不支持任何数据库提供程序 配置。通过覆盖OnConfigurang来配置数据库提供程序 在DbContext类中或在设置时在AddDbContext方法中 服务

由于租户的数量是动态的,因此我不能直接在DbContext类中指定连接字符串,也不能使用AddDbContext方法在单个数据库中静态注册DbContext

当我提供静态连接字符串时,迁移会成功创建,但当我尝试使用动态方法时,这些迁移不会应用于正在使用的数据库,并且我无法在EF shell命令中指定连接字符串,以便手动或通过shell脚本执行迁移。我基本上必须为每个租户重写一次配置代码,重新编译,然后使用shell命令执行迁移,这不是一个值得选择的选项

解决方案:

对于要使用的每个上下文,使用以下代码段来迁移上下文:

using (var context = tenantContextFactory.GetTenantContext<MyContext>(tenantId))
{
     context.Database.Migrate();
}
使用(var context=tenantContextFactory.GetTenantContext(tenantId))
{
Migrate();
}
这将自动检查数据库模式是否符合最新的迁移,并在不符合时应用它

如何整合?

需要调整身份验证过程以登录用户