Víceklientská architektura

Řada obchodních aplikací je navržená tak, aby fungovala s více zákazníky. Je důležité zabezpečit data tak, aby zákaznická data nebyla "unikla" ani nebyla viditelná jinými zákazníky a potenciálními konkurenty. Tyto aplikace jsou klasifikovány jako "víceklient", protože každý zákazník se považuje za tenanta aplikace s vlastní sadou dat.

Důležité

Tento dokument obsahuje příklady a řešení tak, jak jsou. Nejedná se o osvědčené postupy, ale spíše o pracovní postupy pro vaše úvahy.

Tip

Zdrojový kód pro tuto ukázku můžete zobrazit na GitHubu.

Podpora víceklientské architektury

Existuje mnoho přístupů k implementaci víceklientské architektury v aplikacích. Jedním z běžných přístupů (někdy je to požadavek) je uchovávat data pro každého zákazníka v samostatné databázi. Schéma je stejné, ale data jsou specifická pro zákazníky. Dalším přístupem je rozdělení dat do existující databáze zákazníkem. To lze provést pomocí sloupce v tabulce nebo s tabulkou ve více schématech se schématem pro každého tenanta.

Přístup Sloupec pro tenanta? Schéma na tenanta? Více databází? Podpora EF Core
Diskriminátor (sloupec) Ano No Číslo Globální filtr dotazů
Databáze na tenanta Číslo Ne Ano Konfigurace
Schéma na tenanta Číslo Ano Ne Nepodporováno

Pro přístup k databázi pro jednotlivé tenanty je přechod na správnou databázi stejně jednoduchý jako poskytnutí správných připojovací řetězec. Když jsou data uložená v jedné databázi, dá se globální filtr dotazů použít k automatickému filtrování řádků podle sloupce ID tenanta a zajistit, aby vývojáři nechtěně nenapsal kód, který má přístup k datům od jiných zákazníků.

Tyto příklady by měly fungovat správně ve většině modelů aplikací, včetně konzoly, WPF, WinForms a aplikací ASP.NET Core. Aplikace Blazor Serveru vyžadují zvláštní pozornost.

Blazor Server apps and the life of the factory

Doporučeným vzorem pro použití Entity Framework Core v aplikacích Blazor je registrace DbContextFactory a jeho voláním vytvořit novou instanci DbContext každé operace. Ve výchozím nastavení je továrna jednoúčelová, takže pro všechny uživatele aplikace existuje jenom jedna kopie. To je obvykle v pořádku, protože i když je továrna sdílená, jednotlivé DbContext instance nejsou.

U víceklientské architektury se ale připojovací řetězec může změnit na uživatele. Vzhledem k tomu, že továrna ukládá konfiguraci do mezipaměti se stejnou životností, znamená to, že všichni uživatelé musí sdílet stejnou konfiguraci. Proto by se životnost měla změnit na Scoped.

K tomuto problému nedochází v aplikacích Blazor WebAssembly, protože jednoúčelový typ je omezený na uživatele. Aplikace Blazor Serveru na druhou stranu představují jedinečnou výzvu. I když je aplikace webovou aplikací, je "udržována naživu" komunikací v reálném čase pomocí služby SignalR. Relace se vytvoří pro jednotlivé uživatele a trvá nad rámec počátečního požadavku. Aby bylo možné povolit nová nastavení, měla by být pro uživatele k dispozici nová továrna. Doba života pro tuto speciální továrnu je vymezena a pro každou uživatelskou relaci se vytvoří nová instance.

Ukázkové řešení (jednoúčelová databáze)

Možným řešením je vytvořit jednoduchou ITenantService službu, která zpracovává nastavení aktuálního tenanta uživatele. Poskytuje zpětná volání, aby kód byl upozorněn při změně tenanta. Implementace (s vynechanými zpětnými voláními pro přehlednost) může vypadat takto:

namespace Common
{
    public interface ITenantService
    {
        string Tenant { get; }

        void SetTenant(string tenant);

        string[] GetTenants();

        event TenantChangedEventHandler OnTenantChanged;
    }
}

Ten DbContext pak může spravovat víceklientské architektury. Přístup závisí na vaší databázové strategii. Pokud ukládáte všechny tenanty do jedné databáze, pravděpodobně použijete filtr dotazu. Předá ITenantService se konstruktoru prostřednictvím injektáže závislostí a používá se k překladu a uložení identifikátoru tenanta.

