Udostępnij przez


Wielodzierżawność

Wiele aplikacji biznesowych jest przeznaczonych do pracy z wieloma klientami. Ważne jest, aby zabezpieczyć dane, aby dane klientów nie zostały "ujawnione" ani widoczne dla innych klientów i potencjalnych konkurentów. Te aplikacje są klasyfikowane jako "wielodzierżawcze", ponieważ każdy klient jest uważany za najemcę aplikacji z własnym zestawem danych.

Ostrzeżenie

W tym artykule jest używana lokalna baza danych, która nie wymaga uwierzytelnienia użytkownika. Aplikacje produkcyjne powinny korzystać z najbezpieczniejszego dostępnego przepływu uwierzytelniania. Aby uzyskać więcej informacji na temat uwierzytelniania dla wdrożonych aplikacji testowych i produkcyjnych, zobacz Bezpieczne przepływy uwierzytelniania.

Ważne

Ten dokument zawiera przykłady i rozwiązania "jak to jest". Nie mają one być "najlepszymi rozwiązaniami", ale raczej "praktykami roboczymi" do rozważenia.

Obsługa wielości najemców

Istnieje wiele podejść do implementacji wielotenancyjności w aplikacjach. Jednym z typowych podejść (czasami jest to wymaganie) jest przechowywanie danych dla każdego klienta w oddzielnej bazie danych. Schemat jest taki sam, ale dane są specyficzne dla klienta. Innym podejściem jest partycjonowanie danych w istniejącej bazie danych przez klienta. Można to zrobić, używając kolumny w tabeli lub mając tabelę w wielu schematach, gdzie każdy schemat odpowiada innemu najemcy.

Metoda Kolumna dla najemcy? Schemat dla dzierżawcy? Wiele baz danych? Obsługa platformy EF Core
Dyskryminujące (kolumna) Tak Nie. Nie. Globalny filtr zapytań
Baza danych dla każdego dzierżawcy Nie. Nie. Tak Konfigurowanie
Schemat na najemcę Nie. Tak Nie. Brak wsparcia

W przypadku podejścia bazy danych na dzierżawcę przełączenie do właściwej bazy danych jest tak proste, jak zapewnienie poprawnego parametru połączenia. Gdy dane są przechowywane w pojedynczej bazie danych, globalny filtr zapytań może służyć do automatycznego filtrowania wierszy według kolumny identyfikatora dzierżawy, dzięki czemu deweloperzy nie będą przypadkowo pisać kodu, który może uzyskiwać dostęp do danych od innych klientów.

Te przykłady powinny działać dobrze w większości modeli aplikacji, w tym w konsoli, WPF, WinForms i ASP.NET Core. Aplikacje Blazor Server wymagają szczególnej uwagi.

Aplikacje blazor Server i żywotność fabryki

Zalecanym wzorcem korzystania z platformy Entity Framework Core w aplikacjach Platformy Blazor jest zarejestrowanie elementu DbContextFactory, a następnie wywołanie go w celu utworzenia nowego wystąpienia DbContext każdej operacji. Domyślnie fabryka jest pojedynczą kopią, więc dla wszystkich użytkowników aplikacji istnieje tylko jedna kopia. Zwykle jest to w porządku, ponieważ mimo że fabryka jest współdzielona, poszczególne DbContext wystąpienia nie są.

Jednak w przypadku wielu dzierżaw łańcuch połączenia może się zmieniać dla każdego użytkownika. Ponieważ fabryka buforuje konfigurację z tym samym okresem istnienia, oznacza to, że wszyscy użytkownicy muszą współużytkować tę samą konfigurację. W związku z tym okres istnienia należy zmienić na Scoped.

W aplikacjach Blazor WebAssembly ten problem nie występuje, ponieważ singleton jest zdefiniowany dla użytkownika. Natomiast aplikacje Blazor Server stanowią wyjątkowe wyzwanie. Chociaż aplikacja jest aplikacją internetową, jest "utrzymywana przy życiu" przez komunikację w czasie rzeczywistym przy użyciu usługi SignalR. Sesja jest tworzona dla użytkownika i trwa dłużej niż początkowe żądanie. Nowa fabryka powinna być udostępniana dla każdego użytkownika, aby zezwolić na nowe ustawienia. Okres istnienia tej specjalnej fabryki jest dokładnie określony, a nowe wystąpienie jest tworzone dla każdej sesji użytkownika.

Przykładowe rozwiązanie (pojedyncza baza danych)

Możliwe rozwiązanie polega na utworzeniu prostej ITenantService usługi obsługującej ustawianie bieżącej dzierżawy użytkownika. Zapewnia wywołania zwrotne, aby kod był powiadamiany o zmianie najemcy. Implementacja (z pominięciem wywołań zwrotnych dla jasności) może wyglądać następująco:

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

        void SetTenant(string tenant);

        string[] GetTenants();

        event TenantChangedEventHandler OnTenantChanged;
    }
}

