Sto lavorando in .Net Core 2.1, creando un'applicazione che usa la multitenancy. Sto applicando i filtri predefiniti al mio contesto. Tuttavia, Entity Framework non sta sfruttando correttamente le query parametrizzate.
Ho passato al mio contesto delle opzioni di configurazione per applicare vincoli che sembrano così:
public class ContextAuthorizationOptions : DbAuthorizationOptions<AstootContext>
{
protected IUserAuthenticationManager _userManager;
protected int _userId => this._userManager.GetUserId();
public ContextAuthorizationOptions(IUserAuthenticationManager authenticationManager, IValidatorProvider validatorProvider)
: base(validatorProvider)
{
this._userManager = authenticationManager;
ConstraintOptions.SetConstraint<Message>(x => x.Conversation.ConversationSubscriptions
.Select(cs => cs.UserId)
.Any(userId => userId == this._userId));
}
}
Come puoi vedere la mia query utilizza una proprietà per memorizzare il valore userId. Il mio contesto accetta le opzioni di vincoli e applica loro OnModels creando in questo modo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var constraintOptions = this._authorizationOptions.ConstraintOptions;
constraintOptions.ApplyStaticConstraint(modelBuilder);
base.OnModelCreating(modelBuilder);
}
Le opzioni del mio modello sembrano così:
protected List<Action<ModelBuilder>> _constraints = new List<Action<ModelBuilder>>();
public void SetConstraint<T>(Expression<Func<T, bool>> constraint)
where T: class
{
this._constraints.Add(m => m.Entity<T>().HasQueryFilter(constraint));
}
public void ApplyStaticConstraint(ModelBuilder modelBuilder)
{
foreach(var applyConstraint in this._constraints)
{
applyConstraint(modelBuilder);
}
}
Poiché i miei filtri utilizzano le proprietà, mi aspetterei che questo generi una query con parametri, ma quando si esegue il dumping sulla tabella dei messaggi per elencarlo genera questo SQL
SELECT [x].[Id], [x].[ConversationId], [x].[Created], [x].[MessageText], [x].[SenderUserId]
FROM [Messages] AS [x]
INNER JOIN [Conversations] AS [x.Conversation] ON [x].[ConversationId] = [x.Conversation].[Id]
WHERE EXISTS (
SELECT 1
FROM [ConversationSubscriptions] AS [cs]
WHERE ([cs].[UserId] = 2005) AND ([x.Conversation].[Id] = [cs].[ConversationId]))
Come posso modificare la mia implementazione in modo che Entity Framework Core possa sfruttare il caching delle query?
Per qualche motivo che solo i progettisti di EF Core possono spiegare, le espressioni del filtro di query sono trattate in modo diverso rispetto alle altre espressioni di query. In particolare, tutte le variabili che non sono radicate nel contesto db di destinazione vengono valutate e convertite in costanti. Il termine radicato si è evoluto da semplice campo diretto / proprietà del contesto a regole più rilassate spiegate in # 10301: Query: QueryFilter con EntityTypeConfiguration non riesce ad iniettare valori di contesto correnti Progettare note di riunione :
Pattern di configurazione che catturano correttamente il contesto e iniettano valori di istanza correnti
- Definizione del filtro in
OnModelCreating
- Definizione del filtro in
EntityTypeConfiguration
passando il contesto attraverso il costruttore- Definizione del filtro mediante il metodo (interno / esterno
DbContext
o metodo di estensione) in cui il contesto viene passato come parametro. Qualsiasi di sopra in cui il contesto è racchiuso all'interno di un altro tipo di oggetto e quel tipo viene passato in giro.Oltre a sopra, parametrizzeremo qualsiasi tipo di chiamata su
DbContext
esempio accesso a proprietà / campo, chiamata di metodo, passando attraverso più livelli.
Il punto 3 ( "Definizione del filtro usando il metodo (interno / esterno DbContext
o metodo di estensione) in cui il contesto è passato come parametro." ) Mi porta a una soluzione generica relativamente semplice.
Aggiungi la seguente classe semplice:
public static class Filter
{
public static T Variable<T>(this DbContext context, T value) => value;
}
Modifica la tua classe di opzioni in questo modo:
protected List<Action<ModelBuilder, DbContext>> _constraints = new List<Action<ModelBuilder, DbContext>>();
public void SetConstraint<T>(Func<DbContext, Expression<Func<T, bool>>> constraint)
where T : class
{
this._constraints.Add((mb, c) => mb.Entity<T>().HasQueryFilter(constraint(c)));
}
public void ApplyStaticConstraint(ModelBuilder modelBuilder, DbContext context)
{
foreach (var applyConstraint in this._constraints)
{
applyConstraint(modelBuilder, context);
}
}
la chiamata SetConstraint
modo (nota il wrapping this._userId
nella chiamata al metodo Variable
):
ConstraintOptions.SetConstraint<Message>(c => x => x.Conversation.ConversationSubscriptions
.Select(cs => cs.UserId)
.Any(userId => userId == c.Variable(this._userId)));
e infine la chiamata ApplyStaticConstraint
:
constraintOptions.ApplyStaticConstraint(modelBuilder, this);
Ora la query utilizzerà il parametro anziché un valore costante.