Multi-tenancy
Molte applicazioni line-of-business sono progettate per lavorare con più clienti. È importante proteggere i dati in modo che i dati dei clienti non vengano "trapelati" o visti da altri clienti e potenziali concorrenti. Queste applicazioni vengono classificate come "multi-tenant" perché ogni cliente viene considerato un tenant dell'applicazione con il proprio set di dati.
Importante
Questo documento fornisce esempi e soluzioni "così come sono". Questi non sono destinati a essere "procedure consigliate", ma piuttosto "procedure di lavoro" per la vostra considerazione.
Suggerimento
È possibile visualizzare il codice sorgente per questo esempio in GitHub
Supporto di multi-tenancy
Esistono molti approcci per implementare la multi-tenancy nelle applicazioni. Un approccio comune (che a volte è un requisito) consiste nel mantenere i dati per ogni cliente in un database separato. Lo schema è lo stesso, ma i dati sono specifici del cliente. Un altro approccio consiste nel partizionare i dati in un database esistente da parte del cliente. A tale scopo, è possibile usare una colonna in una tabella o avere una tabella in più schemi con uno schema per ogni tenant.
Approccio | Colonna per tenant? | Schema per Tenant? | Più database? | Supporto di EF Core |
---|---|---|---|---|
Discriminare (colonna) | Sì | No | No | Filtro query globale |
Database per tenant | No | No | Sì | Configurazione |
Schema per tenant | No | Sì | No | Non supportato |
Per l'approccio basato su database per tenant, il passaggio al database corretto è semplice quanto fornire il stringa di connessione corretto. Quando i dati vengono archiviati in un singolo database, è possibile usare un filtro di query globale per filtrare automaticamente le righe in base alla colonna ID tenant, assicurandosi che gli sviluppatori non scrivano accidentalmente codice in grado di accedere ai dati da altri clienti.
Questi esempi dovrebbero funzionare correttamente nella maggior parte dei modelli di app, tra cui console, WPF, WinForms e ASP.NET app Core. Le app Blazor Server richiedono considerazioni speciali.
App Blazor Server e vita della factory
Il modello consigliato per l'uso di Entity Framework Core nelle app Blazor consiste nel registrare DbContextFactory, quindi chiamarlo per creare una nuova istanza di DbContext
ogni operazione. Per impostazione predefinita, la factory è un singleton , quindi esiste una sola copia per tutti gli utenti dell'applicazione. Questa operazione è in genere corretto perché, anche se la factory è condivisa, le singole DbContext
istanze non sono.
Per il multi-tenancy, tuttavia, il stringa di connessione può cambiare per utente. Poiché la factory memorizza nella cache la configurazione con la stessa durata, significa che tutti gli utenti devono condividere la stessa configurazione. Pertanto, la durata deve essere modificata in Scoped
.
Questo problema non si verifica nelle app Blazor WebAssembly perché l'ambito del singleton è l'utente. Le app Blazor Server, d'altra parte, presentano una sfida unica. Anche se l'app è un'app Web, viene "mantenuta attiva" dalla comunicazione in tempo reale tramite SignalR. Viene creata una sessione per utente e dura oltre la richiesta iniziale. Per consentire nuove impostazioni, è necessario specificare una nuova factory per ogni utente. La durata di questa factory speciale è con ambito e viene creata una nuova istanza per sessione utente.
Soluzione di esempio (database singolo)
Una possibile soluzione consiste nel creare un semplice ITenantService
servizio che gestisce l'impostazione del tenant corrente dell'utente. Fornisce callback in modo che il codice venga informato quando il tenant cambia. L'implementazione (con i callback omessi per maggiore chiarezza) potrebbe essere simile alla seguente:
namespace Common
{
public interface ITenantService
{
string Tenant { get; }
void SetTenant(string tenant);
string[] GetTenants();
event TenantChangedEventHandler OnTenantChanged;
}
}
DbContext
può quindi gestire il multi-tenancy. L'approccio dipende dalla strategia del database. Se si archiviano tutti i tenant in un singolo database, è probabile che si usi un filtro di query. L'oggetto ITenantService
viene passato al costruttore tramite inserimento delle dipendenze e usato per risolvere e archiviare l'identificatore del tenant.
public ContactContext(
DbContextOptions<ContactContext> opts,
ITenantService service)
: base(opts) => _tenant = service.Tenant;
Il OnModelCreating
metodo viene sottoposto a override per specificare il filtro di query:
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<MultitenantContact>()
.HasQueryFilter(mt => mt.Tenant == _tenant);
In questo modo ogni query viene filtrata in base al tenant in ogni richiesta. Non è necessario filtrare il codice dell'applicazione perché il filtro globale verrà applicato automaticamente.
Il provider tenant e DbContextFactory
sono configurati nell'avvio dell'applicazione come illustrato di seguito, usando Sqlite come esempio:
builder.Services.AddDbContextFactory<ContactContext>(
opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);
Si noti che la durata del servizio è configurata con ServiceLifetime.Scoped
. Ciò consente di accettare una dipendenza dal provider di tenant.
Nota
Le dipendenze devono sempre fluire verso il singleton. Ciò significa che un Scoped
servizio può dipendere da un altro Scoped
servizio o da un Singleton
servizio, ma un Singleton
servizio può dipendere solo da altri Singleton
servizi: Transient => Scoped => Singleton
.
Più schemi
Avviso
Questo scenario non è supportato direttamente da EF Core e non è una soluzione consigliata.
In un approccio diverso, lo stesso database può gestire tenant1
e tenant2
usando schemi di tabella.
- Tenant1 -
tenant1.CustomerData
- Tenant2 -
tenant2.CustomerData
Se EF Core non usa EF Core per gestire gli aggiornamenti del database con le migrazioni e dispone già di tabelle con più schemi, è possibile eseguire l'override dello schema in un in come DbContext
OnModelCreating
in questo (lo schema per la tabella CustomerData
è impostato sul tenant):
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);
Più database e stringa di connessione
La versione di più database viene implementata passando un stringa di connessione diverso per ogni tenant. Questa operazione può essere configurata all'avvio risolvendo il provider di servizi e usandolo per compilare il stringa di connessione. Al file di configurazione viene aggiunta appsettings.json
una sezione stringa di connessione per tenant.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"TenantA": "Data Source=tenantacontacts.sqlite",
"TenantB": "Data Source=tenantbcontacts.sqlite"
},
"AllowedHosts": "*"
}
Il servizio e la configurazione vengono entrambi inseriti in DbContext
:
public ContactContext(
DbContextOptions<ContactContext> opts,
IConfiguration config,
ITenantService service)
: base(opts)
{
_tenantService = service;
_configuration = config;
}
Il tenant viene quindi usato per cercare il stringa di connessione in OnConfiguring
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenant = _tenantService.Tenant;
var connectionStr = _configuration.GetConnectionString(tenant);
optionsBuilder.UseSqlite(connectionStr);
}
Questa operazione funziona correttamente per la maggior parte degli scenari, a meno che l'utente non possa cambiare tenant durante la stessa sessione.
Cambio di tenant
Nella configurazione precedente per più database, le opzioni vengono memorizzate nella cache a Scoped
livello di . Ciò significa che se l'utente modifica il tenant, le opzioni non vengono rivalutate e quindi la modifica del tenant non viene riflessa nelle query.
La soluzione più semplice quando il tenant può cambiare consiste nell'impostare la durata su Transient.
Questo garantisce che il tenant venga rivalutato insieme al stringa di connessione ogni volta che viene richiesto un oggetto DbContext
. L'utente può cambiare tenant con la frequenza desiderata. La tabella seguente consente di scegliere la durata più appropriata per la factory.
Scenario | Database singolo | Multiple databases (Più database) |
---|---|---|
L'utente rimane in un singolo tenant | Scoped |
Scoped |
L'utente può cambiare tenant | Scoped |
Transient |
L'impostazione predefinita di Singleton
ha comunque senso se il database non assume dipendenze con ambito utente.
Note sulle prestazioni
EF Core è stato progettato in modo che DbContext
le istanze possano essere create rapidamente con il minor sovraccarico possibile. Per questo motivo, la creazione di un nuovo DbContext
per operazione dovrebbe in genere essere corretto. Se questo approccio influisce sulle prestazioni dell'applicazione, è consigliabile usare il pooling DbContext.
Conclusione
Questo è il materiale sussidiario funzionante per l'implementazione di multi-tenancy nelle app EF Core. Se si hanno altri esempi o scenari o si desidera fornire commenti e suggerimenti, aprire un problema e fare riferimento a questo documento.