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.