Глобальные фильтры запросов

Глобальные фильтры запросов — это предикаты запросов LINQ, применяемые к типам сущностей в модели метаданных (обычно в OnModelCreating). Предикат запроса — это логическое выражение, обычно переданное оператору запроса LINQ Where . EF Core автоматически применяет такие фильтры ко всем запросам LINQ, включающим эти типы сущностей. EF Core также применяет их к типам сущностей, на которые косвенно ссылаются посредством использования Include или свойства навигации. Ниже приведены некоторые типичные способы применения этой функции.

  • Обратимое удаление. Тип сущности определяет свойство IsDeleted.
  • Мультитенантность. Тип сущности определяет свойство TenantId.

Пример

В следующем примере показано, как использовать глобальные фильтры запросов для реализации такого поведения запроса, как мультитенантность и обратимое удаление, в простой модели ведения блогов.

Совет

Вы можете скачать используемый в этой статье пример из репозитория GitHub.

Примечание.

Мультитенантность используется здесь просто для примера. Также есть статья с полным руководством по мультитенантности в приложениях EF Core.

Сначала определите сущности:

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

Обратите внимание на объявление _tenantId поля в сущности Blog . Это поле будет использоваться для связывания каждого экземпляра блога с конкретным клиентом. Также определено IsDeleted свойство типа сущности Post . Это свойство используется, чтобы проверять, был ли экземпляр записи удален обратимо. То есть экземпляр помечен как удаленный без физического удаления базовых данных.

Затем настройте фильтры запросов с OnModelCreating помощью HasQueryFilter API.

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

Выражения предиката, передаваемые HasQueryFilter вызовам, теперь автоматически будут применяться к любым запросам LINQ для этих типов.

Совет

Обратите внимание на использование поля уровня экземпляра DbContext. _tenantId используется для установки текущего клиента. Фильтры на уровне модели будут использовать значение из правильного экземпляра контекста (то есть экземпляра, выполняющего запрос).

Примечание.

Сейчас невозможно определить несколько фильтров запросов для одной и той же сущности. Будет применен только последний из них. Однако можно определить один фильтр с несколькими условиями с помощью логического AND оператора (&& в C#).

Использование свойств навигации

Свойства навигации можно также использовать при определении глобальных фильтров запросов. Использование свойств навигации в фильтре запросов приведет к рекурсивному применению фильтров запросов. Когда EF Core разворачивает свойства навигации, используемые в фильтрах запросов, он также применяет фильтры запросов, определенные для ссылочных сущностей.

Чтобы проиллюстрировать эту настройку фильтров OnModelCreating запросов следующим образом:

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

Затем выполните запрос ко всем Blog сущностям:

var filteredBlogs = db.Blogs.ToList();

Этот запрос возвращает следующий SQL-код, который применяет фильтры, определенные для сущностей Blog и 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

Примечание.

EF Core сейчас не обнаруживает циклы в определениях глобальных фильтров запросов, поэтому при определении циклов следует соблюдать особую осторожность. Их некорректное указание может делать циклы бесконечными при преобразовании запроса.

Доступ к сущности с фильтром запроса с помощью обязательной навигации

Внимание

Использование обязательной навигации для доступа к сущности, для которой определен глобальный фильтр запросов, может привести к непредвиденным результатам.

Для обязательной навигации всегда ожидается наличие связанной сущности. Если исключить обязательную связанную сущность с помощью фильтра запросов, родительская сущность также будет отсутствовать в результате. Так что результат может содержать меньше элементов, чем вы ожидали.

Чтобы проиллюстрировать проблему, можно использовать BlogPost указанные выше сущности и следующий OnModelCreating метод:

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

Модели можно присвоить начальные значения с помощью следующих данных:

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

Проблема возникает при выполнении двух запросов:

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

При указанной выше настройке первый запрос возвращает все шесть сущностей Post, однако второй запрос возвращает только три сущности. Это несоответствие происходит, так как Include метод во втором запросе загружает связанные Blog сущности. Поскольку навигация между Blog и Post является обязательной, при создании запроса EF Core использует INNER JOIN:

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 фильтр исключает все сущности Post, чьи связанные сущности Blog были удалены глобальным фильтром запросов.

Для решения этой проблемы можно вместо обязательной навигации использовать необязательную. В этом случае первый запрос остается прежним, но второй запрос будет создавать LEFT JOIN и возвращать шесть результатов.

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

В качестве альтернативного подхода можно указать согласованные фильтры для обеих сущностей Blog и Post. В этом случае к Blog и Post будут применяться совпадающие фильтры. Сущности Post, которые могут оказаться в непредвиденном состоянии, удаляются, и оба запроса возвращают три результата.

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

Отключение фильтров

Фильтры можно отключить для отдельных запросов LINQ, используя оператор IgnoreQueryFilters.

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

Ограничения

Глобальные фильтры запросов имеют следующие ограничения:

  • Фильтры можно определить только для корневого типа сущности в иерархии наследования.