Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Многие бизнес-приложения предназначены для работы с несколькими клиентами. Важно обеспечить защиту данных, чтобы данные клиентов не утекли и не были доступны другим клиентам и потенциальным конкурентам. Эти приложения классифицируются как "мультитенантные", так как каждый клиент считается клиентом приложения с собственным набором данных.
Предупреждение
В этой статье используется локальная база данных, которая не требует проверки подлинности пользователя. Рабочие приложения должны использовать самый безопасный поток проверки подлинности. Дополнительные сведения о проверке подлинности для развернутых тестовых и рабочих приложений см. в разделе "Безопасные потоки проверки подлинности".
Внимание
В этом документе приведены примеры и решения "как есть". Они не предназначены для того, чтобы быть "рекомендациями", а скорее "рабочими практиками" для вашего рассмотрения.
Совет
Исходный код этого примера можно просмотреть на сайте GitHub.
Поддержка мультитенантности
Существует множество подходов к реализации многотенантности в приложениях. Одним из распространенных подходов (иногда является требованием) является хранение данных для каждого клиента в отдельной базе данных. Схема одинакова, но данные зависят от клиента. Другим подходом является секционирование данных в существующей базе данных клиентом. Это можно сделать с помощью столбца в таблице или с таблицей в нескольких схемах с схемой для каждого клиента.
| Подход | Столбец для арендатора? | Схема для каждого клиента? | Несколько баз данных? | Поддержка EF Core |
|---|---|---|---|---|
| Дискриминатор (столбец) | Да | Нет | Нет | Глобальный фильтр запросов |
| База данных на каждый клиент | Нет | Нет | Да | Настройка |
| Схема для каждого клиента | Нет | Да | Нет | Не поддерживается |
Для подхода 'одна база данных на клиента' переключение на правильную базу данных так же просто, как предоставление правильной строки подключения. Если данные хранятся в одной базе данных, глобальный фильтр запросов можно использовать для автоматического фильтрации строк по столбцу идентификатора клиента, гарантируя, что разработчики не случайно записывают код, который может получить доступ к данным от других клиентов.
Эти примеры должны работать хорошо в большинстве моделей приложений, включая консоль, WPF, WinForms и приложения ASP.NET Core. Приложения Blazor Server требуют особого внимания.
Приложения Blazor Server и жизнь фабрики
Рекомендуемый шаблон использования Entity Framework Core в приложениях Blazor — зарегистрировать DbContextFactory, а затем вызвать его для создания нового экземпляра каждой DbContext операции. По умолчанию фабрика является синглтоном, так что для всех пользователей приложения существует только одна её копия. Это обычно нормально, потому что, хотя фабрика используется совместно, отдельные DbContext экземпляры не разделены.
Однако в многопользовательской системе строка подключения может изменяться для каждого пользователя. Так как фабрика кэширует конфигурацию с одинаковым временем существования, это означает, что все пользователи должны совместно использовать одну и ту же конфигурацию. Поэтому время существования должно быть изменено на Scoped.
Эта проблема не возникает в приложениях Blazor WebAssembly, так как синглтон привязан к пользователю. С другой стороны, приложения Blazor Server представляют собой уникальную задачу. Хотя приложение является веб-приложением, оно "сохраняется в живых" в режиме реального времени с помощью SignalR. Сеанс создается на пользователя и продолжается за пределами первоначального запроса. Для поддержки новых параметров следует предоставить новую фабрику для каждого пользователя. Время существования этой специальной фабрики ограничивается, а новый экземпляр создается для каждого сеанса пользователя.
Пример решения (отдельная база данных)
Возможное решение — создать простую ITenantService службу, которая обрабатывает настройку текущего клиента пользователя. Он предоставляет обратные вызовы, поэтому код оповещается при изменении арендатора. Реализация (с пропущенными для ясности обратными вызовами) может выглядеть следующим образом:
namespace Common
{
public interface ITenantService
{
string Tenant { get; }
void SetTenant(string tenant);
string[] GetTenants();
event TenantChangedEventHandler OnTenantChanged;
}
}
Затем DbContext может управлять мультитенантностью. Подход зависит от стратегии базы данных. Если вы храните всех арендаторов в одной базе данных, скорее всего, вы будете использовать фильтр для запросов.
ITenantService передается конструктору через внедрение зависимостей и используется для разрешения и хранения идентификатора клиента.
public ContactContext(
DbContextOptions<ContactContext> opts,
ITenantService service)
: base(opts) => _tenant = service.Tenant;
Метод OnModelCreating переопределяется, чтобы указать фильтр запросов:
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<MultitenantContact>()
.HasQueryFilter(mt => mt.Tenant == _tenant);
Это гарантирует, что каждый запрос фильтруется к арендатору при каждом запросе. Нет необходимости фильтровать в коде приложения, так как глобальный фильтр будет применен автоматически.
Поставщик клиента и DbContextFactory настроен в запуске приложения следующим образом, используя Sqlite в качестве примера:
builder.Services.AddDbContextFactory<ContactContext>(
opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);
Обратите внимание, что срок службы службы настроен с помощью ServiceLifetime.Scoped. Это позволяет ему принимать зависимость от поставщика клиента.
Примечание.
Зависимости всегда должны направляться в сторону синглтона. Это означает, что Scoped служба может зависеть от другой Scoped службы или Singleton службы, но Singleton служба может зависеть только от других Singleton служб: Transient => Scoped => Singleton
Несколько схем
Предупреждение
Этот сценарий не поддерживается непосредственно EF Core и не является рекомендуемом решением.
В другом подходе та же база данных может обрабатывать tenant1 и tenant2, используя схемы таблиц.
-
Арендатор1 -
tenant1.CustomerData -
Арендатор2 -
tenant2.CustomerData
Если вы не используете EF Core для обработки обновлений базы данных с миграцией и уже имеете таблицы с несколькими схемами, можно переопределить схему в DbContextOnModelCreating таком виде (схема таблицы CustomerData задана для клиента):
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);
Несколько баз данных и строки подключения
Реализация различных версий базы данных осуществляется путем передачи разных строк подключения для каждого клиента. Это можно настроить при запуске, разрешая поставщик услуг и используя его для создания строка подключения. В файл конфигурации добавляется appsettings.json строка подключения раздел клиента.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"TenantA": "Data Source=tenantacontacts.sqlite",
"TenantB": "Data Source=tenantbcontacts.sqlite"
},
"AllowedHosts": "*"
}
Служба и конфигурация внедряются в DbContext:
public ContactContext(
DbContextOptions<ContactContext> opts,
IConfiguration config,
ITenantService service)
: base(opts)
{
_tenantService = service;
_configuration = config;
}
Затем клиент используется для поиска строка подключения вOnConfiguring:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenant = _tenantService.Tenant;
var connectionStr = _configuration.GetConnectionString(tenant);
optionsBuilder.UseSqlite(connectionStr);
}
Это работает хорошо для большинства сценариев, если пользователь не может переключать арендаторы во время одного сеанса.
Переключение клиентов
В предыдущей конфигурации для нескольких баз данных параметры кэшируются на Scoped уровне. Это означает, что если пользователь изменяет клиент, параметры не переоценены, поэтому изменение клиента не отражается в запросах.
Простое решение для этого, если клиент может измениться, — установить время жизни на Transient., что гарантирует повторное вычисление клиента вместе со строкой подключения каждый раз, когда запрашивается DbContext. Пользователь может переключать арендаторы так часто, как им нравится. В следующей таблице вы можете выбрать, какое время существования имеет наибольшее значение для вашей фабрики.
| Сценарий | Отдельная база данных | Несколько баз данных |
|---|---|---|
| Пользователь остается в одном клиенте | Scoped |
Scoped |
| Пользователь может переключать арендаторы | Scoped |
Transient |
Значение по умолчанию Singleton по-прежнему имеет смысл, если ваша база данных не использует зависимости, ограниченные пользователем.
Заметки о производительности
EF Core был разработан таким образом, чтобы DbContext экземпляры можно было быстро создавать с минимальными затратами. По этой причине создание нового DbContext для каждой операции обычно не будет проблем. Если этот подход влияет на производительность вашего приложения, рассмотрите возможность использования пула DbContext.
Заключение
Это руководство по реализации мультитенантности в приложениях EF Core. Если у вас есть дополнительные примеры или сценарии или хотите предоставить отзыв, откройте проблему и обратитесь к этому документу.