В этом руководстве мы подробно разберём одну из самых полезных функций 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 с именованными фильтрами.
Используя эти фильтры, вы можете избежать повторяющегося кода, повысить безопасность и сделать архитектуру приложения чище и удобнее.