Sto creando un'applicazione multi-tenant e sto incontrando difficoltà con quello che penso sia l'EF Core che memorizza nella cache l'ID tenant attraverso le richieste. L'unica cosa che sembra aiutare è ricostruire costantemente l'applicazione mentre accedo e esco dagli inquilini.
Ho pensato che potrebbe avere qualcosa a che fare con l'istanza IHttpContextAccessor
è un singleton, ma non può essere un ambito, e quando IHttpContextAccessor
e esco senza ricostruzione posso vedere il cambio di nome del tenant nella parte superiore della pagina, quindi non è il problema.
L'unica altra cosa che posso pensare è che EF Core sta facendo una specie di cache di query. Non sono sicuro del motivo per cui dovrebbe considerare che si tratta di un'istanza di ambito e che dovrebbe essere rigenerata ad ogni richiesta, a meno che non sbagli, cosa che probabilmente sono. Speravo che si comportasse come un'istanza dell'ambito in modo da poter semplicemente iniettare l'ID tenant al momento della compilazione del modello su ogni istanza.
Lo apprezzerei davvero se qualcuno potesse indicarmi la giusta direzione. Ecco il mio codice attuale:
TenantProvider.cs
public sealed class TenantProvider :
ITenantProvider {
private readonly IHttpContextAccessor _accessor;
public TenantProvider(
IHttpContextAccessor accessor) {
_accessor = accessor;
}
public int GetId() {
return _accessor.HttpContext.User.GetTenantId();
}
}
... che viene iniettato in TenantEntityConfigurationBase.cs dove lo utilizzo per impostare un filtro di query globale.
internal abstract class TenantEntityConfigurationBase<TEntity, TKey> :
EntityConfigurationBase<TEntity, TKey>
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey> {
protected readonly ITenantProvider TenantProvider;
protected TenantEntityConfigurationBase(
string table,
string schema,
ITenantProvider tenantProvider) :
base(table, schema) {
TenantProvider = tenantProvider;
}
protected override void ConfigureFilters(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureFilters(builder);
builder.HasQueryFilter(
e => e.TenantId == TenantProvider.GetId());
}
protected override void ConfigureRelationships(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureRelationships(builder);
builder.HasOne(
t => t.Tenant).WithMany().HasForeignKey(
k => k.TenantId);
}
}
... che viene quindi ereditato da tutte le altre configurazioni di entità titolare. Sfortunatamente non sembra funzionare come avevo programmato.
Ho verificato che l'ID tenant restituito dall'entità dell'utente sta cambiando a seconda di quale utente tenant ha effettuato l'accesso, quindi non è questo il problema. Grazie in anticipo per qualsiasi aiuto!
Aggiornare
Per una soluzione quando si utilizza EF Core 2.0.1+, guardare la risposta non accettata da me.
Aggiornamento 2
Guardate anche l'aggiornamento di Ivan per 2.0.1+, proxy nell'espressione filtro da DbContext che ripristina la possibilità di definirlo una volta in una classe di configurazione di base. Entrambe le soluzioni hanno i loro pro e contro. Ho optato per Ivan di nuovo perché voglio solo sfruttare le mie configurazioni di base il più possibile.
Attualmente (a partire da EF Core 2.0.0) il filtro dinamico delle query globali è piuttosto limitato. Funziona solo se la parte dinamica è fornita dalla proprietà diretta della classe derivata DbContext
destinazione (o una delle sue classi derivate DbContext
base). Esattamente come nell'esempio dei filtri di query a livello di modello dalla documentazione. Esattamente in questo modo - nessuna chiamata al metodo, nessun accesso alla proprietà nidificata - solo proprietà del contesto. È un po 'spiegato nel link:
Si noti l'uso di un
DbContext
proprietà level esempio:TenantId
. I filtri a livello di modello utilizzeranno il valore dall'istanza di contesto corretta. cioè quello che sta eseguendo la query.
Per farlo funzionare nel tuo scenario, devi creare una classe base come questa:
public abstract class TenantDbContext : DbContext
{
protected ITenantProvider TenantProvider;
internal int TenantId => TenantProvider.GetId();
}
derivare la classe di contesto da essa e in qualche modo iniettare l'istanza TenantProvider
in essa. Quindi modificare la classe TenantEntityConfigurationBase
per ricevere TenantDbContext
:
internal abstract class TenantEntityConfigurationBase<TEntity, TKey> :
EntityConfigurationBase<TEntity, TKey>
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey> {
protected readonly TenantDbContext Context;
protected TenantEntityConfigurationBase(
string table,
string schema,
TenantDbContext context) :
base(table, schema) {
Context = context;
}
protected override void ConfigureFilters(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureFilters(builder);
builder.HasQueryFilter(
e => e.TenantId == Context.TenantId);
}
protected override void ConfigureRelationships(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureRelationships(builder);
builder.HasOne(
t => t.Tenant).WithMany().HasForeignKey(
k => k.TenantId);
}
}
e tutto funzionerà come previsto. E ricorda, il tipo di variabile di Context
deve essere una classe derivata DbContext
- la sua sostituzione con l' interfaccia non funzionerà.
Aggiornamento per 2.0.1 : Come sottolineato da @Smit nei commenti, v2.0.1 ha rimosso la maggior parte delle limitazioni - ora è possibile utilizzare metodi e proprietà secondarie.
Tuttavia, ha introdotto un altro requisito: l'espressione dinamica deve essere radicata su DbContext
.
Questo requisito interrompe la soluzione di cui sopra, poiché l'espressione root è TenantEntityConfigurationBase<TEntity, TKey>
classe TenantEntityConfigurationBase<TEntity, TKey>
e non è così facile creare tale espressione al di fuori di DbContext
causa della mancanza di supporto del tempo di compilazione per la generazione di espressioni costanti.
Potrebbe essere risolto con alcuni metodi di manipolazione delle espressioni di basso livello, ma il più semplice nel tuo caso sarebbe spostare la creazione del filtro nel metodo di istanza generico di TenantDbContext
e chiamarlo dalla classe di configurazione dell'entità.
Ecco le modifiche:
Classe TenantDbContext :
internal Expression<Func<TEntity, bool>> CreateFilter<TEntity, TKey>()
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey>
{
return e => e.TenantId == TenantId;
}
Classe TenantEntityConfigurationBase <TEntity, TKey> :
builder.HasQueryFilter(Context.CreateFilter<TEntity, TKey>());
Rispondi per 2.0.1+
Quindi, il giorno in cui ho funzionato, è stato rilasciato EF Core 2.0.1. Non appena ho aggiornato, questa soluzione si è bloccata. Dopo un lungo thread qui , si è scoperto che era davvero un caso che funzionasse in 2.0.0.
Ufficialmente per 2.0.1 e oltre qualsiasi filtro di query che dipende da un valore esterno, come l'ID tenant nel mio caso, deve essere definito nel metodo OnModelCreating
e deve fare riferimento a una proprietà su DbContext
. Il motivo è perché alla prima esecuzione dell'app o prima chiamata in EF tutte le classi EntityTypeConfiguration
vengono elaborate e i loro risultati vengono memorizzati nella cache indipendentemente dal numero di istanze in cui DbContext
viene istanziato.
Ecco perché la definizione dei filtri di query nel metodo OnModelCreating
funziona perché è una nuova istanza e il filtro vive e muore con esso.
public class MyDbContext : DbContext {
private readonly ITenantService _tenantService;
private int TenantId => TenantService.GetId();
public DbSet<User> Users { get; set; }
public MyDbContext(
DbContextOptions options,
ITenantService tenantService) {
_tenantService = tenantService;
}
protected override void OnModelCreating(
ModelBuilder modelBuilder) {
modelBuilder.Entity<User>().HasQueryFilter(
u => u.TenantId == TenantId);
}
}