public ContactContext(
    DbContextOptions<ContactContext> opts,
    ITenantService service)
    : base(opts) => _tenant = service.Tenant;

Metoda OnModelCreating se přepíše a určí filtr dotazu:

protected override void OnModelCreating(ModelBuilder modelBuilder)
    => modelBuilder.Entity<MultitenantContact>()
        .HasQueryFilter(mt => mt.Tenant == _tenant);

Tím zajistíte, že se každý dotaz vyfiltruje do tenanta na každém požadavku. V kódu aplikace není potřeba filtrovat, protože globální filtr se použije automaticky.

Poskytovatel tenanta a DbContextFactory je nakonfigurovaný při spuštění aplikace takto:

builder.Services.AddDbContextFactory<ContactContext>(
    opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);

Všimněte si, že životnost služby je nakonfigurovaná pomocí ServiceLifetime.Scoped. To umožňuje převzít závislost na poskytovateli tenanta.

Poznámka

Závislosti musí vždy probíhat směrem k jednoúčelové závislosti. To znamená, že Scoped služba může záviset na jiné Scoped službě nebo službě Singleton , ale Singleton služba může záviset pouze na jiných Singleton službách: Transient => Scoped => Singleton.

Více schémat

Upozorňující

Tento scénář přímo nepodporuje EF Core a není doporučeným řešením.

V jiném přístupu může stejná databáze zpracovávat tenant1 schémata tabulek a tenant2 používat je.

  • Tenant1 - tenant1.CustomerData
  • Tenant2 - tenant2.CustomerData

Pokud nepoužíváte EF Core ke zpracování aktualizací databáze pomocí migrací a už máte tabulky s více schématy, můžete schéma přepsat v OnModelCreating podobném příkladu DbContext (schéma pro tabulku CustomerData je nastavené na tenanta):

protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);

Více databází a připojovací řetězec

Více verzí databáze se implementuje předáním jiného připojovací řetězec pro každého tenanta. To je možné nakonfigurovat při spuštění tím, že přeloží poskytovatele služeb a použije ho k sestavení připojovací řetězec. Do konfiguračního souboru se přidá oddíl připojovací řetězec podle tenantaappsettings.json.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "TenantA": "Data Source=tenantacontacts.sqlite",
    "TenantB": "Data Source=tenantbcontacts.sqlite"
  },
  "AllowedHosts": "*"
}

Služba i konfigurace se vloží do DbContext:

public ContactContext(
    DbContextOptions<ContactContext> opts,
    IConfiguration config,
    ITenantService service)
    : base(opts)
{
    _tenantService = service;
    _configuration = config;
}

Tenant se pak použije k vyhledání připojovací řetězec vOnConfiguring:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var tenant = _tenantService.Tenant;
    var connectionStr = _configuration.GetConnectionString(tenant);
    optionsBuilder.UseSqlite(connectionStr);
}

To funguje správně pro většinu scénářů, pokud uživatel nemůže přepnout tenanty během stejné relace.

Přepínání tenantů

V předchozí konfiguraci pro více databází se možnosti ukládají do mezipaměti na Scoped úrovni. To znamená, že pokud uživatel změní tenanta, možnosti se znovu nevyhodnocují , takže se změna tenanta neprojeví v dotazech.

Snadné řešení pro to, když se tenant může změnit, je nastavit dobu životnosti tak, aby Transient. se zajistilo, že se tenant znovu vyhodnotí spolu s připojovací řetězec při DbContext každém vyžádání. Uživatel může tenanty přepínat tak často, jak se jim líbí. Následující tabulka vám pomůže zvolit, která doba života je pro vaši továrnu nejvhodnější.

Scénář Jednoúčelová databáze Více databází
Uživatel zůstane v jednom tenantovi. Scoped Scoped
Uživatel může přepínat tenanty Scoped Transient

Výchozí hodnota Singleton stále dává smysl, pokud vaše databáze nepřebírají závislosti v oboru uživatele.

Poznámky k výkonu

EF Core byla navržena tak, aby DbContext bylo možné rychle vytvořit instanci instancí s co nejmenší režií. Z tohoto důvodu by mělo být vytvoření nové DbContext operace obvykle v pořádku. Pokud tento přístup ovlivňuje výkon vaší aplikace, zvažte použití sdružování DbContext.

Závěr

Toto je pracovní pokyny pro implementaci víceklientské architektury v aplikacích EF Core. Pokud máte další příklady nebo scénáře nebo chcete poskytnout zpětnou vazbu, otevřete problém a projděte si tento dokument.