Provo ad utilizzare i filtri di query globali per implementare la multi-tenancy in un'applicazione web ASP.NET Core. Al momento, ho un database separato per ogni titolare e configurare il contesto in startup.cs in questo modo:
services.AddDbContext<dbcontext>((service, options) =>
options.UseSqlServer(Configuration[$"Tenant:{service.GetService<ITenantProvider>().Current}:Database"])
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)),
contextLifetime: ServiceLifetime.Scoped, optionsLifetime: ServiceLifetime.Scoped);
Questo funziona bene. Ora il cliente non vuole più un database separato per ogni tenant, quindi ho aggiunto una teanntId
a ogni tabella e voglio sfruttare i filtri di query globali per implementarla.
Come descritto nella documentazione , posso aggiungere il filtro di query nel metodo OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
}
Ma sto usando il primo approccio del database, quindi ogni volta che genererò il modello perderò la configurazione. C'è un altro modo per configurare il filtro di query globale come l'utilizzo di DbContextOptionsBuilder
?
Sto usando EF Core 2.1.2.
Ho finito per utilizzare una classe parziale che sovrascrive il metodo OnModelCreating:
public partial class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
OnModelCreatingInternal(modelBuilder);
modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
}
}
Devo ancora modificare il codice generato (modificare il signatur OnModelCreating generato su OnModelCreatingInternal e rimuovere l'override). Ma al leaset ottengo un errore del compilatore, quindi non posso dimenticarlo.
Questa è la prima cosa che appare su Google quando si cerca questo argomento, quindi sto postando una soluzione un po 'più completa e più facile da usare che mi è venuta in mente dopo averlo provato un po'.
Volevo essere in grado di filtrare automaticamente tutte le entità generate che avevano una colonna nella tabella denominata TenantID e inserire automaticamente il TenantID dell'utente connesso durante il salvataggio.
Esempio di classe parziale:
public partial class Filtered_Db_Context : MyDbContext
{
private int _tenant;
public Filtered_Db_Context(IHttpContextAccessor context) : base()
{
_tenant = AuthenticationMethods.GetTenantId(context?.HttpContext);
}
public Filtered_Db_Context(HttpContext context) : base()
{
_tenant = AuthenticationMethods.GetTenantId(context);
}
public void AddTenantFilter<T>(ModelBuilder mb) where T : class
{
mb.Entity<T>().HasQueryFilter(t => EF.Property<int>(t, "TenantId") == _tenant);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//For any entity that has a TenantId it will only allow logged in user to see data from their own Tenant
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var prop = entityType.FindProperty("TenantId");
if (prop != null && prop.ClrType == typeof(int))
{
GetType()
.GetMethod(nameof(AddTenantFilter))
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder });
}
}
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
InsertTenantId();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override int SaveChanges()
{
InsertTenantId();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
InsertTenantId();
return base.SaveChangesAsync(cancellationToken);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
InsertTenantId();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void InsertTenantId()
{
if (_tenant != 0)
{
var insertedOrUpdated = ChangeTracker.Entries().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified).ToList();
insertedOrUpdated.ForEach(e => {
var prop = e.Property("TenantId");
int propIntVal;
bool isIntVal = int.TryParse(prop.CurrentValue.ToString(), out propIntVal);
if (prop != null && prop.Metadata.IsForeignKey() && isIntVal && propIntVal != _tenant)
{
prop.CurrentValue = _tenant;
}
});
}
}
}
Ora è possibile eseguire tutte le azioni del framework dell'entità come di consueto utilizzando la classe Filtered_Db_Context
e la funzione tenant viene gestita senza pensarci su query e salvataggio.
Basta aggiungerlo all'iniezione di dipendenza in Avvio invece del contesto generato da EF: serivces.AddDbContext<Filtered_Db_Context>()
Quando esegui il reimpalcatura, non è necessario accedere e modificare nessuna delle classi generate.