Filtri di query globali

I filtri di query globali sono predicati di query LINQ applicati ai tipi di entità nel modello di metadati (in genere in OnModelCreating). Un predicato di query è un'espressione booleana in genere passata all'operatore di query LINQ Where . EF Core applica questi filtri automaticamente a qualsiasi query LINQ che coinvolge tali tipi di entità. EF Core le applica anche ai tipi di entità, a cui viene fatto riferimento indirettamente tramite l'uso della proprietà Di inclusione o navigazione. Alcune applicazioni comuni di questa funzionalità sono:

  • Eliminazione temporanea: un tipo di entità definisce una IsDeleted proprietà.
  • Multi-tenancy : un tipo di entità definisce una TenantId proprietà.

Esempio

Nell'esempio seguente viene illustrato come usare i filtri di query globali per implementare comportamenti di query multi-tenancy ed eliminazione temporanea in un modello di blogging semplice.

Suggerimento

È possibile visualizzare l'esempio di questo articolo in GitHub.

Nota

Il multi-tenancy viene usato qui come semplice esempio. È disponibile anche un articolo con linee guida complete per la multi-tenancy nelle applicazioni EF Core.

Prima di tutto, definire le entità:

public class Blog
{
#pragma warning disable IDE0051, CS0169 // Remove unused private members
    private string _tenantId;
#pragma warning restore IDE0051, CS0169 // Remove unused private members

    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public bool IsDeleted { get; set; }

    public Blog Blog { get; set; }
}

Si noti la dichiarazione di un _tenantId campo nell'entità Blog . Questo campo verrà usato per associare ogni istanza del blog a un tenant specifico. Definita anche è una IsDeleted proprietà sul Post tipo di entità. Questa proprietà viene utilizzata per tenere traccia dell'eventuale eliminazione temporanea di un'istanza post. L'istanza è contrassegnata come eliminata senza rimuovere fisicamente i dati sottostanti.

Configurare quindi i filtri di query in OnModelCreating usando l'API HasQueryFilter .

modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);

Le espressioni di predicato passate alle HasQueryFilter chiamate verranno ora applicate automaticamente a qualsiasi query LINQ per tali tipi.

Suggerimento

Si noti l'uso del campo a livello di istanza di DbContext _tenantId, usato per impostare il tenant corrente. I filtri a livello di modello useranno il valore dell'istanza del contesto corretta, ovvero l'istanza che esegue la query.

Nota

Non è attualmente possibile definire più filtri di query sulla stessa entità. Verrà applicato solo l'ultimo. Tuttavia, è possibile definire un singolo filtro con più condizioni usando l'operatore logico AND (&& in C#).

Uso degli spostamenti

È anche possibile usare gli spostamenti nella definizione di filtri di query globali. L'uso degli spostamenti nel filtro di query causerà l'applicazione ricorsiva dei filtri di query. Quando EF Core espande gli spostamenti usati nei filtri di query, applicherà anche i filtri di query definiti nelle entità a cui si fa riferimento.

Per illustrare questa configurazione dei filtri di query nel OnModelCreating modo seguente:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));

Eseguire quindi una query per tutte le Blog entità:

var filteredBlogs = db.Blogs.ToList();

Questa query produce il codice SQL seguente, che applica i filtri di query definiti per entrambe Blog le entità e Post :

SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE (
    SELECT COUNT(*)
    FROM [Posts] AS [p]
    WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0

Nota

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.

Accesso all'entità con il filtro di query usando la navigazione richiesta

Attenzione

L'uso della navigazione necessaria per accedere all'entità con un filtro di query globale definito può causare risultati imprevisti.

La navigazione richiesta prevede che l'entità correlata sia sempre presente. Se necessario, l'entità correlata viene filtrata in base al filtro di query, l'entità padre non sarà neanche risultante. È quindi possibile ottenere meno elementi del previsto nel risultato.

Per illustrare il problema, è possibile usare le Blog entità e Post specificate in precedenza e il metodo seguente OnModelCreating :

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 List<Post>
        {
            new Post { Title = "Fish care 101" },
            new Post { Title = "Caring for tropical fish" },
            new Post { Title = "Types of ornamental fish" }
        }
    });

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/cats",
        Posts = new List<Post>
        {
            new Post { Title = "Cat care 101" },
            new Post { Title = "Caring for tropical cats" },
            new Post { Title = "Types of ornamental cats" }
        }
    });

Il problema può essere osservato durante l'esecuzione di due query:

var allPosts = db.Posts.ToList();
var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList();

Con la configurazione precedente, la prima query restituisce tutti i 6 Posts, ma la seconda query restituisce solo 3. Questa mancata corrispondenza si verifica perché Include il 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]

Uso dei filtri di INNER JOIN tutti i Posti cui elementi correlati Blogsono stati rimossi da un filtro di query globale.

Può essere risolto usando la navigazione facoltativa anziché obbligatoria. In questo modo la prima query rimane invariata come prima, tuttavia la seconda query genera LEFT JOIN e restituisce 6 risultati.

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

L'approccio alternativo consiste nel specificare filtri coerenti per entrambe Blog le entità e Post . In questo modo i filtri corrispondenti vengono applicati sia a che Posta Blog . Postche potrebbero terminare in uno stato imprevisto vengono rimossi 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"));

Disabilitazione dei filtri

È possibile disabilitare i filtri per singole query LINQ usando l'operatore IgnoreQueryFilters.

blogs = db.Blogs
    .Include(b => b.Posts)
    .IgnoreQueryFilters()
    .ToList();

Limiti

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à.