Następnie DbContext może zarządzać wielodostępnością. Podejście zależy od strategii bazy danych. Jeśli przechowujesz wszystkich najemców w jednej bazie danych, prawdopodobnie użyjesz filtru zapytania. Element ITenantService jest przekazywany do konstruktora za pośrednictwem wstrzykiwania zależności i służy do ustalania i przechowywania identyfikatora dzierżawy.

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

Metoda OnModelCreating jest zastępowana w celu określenia filtru zapytania:

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

Dzięki temu każde żądanie jest filtrowane w kontekście dzierżawcy. Nie ma potrzeby filtrowania w kodzie aplikacji, ponieważ filtr globalny zostanie zastosowany automatycznie.

Dostawca najemcy i DbContextFactory są skonfigurowane podczas uruchamiania aplikacji w następujący sposób, używając Sqlite jako przykładu.

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

Zwróć uwagę, że okres istnienia usługi jest skonfigurowany przy użyciu polecenia ServiceLifetime.Scoped. Umożliwia to podjęcie zależności od dostawcy dzierżawy.

Uwaga

Zależności muszą zawsze przepływać w kierunku singletonu. Oznacza to, że Scoped usługa może zależeć od innej Scoped usługi lub Singleton usługi, ale Singleton usługa może zależeć tylko od innych Singleton usług: Transient => Scoped => Singleton.

Wiele schematów

Ostrzeżenie

Ten scenariusz nie jest bezpośrednio obsługiwany przez program EF Core i nie jest zalecanym rozwiązaniem.

W innym podejściu ta sama baza danych może obsługiwać tenant1 i tenant2 za pomocą schematów tabel.

  • Najemca1 - tenant1.CustomerData
  • Najemca2 - tenant2.CustomerData

Jeśli nie używasz frameworku EF Core do zarządzania aktualizacjami bazy danych przy pomocy migracji i masz już tabele z wieloma schematami, możesz przesłonić schemat w pliku DbContext w OnModelCreating w następujący sposób (schemat tabeli CustomerData jest ustawiony na najemcę):

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

Wiele baz danych i parametry połączenia

Wersja wielu baz danych jest implementowana przez przekazanie różnych ciągów połączeniowych dla każdego najemcy. Można to skonfigurować podczas uruchamiania, rozpoznając dostawcę usług i używając go do zbudowania łańcucha połączenia. String połączenia dla sekcji dzierżawy jest dodawany do pliku konfiguracji appsettings.json.

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

Usługa i konfiguracja są wstrzykiwane do elementu DbContext:

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

Dzierżawca jest następnie używany do wyszukiwania łańcucha połączenia w OnConfiguring.

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

Działa to dobrze w przypadku większości scenariuszy, chyba że użytkownik może przełączać tenantów podczas tej samej sesji.

Przełączanie najemców

W poprzedniej konfiguracji dla wielu baz danych opcje są buforowane na Scoped poziomie. Oznacza to, że jeśli użytkownik zmieni dzierżawę, opcje nie zostaną ponownie ocenione, w związku z tym zmiana dzierżawy nie zostanie odzwierciedlona w zapytaniach.

Rozwiązaniem tego problemu, gdy dzierżawa może ulec zmianie, jest ustawienie okresu istnienia Transient.. To gwarantuje, że dzierżawa jest ponownie oceniana wraz z parametrem połączenia za każdym razem, gdy DbContext jest wymagane. Użytkownik może przełączać najemców tak często, jak chcą. W poniższej tabeli przedstawiono wybór okresu istnienia, który jest najbardziej odpowiedni dla twojej fabryki.

Scenariusz Pojedyncza baza danych Wiele baz danych
Użytkownik pozostaje w jednym najemcy Scoped Scoped
Użytkownik może przełączać najemców Scoped Transient

Wartość domyślna Singleton nadal ma sens, jeśli baza danych nie korzysta z zależności o zakresie użytkownika.

Uwagi dotyczące wydajności

Program EF Core został zaprojektowany tak, aby DbContext wystąpienia mogły być tworzone szybko z jak najmniejszym obciążeniem. Z tego powodu utworzenie nowego DbContext dla każdej operacji powinno być zwykle w porządku. Jeśli takie podejście ma wpływ na wydajność aplikacji, rozważ użycie puli DbContext.

Podsumowanie

To są praktyczne wytyczne dotyczące implementowania wielodostępności w aplikacjach EF Core. Jeśli masz więcej przykładów lub scenariuszy lub chcesz przekazać opinię, otwórz problem i odwołaj się do tego dokumentu.