Closed. This question needs to be more
focused。它当前不接受答案。
想要改善这个问题吗?更新问题,使它仅关注
editing this post的一个问题。
5年前关闭。
Improve this question
我正在使用ASP.NET5的Web API和EF7构建后端服务,以设置 Multi-Tenancy 数据库结构。要求如下:
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;" }
]
}
然后,此配置用于设置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中:
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);
}
}
然后可以将工厂作为服务注入(inject)到需要它的任何 Controller 或服务中,并正确地实例化所需的上下文。
到现在为止还挺好。仍然存在以下问题:
如何集成EF7迁移? (已解决)
尝试添加迁移时,出现以下错误:
System.InvalidOperationException: No database providers are configured. Configure a database provider by overriding OnConfiguring in your DbContext class or in the AddDbContext method when setting up services.
由于租户的数量是动态的,因此我无法直接在DbContext类中指定连接字符串,也无法使用AddDbContext方法向单个数据库静态注册DbContext。
当我确实提供静态连接字符串时,会成功创建迁移,但是当我尝试使用动态方法时,这些迁移将不会应用于所使用的数据库,并且我无法在EF Shell中指定连接字符串命令以手动或通过Shell脚本执行迁移。我基本上必须为每个租户重写一次配置代码,重新编译,然后使用Shell命令执行迁移,这不是一个值得的选择。
解决方案:
通过对要使用的每个上下文使用以下片段来迁移上下文:
using (var context = tenantContextFactory.GetTenantContext<MyContext>(tenantId))
{
context.Database.Migrate();
}
这会自动检查数据库是否符合最新的迁移模式,如果不符合,则应用数据库。
如何集成ASP.NET Identity?
需要调整身份验证过程以正确登录用户。
我现在正在处理,并将在此处发布有关我的进度的更新。
如何在运行时更改租户?
这与前面的问题有关。如何确保可以通过编辑配置文件安全地添加或删除租户,而不必重新启动服务?那有可能吗?
编辑:
我在EF7中找到了迁移问题的解决方案。下一个挑战是ASP.NET身份。
正如许多人建议的那样,拥有一个数据库会更加干净。
请参阅我在讨论中的评论。
在数据库中有一个提供程序/源表。
然后创建必须接受SourceId的Repo。
public interface IRepository<T>
{
T GetById(int sourceid, int id);
}
public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class
{
public TEntity GetById(int sourceId, int id)
{
return _dbContext.Set<TEntity>().Find(sourceId, id);
}
}
我知道这将要求您基本上在每个表上都有SourceId。
我知道这不是您要寻找的答案,而是可能需要考虑的问题。
对我来说,代码非常雄心勃勃,维护起来将非常复杂。
但我希望你做对了!
更新
using(var context = new MyContext(DbHelper.GetConnectionString()))
{
}
public static class DbHelper
{
public static string GetConnectionString()
{
//some logic to get the corrosponding connection string
// which you are wanting this could be based of url
}
}
我是一名优秀的程序员,十分优秀!