Globale Abfragefilter

Globale Abfragefilter sind LINQ-Abfrageprädikate, die auf Entitätstypen im Metadatenmodell (normalerweise in OnModelCreating) angewendet werden. Bei einem Abfrageprädikat handelt es sich um einen booleschen Ausdruck, der in der Regel an den LINQ-Abfrageoperator Whereübergeben wird. EF Core wendet solche Filter automatisch auf alle LINQ-Abfragen an, die diese Entitätstypen einschließen. EF Core wendet sie auch auf Entitätstypen an, auf die indirekt durch Verwendung der Include-Eigenschaft oder Navigationseigenschaft verwiesen wird. Zu den häufigsten Anwendungsfällen dieses Features zählen Folgende:

  • Vorläufiges Löschen: Ein Entitätstyp definiert eine IsDeleted-Eigenschaft.
  • Mehrinstanzenfähigkeit: Ein Entitätstyp definiert eine TenantId-Eigenschaft.

Beispiel

Im folgenden Beispiel wird in einem einfachen Blogmodell dargestellt, wie globale Abfragefilter zum Implementieren des Abfrageverhaltens für das vorläufige Löschen und die Mehrinstanzenfähigkeit verwendet werden.

Tipp

Das in diesem Artikel verwendete Beispiel finden Sie auf GitHub.

Hinweis

Mehrinstanzenfähigkeit wird hier als einfaches Beispiel verwendet. Es gibt auch einen Artikel mit umfassenden Anleitungen zur Mehrinstanzenfähigkeit in EF Core-Anwendungen.

Definieren Sie zunächst die Entitäten:

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; }
}

Beachten Sie die Deklaration eines _tenantId-Felds in der Blog-Entität. Dieses Feld wird dazu verwendet, jede Bloginstanz einem bestimmten Mandanten zuzuordnen. Außerdem wird eine IsDeleted-Eigenschaft auf dem Post-Entitätstyp definiert. Mit dieser Eigenschaft wird nachverfolgt, ob eine Post-Instanz „vorläufig gelöscht“ wurde. Das heißt, die Instanz wird als gelöscht gekennzeichnet, ohne dass zugrunde liegende Daten physisch entfernt werden.

Konfigurieren Sie als nächstes die Abfragefilter in OnModelCreating mithilfe der HasQueryFilter-API.

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

Die Prädikatausdrücke, die an HasQueryFilter-Aufrufe weitergegeben werden, werden nun automatisch auf alle LINQ-Abfragen dieser Typen angewendet.

Tipp

Beachten Sie die Verwendung eines DbContext-Instanzfelds: _tenantId wird zum Festlegen des aktuellen Mandanten verwendet. Filter auf Modellebene verwenden den Wert der korrekten Kontextinstanz, d.h. der Instanz, die die Abfrage ausführt.

Hinweis

Es ist derzeit nicht möglich, mehrere Abfragefilter für dieselbe Entität zu definieren, nur der letzte wird angewendet. Mithilfe des logischen AND-Operators (&& in C#) können jedoch einen einzelnen Filter mit mehreren Bedingungen definieren.

Verwenden von Navigationselementen

Navigationselemente können beim Definieren lokaler Abfragefilter verwendet werden. Das Verwenden von Navigationselementen in Abfragefiltern führt dazu, dass Abfragefilter rekursiv angewendet werden. Wenn EF Core die in Navigationselementen verwendeten Abfragefilter erweitert, werden auch die für die Entitäten, auf die verwiesen wird, definierten Abfragefilter angewendet.

Um dies zu veranschaulichen, konfigurieren Sie Abfragefilter in OnModelCreating wie folgt:

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"));

Als Nächstes fragen Sie alle Blog-Entitäten ab:

var filteredBlogs = db.Blogs.ToList();

Diese Abfrage erzeugt den folgenden SQL-Code, der Abfragefilter anwendet, die für die Entitäten Blog und Post definiert sind:

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

Hinweis

Derzeit ermittelt EF Core keine Zyklen in globalen Abfragefilterdefinitionen. Daher sollten Sie bei der Definition vorsichtig vorgehen. Wenn diese fehlerhaft angegeben wird, können Endlosschleifen bei der Abfrageübersetzung auftreten.

Zugreifen auf Entitäten mit Abfragefilter mithilfe erforderlicher Navigationselemente

Achtung

Die Verwendung erforderlicher Navigationselemente für den Zugriff auf eine Entität mit einem definierten globalen Abfragefilter kann zu unerwarteten Ergebnissen führen.

Die erforderlichen Navigationselemente erwarten, dass die zugehörige Entität immer vorhanden ist. Wenn erforderliche verwandte Entitäten vom Abfragefilter herausgefiltert werden, würde die übergeordnete Entität ebenso nicht im Ergebnis ausgegeben werden. Daher erhalten Sie möglicherweise weniger Elemente als erwartet.

Zur Veranschaulichung dieses Problems können die oben angegebenen Entitäten Blog und Post mit der OnModelCreating-Methode verwendet werden:

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

Das Modell kann mit den folgenden Daten eingerichtet werden:

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" }
        }
    });

Das Problem tritt auf, wenn zwei Abfragen ausgeführt werden:

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

Bei diesem Setup gibt die erste Abfrage alle sechs Post-Anfragen zurück. Die zweite Abfrage gibt jedoch nur drei zurück. Das liegt daran, dass die Include-Methode in der zweiten Abfrage die zugehörigen Blog-Entitäten lädt. Das die Navigation zwischen Blog und Post erforderlich ist, nutzt EF Core INNER JOIN beim Erstellen der Abfrage:

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]

Bei Verwendung von INNER JOIN werden alle Post-Vorgänge herausgefiltert, deren zugehörigen Blog-Entitäten von einem globalen Abfragefilter entfernt wurden.

Dieses Problem können Sie mithilfe optionaler Navigationselemente anstelle erforderlicher umgehen. Auf diese Weise bleibt die erste Abfrage unverändert, die zweite Abfrage generiert nun jedoch LEFT JOIN und gibt sechs Ergebnisse zurück.

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

Der alternative Ansatz besteht darin, konsistente Filter für die beiden Entitäten Blog und Post festzulegen. Auf diese Weise werden entsprechende Filter auf Blog und Post angewendet. Post-Entitäten, die einen unerwarteten Status aufweisen könnten, werden entfernt und beide Abfragen geben drei Ergebnisse zurück.

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"));

Deaktivieren von Filtern

Filter können für einzelne LINQ-Abfragen mit dem IgnoreQueryFilters-Operator deaktiviert werden.

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

Begrenzungen

Globale Abfragefilter unterliegen den folgenden Einschränkungen:

  • Filter können nur für den Stammentitätstyp einer Vererbungshierarchie definiert werden.