Centralizzare la gestione delle entity multitenant in Entity Framework Core

di Stefano Mostarda, in LINQ, Entity Framework,

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

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi