Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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.
Avviso
Questo articolo usa un database locale che non richiede l'autenticazione dell'utente. Le app di produzione devono usare il flusso di autenticazione più sicuro disponibile. Per altre informazioni sull'autenticazione per le app di test e produzione distribuite, vedere Proteggere i flussi di autenticazione.
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 inquilino? | Schema per inquilino? | Più database? | Supporto di EF Core |
---|---|---|---|---|
Discriminare (colonna) | Sì | NO | NO | Filtro query globale |
Database per inquilino | NO | NO | Sì | Impostazione |
Schema per istanza | NO | Sì | NO | Non supportato |
Per l'approccio basato su un database per singolo tenant, passare al database corretto è semplice quanto fornire la stringa di connessione corretta. 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 il ciclo di 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 corretta perché le singole istanze DbContext
non sono condivise, anche se la fabbrica lo è.
Per il multi-tenancy, tuttavia, la 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 è limitato all'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 è limitata a un ambito e viene creata una nuova istanza per ogni 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 avvisato 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 sovrascritto 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 per il tenant ad 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 non stai utilizzando EF Core per gestire gli aggiornamenti del database con le migrazioni e disponi già di tabelle con più schemi, è possibile eseguire l'override dello schema in un DbContext
in OnModelCreating
come questo (lo schema per la tabella CustomerData
è impostato sul locatario):
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'inizio risolvendo il provider di servizi e usandolo per costruire la stringa di connessione. Nel file di configurazione viene aggiunta una stringa di connessione per ogni sezione di tenant appsettings.json
.
{
"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 trovare la 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 locatario durante la stessa sessione.
Cambio di tenant
Nella configurazione precedente per più database, le opzioni vengono memorizzate nella cache a livello Scoped
. Ciò significa che se l'utente modifica il tenant, le opzioni non vengono rivalutate e quindi la modifica del tenant non si riflette 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 quale vita utile ha più senso per la fabbrica.
Scenario | Database singolo | Multiple databases (Più database) |
---|---|---|
L'utente ha una permanenza 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 relative alle 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.