Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
I filtri di query globali consentono di associare un filtro a un tipo di entità e di applicare tale filtro ogni volta che viene eseguita una query su tale tipo di entità; considerarli come un operatore LINQ Where aggiuntivo che viene aggiunto ogni volta che viene eseguita una query sul tipo di entità. Tali filtri sono utili in un'ampia gamma di casi.
Suggerimento
È possibile visualizzare l'esempio di questo articolo in GitHub.
Esempio di base : eliminazione temporanea
In alcuni scenari, anziché eliminare una riga dal database, è preferibile impostare un IsDeleted flag per contrassegnare la riga come eliminata. Questo modello è denominato eliminazione temporanea. L'eliminazione temporanea consente di annullare l'eliminazione delle righe se necessario o di mantenere un audit trail in cui le righe eliminate sono ancora accessibili. I filtri di query globali possono essere usati per filtrare le righe eliminate temporaneamente per impostazione predefinita, consentendo comunque di accedervi in posizioni specifiche disabilitando il filtro per una query specifica.
Per abilitare l'eliminazione soft, aggiungiamo una IsDeleted proprietà al tipo Blog:
public class Blog
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string Name { get; set; }
}
È ora stato configurato un filtro di query globale usando l'API HasQueryFilter in OnModelCreating:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
È ora possibile eseguire query Blog sulle entità come di consueto. Il filtro configurato garantirà che tutte le query, per impostazione predefinita, filtrano tutte le istanze in cui IsDeleted è true.
Si noti che a questo punto, è necessario impostare IsDeleted manualmente per eliminare temporaneamente un'entità. Per una soluzione più end-to-end, è possibile eseguire l'override del metodo del tipo di contesto SaveChangesAsync per aggiungere logica che si applica a tutte le entità eliminate dall'utente e le contrassegna come modificate, impostando la proprietà IsDeleted su true.
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ChangeTracker.DetectChanges();
foreach (var item in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
{
item.State = EntityState.Modified;
item.CurrentValues["IsDeleted"] = true;
}
return await base.SaveChangesAsync(cancellationToken);
}
In questo modo è possibile usare le API di Entity Framework che eliminano un'istanza di entità come di consueto e che vengano eliminate in modo predefinito.
Uso dei dati di contesto - multi-tenancy
Un altro scenario mainstream per i filtri di query globali è multi-tenancy, in cui l'applicazione archivia i dati appartenenti a utenti diversi nella stessa tabella. In questi casi, in genere è presente una colonna ID tenant che associa la riga a un tenant specifico e i filtri di query globali possono essere usati per filtrare automaticamente le righe del tenant corrente. Ciò garantisce un forte isolamento del tenant per le query per impostazione predefinita, eliminando la necessità di pensare al filtraggio del tenant in ciascuna query.
A differenza dell'eliminazione temporanea, la multi-tenancy richiede conoscere l'ID tenant corrente; questo valore viene in genere determinato, ad esempio, quando l'utente esegue l'autenticazione sul Web. Ai fini di Entity Framework, l'ID tenant deve essere disponibile nell'istanza di contesto, in modo che il filtro di query globale possa farvi riferimento e usarlo durante l'esecuzione di query. Accettiamo un tenantId parametro nel costruttore del nostro tipo di contesto, e facciamo riferimento a esso nel nostro filtro.
public class MultitenancyContext(string tenantId) : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
}
}
In questo modo chiunque costruisce un contesto per specificare l'ID tenant associato e garantisce che solo Blog le entità con tale ID vengano restituite dalle query per impostazione predefinita.
Annotazioni
In questo esempio sono stati illustrati solo i concetti di base relativi al multi-tenancy necessari per illustrare i filtri di query globali. Per altre informazioni su multi-tenancy ed EF, vedere multi-tenancy nelle applicazioni di EF Core.
Uso di più filtri di query
La chiamata HasQueryFilter con un filtro semplice sovrascrive qualsiasi filtro precedente, quindi non è possibile definire più filtri sullo stesso tipo di entità in questo modo:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
Annotazioni
Questa funzionalità viene introdotta in EF Core 10.0 (in anteprima).
Per definire più filtri di query sullo stesso tipo di entità, devono essere denominati:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
.HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);
In questo modo è possibile gestire ogni filtro separatamente, inclusa la disabilitazione selettiva di una, ma non l'altra.
Disabilitazione dei filtri
I filtri possono essere disabilitati per singole query LINQ usando l'operatore IgnoreQueryFilters :
var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();
Se sono configurati più filtri denominati, vengono disabilitati tutti. Per disabilitare in modo selettivo filtri specifici (a partire da EF 10), passare l'elenco dei nomi dei filtri da disabilitare:
var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();
Filtri di query e navigazioni necessarie
Attenzione
L'uso della navigazione necessaria per accedere all'entità con un filtro di query globale definito può causare risultati imprevisti.
Gli spostamenti necessari in Entity Framework implicano che l'entità correlata sia sempre presente. Poiché gli inner join possono essere usati per recuperare le entità correlate, se un'entità correlata richiesta viene esclusa dal filtro di query, l'entità padre potrebbe essere filtrata anch'essa. Ciò può comportare il recupero imprevisto di un numero inferiore di elementi del previsto.
Per illustrare il problema, è possibile usare Blog ed Post entità e configurarle come indicato di seguito:
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Il seeding del modello può essere eseguito con i dati seguenti:
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/fish",
Posts =
[
new() { Title = "Fish care 101" },
new() { Title = "Caring for tropical fish" },
new() { Title = "Types of ornamental fish" }
]
});
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/cats",
Posts =
[
new() { Title = "Cat care 101" },
new() { Title = "Caring for tropical cats" },
new() { Title = "Types of ornamental cats" }
]
});
Il problema può essere osservato durante l'esecuzione delle due query seguenti:
var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();
Con la configurazione precedente, la prima query restituisce tutte e 6 Post le istanze, ma la seconda restituisce solo 3. Questa mancata corrispondenza si verifica perché il Include metodo nella seconda query carica le entità correlate Blog . Poiché lo spostamento tra Blog e Post è obbligatorio, EF Core usa INNER JOIN per la creazione della query:
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
FROM [Posts] AS [p]
INNER JOIN (
SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE [b].[Url] LIKE N'%fish%'
) AS [t] ON [p].[BlogId] = [t].[BlogId]
L'uso del INNER JOIN filtra tutte le Post righe le cui Blog righe correlate sono state eliminate da un filtro di query. Questo problema può essere risolto configurando la navigazione come facoltativa anziché obbligatoria, causando la generazione di un LEFT JOIN da parte di EF anziché di un INNER JOIN.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Un approccio alternativo consiste nel specificare filtri coerenti su entrambi i tipi di entità Blog e Post; una volta applicati i filtri corrispondenti su entrambi Blog e Post, le righe Post che potrebbero terminare in uno stato imprevisto vengono rimosse e entrambe le query restituiscono 3 risultati.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
Filtri di query e IEntityTypeConfiguration
Se il filtro di query deve accedere a un ID tenant o a informazioni contestuali simili, IEntityTypeConfiguration<TEntity> può rappresentare un'ulteriore complicazione poiché, a differenza di OnModelCreating, non esiste un'istanza disponibile del tipo di contesto a cui fare riferimento direttamente dal filtro di query. Come soluzione alternativa, aggiungere un contesto fittizio al tipo di configurazione e fare riferimento ad esso come segue:
private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
private readonly SomeDbContext _context = null!;
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
}
}
Limitazioni
I filtri di query globali presentano le limitazioni seguenti:
- I filtri possono essere definiti solo per il tipo di entità radice di una gerarchia di ereditarietà.
- Attualmente EF Core non rileva cicli nelle definizioni di filtro di query globali, quindi è consigliabile prestare attenzione quando vengono definiti. Se specificato in modo non corretto, i cicli potrebbero causare cicli infiniti durante la conversione delle query.