Как настроить многопользовательский режим в ASP.NET 5 Web API и Entity Framework 7?
Я создаю бэкэнд-сервис, используя веб-API и EF7 ASP.NET5 для настройки структуры базы данных с несколькими арендаторами. Требования следующие:
- Конечные точки API одинаковы для всех арендаторов,
- каждый арендатор имеет свою собственную базу данных, содержащую специфические данные арендатора,
- все арендаторы используют одну и ту же структуру базы данных, поэтому одни и те же классы DbContext могут использоваться для всех арендаторов, но с разными строками соединения,
- каждая база данных арендаторов содержит информацию о пользователе для аутентификации на основе ASP.NET Identity.
В настоящий момент я сталкиваюсь со следующими проблемами:
- Экземпляры DbContext не являются поточно-ориентированными, поэтому их жизненный цикл должен быть коротким. Это означает, что я не могу просто где-то хранить экземпляры DbContexts, но должен динамически создавать и располагать экземпляры, когда это необходимо,
- Должна быть возможность динамического добавления или удаления арендаторов, желательно без перезапуска службы,
- Миграции 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);
}
}
Затем фабрика может быть введена в качестве службы любому контроллеру или службе, которая нуждается в ней, и правильно создает нужные контексты.
Все идет нормально. Следующие вопросы остаются:
Как интегрировать миграции EF7? (решена)
Пытаясь добавить миграции, я получаю следующую ошибку:
System.InvalidOperationException: поставщики базы данных не настроены. Настройте поставщика базы данных, переопределив OnConfiguring в своем классе DbContext или в методе AddDbContext при настройке служб.
Поскольку число арендаторов является динамическим, я не могу указать строку подключения непосредственно в классе DbContext или использовать метод AddDbContext для статической регистрации DbContexts в одной базе данных.
Когда я предоставляю статическую строку соединения, миграции успешно создаются, но когда я пытаюсь использовать свой динамический подход, эти миграции не применяются к используемым базам данных, и у меня нет возможности указать строку соединения в оболочке EF Команды для выполнения миграции вручную или через скрипт оболочки. В основном мне пришлось бы переписывать код конфигурации один раз для каждого клиента, перекомпилировать и затем использовать команду оболочки для выполнения миграции, что не является целесообразным вариантом.
РЕШЕНИЕ:
Контексты, которые нужно перенести, используя следующий фрагмент для каждого контекста, который вы хотите использовать:
using (var context = tenantContextFactory.GetTenantContext<MyContext>(tenantId))
{
context.Database.Migrate();
}
Это автоматически проверяет базу данных, соответствует ли схема последним миграциям, и применяет ее, когда нет.
Как интегрировать ASP.NET Identity?
Процесс аутентификации должен быть настроен для правильной регистрации пользователей.
Я нахожусь на этом прямо сейчас и буду публиковать обновления о моем прогрессе здесь.
Как поменять жильцов во время выполнения?
Это связано с предыдущими вопросами. Как я могу гарантировать, что я могу безопасно добавлять или удалять арендаторов, редактируя файл конфигурации без необходимости перезапуска службы? Это вообще возможно?
РЕДАКТИРОВАТЬ:
Я нашел решение проблемы миграции в EF7. Следующая проблема - удостоверение ASP.NET.
1 ответ
Как многие предполагали, было бы намного чище иметь одну базу данных. Смотрите мой комментарий в обсуждениях..
В базе данных есть таблица провайдеров / источников.
Затем создайте репо, которые должны принять SourceId.
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
}
}