В этом руководстве мы подробно разберём одну из самых полезных функций 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 файла выполняем тестовые запросы:
-
GET /api/blogs→ возвращает пустой массив. -
POST /api/blogs→ создаёт новый блог (ответ 201 Created). -
GET /api/blogs→ возвращает созданный блог. -
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 глобальные фильтры имели два серьёзных ограничения:
-
Можно было задать только один фильтр на сущность.
-
Метод
.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 с именованными фильтрами.
Используя эти фильтры, вы можете избежать повторяющегося кода, повысить безопасность и сделать архитектуру приложения чище и удобнее.