5/5 - (1 голос)

В этом руководстве мы подробно разберём одну из самых полезных функций Entity Framework Core (EF Core)глобальные фильтры запросов (Global Query Filters).

Если вам когда-либо приходилось писать одно и то же условие WHERE в каждом запросе, вы знаете, как легко забыть добавить его один раз — и получить ошибку или даже случайно раскрыть чувствительные данные. Глобальные фильтры запросов решают эту проблему, автоматически применяя заданные условия ко всем запросам в приложении.

В этой статье мы разберём, что такое глобальные фильтры, когда их стоит использовать, как их реализовать и какие улучшения появились в EF Core 10 благодаря появлению именованных фильтров (Named Filters). Мы также создадим практический пример реализации на .NET 10 с использованием SQLite.

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

Проще говоря, глобальный фильтр запроса — это условие, которое EF Core автоматически применяет ко всем запросам для конкретной сущности. Можно считать его постоянным WHERE-условием, определённым один раз в модели и применяемым EF Core ко всем выборкам.

Это особенно полезно, когда нужно, чтобы определённые правила применялись всегда. Наиболее распространённые примеры:

  • Мягкое удаление (soft delete) — скрытие записей, помеченных как удалённые.

  • Мультиарендность (multi-tenancy) — обеспечение изоляции данных для разных клиентов (тенантов).

  • Архивация — хранение старых записей без отображения их в обычных запросах.

Практические примеры

1. Мягкое удаление (Soft Delete)

Вместо физического удаления строк из базы данных можно помечать их как удалённые, устанавливая флаг IsDeleted = true. Глобальные фильтры автоматически исключат такие записи из всех запросов.

2. Мультиарендность (Multi-Tenancy)

В SaaS-приложениях, где несколько компаний используют одну базу данных, глобальный фильтр гарантирует, что каждый клиент увидит только свои данные — без необходимости вручную добавлять фильтр в каждый запрос.

3. Архивация

Некоторые данные нужно сохранять для аудита или отчётности, но не отображать в ежедневных операциях. Глобальный фильтр позволяет скрывать такие записи, сохраняя при этом возможность их извлечения при необходимости.

Реализация глобальных фильтров в .NET 10

Рассмотрим, как реализовать мягкое удаление с помощью глобальных фильтров запросов EF Core в новом проекте .NET 10 Web API.

Шаг 1: Настройка проекта

Создаём новый проект и устанавливаем необходимые пакеты EF Core:

  • Microsoft.EntityFrameworkCore

  • Microsoft.EntityFrameworkCore.Sqlite

  • Microsoft.EntityFrameworkCore.Tools

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

Шаг 2: Создание сущности

В папке Entities создаём класс Blog:

public sealed class Blog
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsDeleted { get; set; }
}

Флаг IsDeleted будет использоваться для пометки записей как удалённых, без физического удаления из базы.

Шаг 3: Настройка DbContext

В папке Data создаём класс ApplicationDbContext, наследующий DbContext.
Используем primary constructor для передачи параметров в базовый класс.

Добавляем свойство DbSet<Blog> — оно указывает EF Core, что нужно создать таблицу Blogs в базе данных.

Переопределяем метод OnModelCreating и добавляем глобальный фильтр:

modelBuilder.Entity<Blog>()
.HasQueryFilter(b => !b.IsDeleted);

Теперь при каждом запросе к таблице Blogs EF Core будет автоматически исключать мягко удалённые записи.

Шаг 4: Настройка API-эндпоинтов

Создаём несколько API-методов для работы с блогами:

  • GET /api/blogs — возвращает все блоги (без удалённых).

  • GET /api/blogs/all — возвращает все блоги, включая удалённые (.IgnoreQueryFilters()).

  • GET /api/blogs/{id} — возвращает блог по ID.

  • POST /api/blogs — создаёт новый блог.

  • DELETE /api/blogs/{id} — выполняет мягкое удаление блога.

Шаг 5: Настройка SQLite и DI

В файле appsettings.json добавляем строку подключения:

"ConnectionStrings": {
"DefaultConnection": "Data Source=Data/AppDb.db"
}

В Program.cs регистрируем контекст базы данных:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

Шаг 6: Создание миграций

В консоли диспетчера пакетов выполняем:

Add-Migration Initial
Update-Database

После этого создастся база данных SQLite и таблица Blogs с полями Id, Name и IsDeleted.

Тестирование API

С помощью Postman или .http файла выполняем тестовые запросы:

  1. GET /api/blogs → возвращает пустой массив.

  2. POST /api/blogs → создаёт новый блог (ответ 201 Created).

  3. GET /api/blogs → возвращает созданный блог.

  4. GET /api/blogs/all → показывает те же данные (удалённых пока нет).

Реализация мягкого удаления

Теперь изменим поведение метода Delete, чтобы он не удалял записи физически.

Переопределим метод SaveChangesAsync в ApplicationDbContext:

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
}
return base.SaveChangesAsync(cancellationToken);
}

Теперь при удалении запись не удаляется, а просто помечается как удалённая.

После выполнения удаления:

  • GET /api/blogs → возвращает пустой массив.

  • GET /api/blogs/all → показывает удалённый блог с IsDeleted = true.

Улучшения в EF Core 10: Именованные фильтры

До версии EF Core 10 глобальные фильтры имели два серьёзных ограничения:

  1. Можно было задать только один фильтр на сущность.

  2. Метод .IgnoreQueryFilters() отключал все фильтры сразу.

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

Именованные фильтры решают эту проблему

Теперь можно:

  • Определять несколько фильтров для одной сущности.

  • Отключать конкретный фильтр, не трогая остальные.

Пример:

modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeleteFilter", b => !b.IsDeleted);

Чтобы обойти только этот фильтр:

context.Blogs.IgnoreQueryFilters("SoftDeleteFilter");

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

Использование констант для имён фильтров

Чтобы избежать опечаток и сделать код более читаемым, создайте статический класс с константами:

public static class BlogFilters
{
public const string SoftDeleteFilter = "SoftDeleteFilter";
}

Теперь вы можете ссылаться на фильтр так:

.IgnoreQueryFilters(BlogFilters.SoftDeleteFilter);

Если имя фильтра изменится, его нужно будет обновить только в одном месте.

Финальное тестирование

После добавления именованных фильтров:

  • GET /api/blogs → показывает только активные блоги.

  • POST /api/blogs → создаёт новый блог.

  • GET /api/blogs/all → показывает все блоги, включая мягко удалённые.

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

Заключение

Глобальные фильтры запросов — это мощный инструмент EF Core, который помогает автоматизировать логику выборки данных и повышает безопасность приложения.

В этом руководстве мы разобрали:

  • Что такое глобальные фильтры запросов.

  • Как реализовать мягкое удаление.

  • Какие улучшения появились в EF Core 10 с именованными фильтрами.

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