Мультитенантность

Многие бизнес-приложения предназначены для работы с несколькими клиентами. Важно обеспечить защиту данных, чтобы данные клиента не были "утечками" или видели другие клиенты и потенциальные конкуренты. Эти приложения классифицируются как "мультитенантные", так как каждый клиент считается клиентом приложения с собственным набором данных.

Важно!

В этом документе приведены примеры и решения "как есть". Они не предназначены для того, чтобы быть "рекомендациями", а скорее "рабочими практиками" для вашего рассмотрения.

Совет

Исходный код этого примера можно просмотреть на сайте GitHub.

Поддержка мультитенантности

Существует множество подходов к реализации многотенантности в приложениях. Одним из распространенных подходов (иногда является требованием) является хранение данных для каждого клиента в отдельной базе данных. Схема одинакова, но данные зависят от клиента. Другим подходом является секционирование данных в существующей базе данных клиентом. Это можно сделать с помощью столбца в таблице или с таблицей в нескольких схемах с схемой для каждого клиента.

Подход Столбец для клиента? Схема для каждого клиента? Несколько баз данных? Поддержка EF Core
Дискриминатор (столбец) Да No No Глобальный фильтр запросов
Однотенантная база данных No Нет Да Настройка
Схема для каждого клиента No Да No Не поддерживается

Для подхода к базе данных на клиент переключение на правильную базу данных так же просто, как предоставление правильной строка подключения. Если данные хранятся в одной базе данных, глобальный фильтр запросов можно использовать для автоматического фильтрации строк по столбцу идентификатора клиента, гарантируя, что разработчики не случайно записывают код, который может получить доступ к данным от других клиентов.

Эти примеры должны работать хорошо в большинстве моделей приложений, включая консоль, 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. Если у вас есть дополнительные примеры или сценарии или хотите предоставить отзыв, откройте проблему и обратитесь к этому документу.