Filtry zapytań globalnych

Globalne filtry zapytań to predykaty zapytań LINQ stosowane do typów jednostek w modelu metadanych (zwykle w OnModelCreatingsystemie ). Predykat zapytania jest wyrażeniem logicznym zwykle przekazywanym do operatora zapytania LINQ Where . Program EF Core automatycznie stosuje takie filtry do wszystkich zapytań LINQ obejmujących te typy jednostek. Program EF Core stosuje je również do typów jednostek, do których odwołuje się pośrednio, za pomocą właściwości Dołączanie lub nawigacja. Oto niektóre typowe zastosowania tej funkcji:

  • Usuwanie nietrwałe — typ jednostki definiuje IsDeleted właściwość.
  • Wielodostępność — typ jednostki definiuje TenantId właściwość.

Przykład

W poniższym przykładzie pokazano, jak używać globalnych filtrów zapytań do implementowania zachowań zapytań obejmujących wiele dzierżaw i usuwania nietrwałego w prostym modelu blogowania.

Napiwek

Przykład z tego artykułu można zobaczyć w witrynie GitHub.

Uwaga

Wielodostępność jest używana tutaj jako prosty przykład. Istnieje również artykuł z kompleksowymi wskazówkami dotyczącymi wielodostępności w aplikacjach EF Core.

Najpierw zdefiniuj jednostki:

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

Zanotuj deklarację _tenantId pola w jednostce Blog . To pole będzie używane do skojarzenia każdego wystąpienia blogu z określoną dzierżawą. Zdefiniowana również właściwość jest właściwością IsDeletedPost typu jednostki. Ta właściwość służy do śledzenia, czy wystąpienie post zostało "usunięte nietrwale". Oznacza to, że wystąpienie jest oznaczone jako usunięte bez fizycznego usuwania bazowych danych.

Następnie skonfiguruj filtry zapytań przy OnModelCreating użyciu interfejsu HasQueryFilter API.

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

Wyrażenia predykatu przekazane do wywołań będą teraz automatycznie stosowane do HasQueryFilter wszystkich zapytań LINQ dla tych typów.

Napiwek

Zwróć uwagę na użycie pola poziomu wystąpienia DbContext: _tenantId służy do ustawiania bieżącej dzierżawy. Filtry na poziomie modelu będą używać wartości z poprawnego wystąpienia kontekstu (czyli wystąpienia, które wykonuje zapytanie).

Uwaga

Obecnie nie można zdefiniować wielu filtrów zapytań w tej samej jednostce — zostanie zastosowany tylko ostatni. Można jednak zdefiniować jeden filtr z wieloma warunkami przy użyciu operatora logicznego AND (&& w języku C#).

Korzystanie z nawigacji

Możesz również użyć nawigacji podczas definiowania globalnych filtrów zapytań. Użycie nawigacji w filtrze zapytań spowoduje, że filtry zapytań będą stosowane rekursywnie. Gdy program EF Core rozszerza nawigacje używane w filtrach zapytań, będzie również stosować filtry zapytań zdefiniowane dla odwołanych jednostek.

Aby zilustrować te filtry zapytań OnModelCreating w następujący sposób:

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

Następnie wykonaj zapytanie dotyczące wszystkich Blog jednostek:

var filteredBlogs = db.Blogs.ToList();

To zapytanie tworzy następujący kod SQL, który stosuje filtry zapytań zdefiniowane dla jednostek Blog i 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

Uwaga

Obecnie program EF Core nie wykrywa cykli w globalnych definicjach filtrów zapytań, dlatego należy zachować ostrożność podczas ich definiowania. Jeśli określono niepoprawnie, cykle mogą prowadzić do nieskończonych pętli podczas tłumaczenia zapytań.

Uzyskiwanie dostępu do jednostki z filtrem zapytań przy użyciu wymaganej nawigacji

Uwaga

Użycie wymaganej nawigacji w celu uzyskania dostępu do jednostki, która ma zdefiniowany globalny filtr zapytań, może prowadzić do nieoczekiwanych wyników.

Wymagana nawigacja oczekuje, że powiązana jednostka będzie zawsze obecna. W razie potrzeby powiązana jednostka jest filtrowana według filtru zapytania, jednostka nadrzędna nie byłaby w wyniku. W związku z tym możesz uzyskać mniej elementów niż oczekiwano w wyniku.

Aby zilustrować problem, możemy użyć jednostek Blog i Post określonych powyżej i następującej OnModelCreating metody:

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

Model może zostać wstępnie wypełniony następującymi danymi:

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

Problem można zaobserwować podczas wykonywania dwóch zapytań:

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

W przypadku powyższej konfiguracji pierwsze zapytanie zwraca wszystkie 6 Posts, jednak drugie zapytanie zwraca tylko 3. Ta niezgodność występuje, ponieważ Include metoda w drugim zapytaniu ładuje powiązane Blog jednostki. Ponieważ nawigacja między elementami Blog i Post jest wymagana, program EF Core używa INNER JOIN podczas konstruowania zapytania:

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]

INNER JOIN Użyj filtrów dla wszystkich Postelementów, których powiązane Blogzostały usunięte przez globalny filtr zapytań.

Można go rozwiązać za pomocą opcjonalnej nawigacji zamiast wymaganej. Dzięki temu pierwsze zapytanie pozostaje takie samo jak wcześniej, jednak drugie zapytanie będzie teraz generować LEFT JOIN i zwracać 6 wyników.

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

Alternatywne podejście polega na określeniu spójnych filtrów dla jednostek Blog i Post . W ten sposób pasujące filtry są stosowane do elementów Blog i Post. Posts, które mogą skończyć się nieoczekiwanym stanem, zostaną usunięte, a oba zapytania zwracają 3 wyniki.

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

Wyłączanie filtrów

Filtry mogą być wyłączone dla poszczególnych zapytań LINQ przy użyciu IgnoreQueryFilters operatora .

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

Ograniczenia

Filtry zapytań globalnych mają następujące ograniczenia:

  • Filtry można zdefiniować tylko dla głównego typu jednostki hierarchii dziedziczenia.