Quando dobbiamo sviluppare applicazioni multitenant, una delle cose fondamentali è assicurarci che un dato inserito da un utente di quel tenant, sia effettivamente inserito per il tenant e successivamente visibile solo agli utenti di quel tenant. Questo requisito è talmente importante da non poter essere delegato a chi sviluppa le singole funzioni, ma va accentrato in un unico punto così da non ammettere possibilità di errore. Per centralizzare questo requisito ci basta agire sul contesto dell'applicazione. I passi da eseguire per gestire questa funzionalità non sono molti e li vediamo di seguito.
Per prima cosa, creiamo un'interfaccia che contiene il TenantId e che le entity devono implementare.
public interface IMultiTenantEntity { int TenantId { get; set; } } public class Person : IMultiTenantEntity { public int TenantId { get; set; } }
Il secondo passaggio consiste nel passare al costruttore del contesto il principal dell'utente corrente. Il contesto memorizza quest'utente e lo utilizza per le fasi di scrittura e lettura dati.
public class MyDbContext : DbContext { public DbSet<Person> People { get; set; } private ClaimsPrincipal _principal; public MultitenantDbContext(DbContextOptions<MultitenantDbContext> options, ClaimsPrincipal principal) : base(options) { _principal = principal; } }
A questo punto dobbiamo innanzitutto assicurarci che in fase di scrittura la proprietà TenantId sia correttamente popolata con quello dell'utente corrente. A questo scopo eseguiamo l'override del metodo SaveChanges e per ogni entity in stato di Added che implementa IMultiTenantEntity impostiamo la proprietà TenantId con il tenant dell'utente corrente (recuperato dai claim principal).
Per le entity in stato di Modified o Deleted solleviamo invece un'eccezione se il tenantId è diverso da quello dell'utente.
public override int SaveChanges(bool acceptAllChangesOnSuccess) { ChangeTracker.Entries<IMultiTenantEntity>() .Where(e => e.State == EntityState.Added) .ToList() .ForEach(entry => entry.Entity.TenantId = _principal.GetTenantId()); var any = ChangeTracker.Entries<IMultiTenantEntity>() .Any(e => (e.State == EntityState.Modified || e.State == EntityState.Deleted) && e.Entity.TenantId != _principal.GetTenantId() ); if (any) { throw new InvalidOperationException("Invalid tenant"); } }
Per quanto riguarda la fase di lettura, possiamo ricorrere a un filtro globale impostato in fase di mapping.
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Person>() .HasQueryFilter(p => p.TenantId == _principal.GetTenantId()); }
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Routing statico e PreRendering in una Blazor Web App
Eseguire query verso tipi non mappati in Entity Framework Core
Usare lo spread operator con i collection initializer in C#
Eseguire query manipolando liste di tipi semplici con Entity Framework Core
Utilizzare Tailwind CSS all'interno di React: primi componenti
Sviluppare un'interfaccia utente in React con Tailwind CSS e Preline UI
Eseguire attività basate su eventi con Azure Container Jobs
Cancellare una run di un workflow di GitHub
Configurare il nome della run di un workflow di GitHub in base al contesto di esecuzione
Creare una custom property in GitHub
Evitare la script injection nelle GitHub Actions
.NET Conference Italia 2023