Глобальные фильтры запросов и принадлежащие сущности
Реализуйте изоляцию арендаторов и мягкое удаление с помощью глобальных фильтров запросов, а объекты-значения отображайте через типы принадлежащих сущностей.
«Глобальные фильтры запросов и принадлежащие сущности» — бесплатный урок C# Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Что такое глобальные фильтры запросов
Глобальные фильтры запросов — это предикаты WHERE, которые автоматически применяются к каждому запросу LINQ для заданного типа сущности. Они идеально подходят для мягкого удаления, мультитенантности и безопасности на уровне строк — без повторения условия во всех запросах.
Мягкое удаление с глобальным фильтром
Добавьте флаг IsDeleted и настройте глобальный фильтр, чтобы удалённые записи никогда не возвращались, если только Вы явно не отключите фильтр.
// Entity with soft-delete flag
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool IsDeleted { get; set; }
}
// Configure in OnModelCreating
modelBuilder.Entity<Post>()
.HasQueryFilter(p => !p.IsDeleted);
// All queries now implicitly add WHERE IsDeleted = 0:
var posts = await _db.Posts.ToListAsync();Мультитенантность с глобальным фильтром
Используйте фильтр, который получает текущего арендатора из сервиса с ограниченной областью действия. Каждый запрос автоматически ограничивается данными текущего арендатора.
public class AppDbContext : DbContext
{
private readonly ICurrentUserService _user;
public AppDbContext(DbContextOptions<AppDbContext> opt, ICurrentUserService user)
: base(opt) => _user = user;
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Order>().HasQueryFilter(
o => o.TenantId == _user.TenantId);
}
}
// Every Order query automatically filters by TenantIdОтключение глобальных фильтров
Иногда фильтр нужно обойти — например, в запросах администратора ко всем арендаторам или при восстановлении записи после мягкого удаления. Используйте IgnoreQueryFilters() только для этого запроса.
// Admin: see ALL posts including deleted
var allPosts = await _db.Posts
.IgnoreQueryFilters()
.ToListAsync();
// Restore a specific deleted post
var deleted = await _db.Posts
.IgnoreQueryFilters()
.FirstOrDefaultAsync(p => p.Id == 42);
if (deleted is not null)
{
deleted.IsDeleted = false;
await _db.SaveChangesAsync();
}Фильтры для свойств навигации
Глобальные фильтры участвуют и в операциях JOIN. Если для записей настроен фильтр, связанные записи, загружаемые через Include, также автоматически фильтруются.
// Blog with soft-delete filter on Posts
var blog = await _db.Blogs
.Include(b => b.Posts) // only non-deleted posts are included
.FirstOrDefaultAsync(b => b.Id == 1);
// blog.Posts contains only active posts — the filter applied in the JOINТипы сущностей, принадлежащие владельцу: понятие
Типы сущностей, принадлежащие владельцу, — это классы, которые находятся в исключительной собственности одной сущности-владельца. У них нет первичного ключа, и по умолчанию они хранятся в той же таблице. Это идеальный вариант для объектов-значений в DDD.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address BillingAddress { get; set; } = new();
public Address ShippingAddress { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string ZipCode { get; set; } = "";
public string Country { get; set; } = "";
}Настройка сущностей, принадлежащих владельцу
Используйте OwnsOne, чтобы настроить одну принадлежащую сущность. По умолчанию EF Core добавляет к столбцам префикс с именем свойства навигации.
modelBuilder.Entity<Customer>(entity =>
{
entity.OwnsOne(c => c.BillingAddress, addr =>
{
addr.Property(a => a.Street).HasMaxLength(300);
addr.Property(a => a.Country).HasMaxLength(2);
});
entity.OwnsOne(c => c.ShippingAddress);
});
// Table: Customers with columns:
// BillingAddress_Street, BillingAddress_City, ...
// ShippingAddress_Street, ShippingAddress_City, ...Принадлежащие сущности в отдельной таблице
Используйте ToTable() внутри OwnsOne, чтобы хранить принадлежащие сущности в собственной таблице — это полезно, когда данные велики по объёму или необязательны.
modelBuilder.Entity<Customer>().OwnsOne(
c => c.BillingAddress,
addr => addr.ToTable("CustomerBillingAddresses"));
// Now BillingAddress columns are in a separate table
// with a FK back to CustomerIdOwnsMany для коллекций объектов-значений
OwnsMany настраивает коллекцию принадлежащих сущностей — например, список адресов электронной почты или номеров телефонов, хранящийся в дочерней таблице.
public class Customer
{
public int Id { get; set; }
public IList<PhoneNumber> PhoneNumbers { get; set; } = new List<PhoneNumber>();
}
public class PhoneNumber
{
public string Number { get; set; } = "";
public string Type { get; set; } = "";
}
modelBuilder.Entity<Customer>()
.OwnsMany(c => c.PhoneNumbers,
phone => phone.ToTable("CustomerPhones"));Иерархия в одной таблице и принадлежащие сущности
Глобальные фильтры хорошо работают и с наследованием TPH (таблица на иерархию), где столбец дискриминатора различает подтипы. Применяйте фильтр по дискриминатору для запросов к конкретному типу.
// Base entity
public abstract class Payment { public int Id { get; set; } }
public class CardPayment : Payment { public string CardLast4 { get; set; } = ""; }
public class BankPayment : Payment { public string IBAN { get; set; } = ""; }
// All Payment subtypes in one table with a Discriminator column
// No extra config needed — EF Core handles TPH by default
var cards = await _db.Set<CardPayment>().ToListAsync();
// SQL: SELECT * FROM Payments WHERE Discriminator = 'CardPayment'Реальный пример: изоляция арендаторов в SaaS
Полная настройка мультитенантности SaaS сочетает глобальные фильтры запросов с контекстом арендатора, ограниченным областью действия, благодаря чему каждый запрос автоматически ограничивается нужными данными без кода для отдельных запросов.
// Scoped tenant service
public class TenantContext
{
public Guid TenantId { get; set; }
}
// DbContext uses it
modelBuilder.Entity<Invoice>().HasQueryFilter(
inv => inv.TenantId == _tenantCtx.TenantId);
modelBuilder.Entity<Customer>().HasQueryFilter(
c => c.TenantId == _tenantCtx.TenantId);
// Queries are automatically scoped:
var invoices = await _db.Invoices.ToListAsync();
// WHERE TenantId = 'current-tenant-guid'Быстрая проверка
Какой метод нужно вызвать, чтобы временно обойти глобальный фильтр запросов для конкретного запроса?
Итоги: глобальные фильтры и принадлежащие сущности
Главное:
- Глобальные фильтры запросов автоматически добавляют предикаты WHERE в каждый запрос к сущности
- Они идеально подходят для мягкого удаления, мультитенантности и безопасности на уровне строк
- Обходите фильтры с помощью
IgnoreQueryFilters()при операциях администратора или восстановления - Принадлежащие сущности моделируют объекты-значения — у них нет PK, и они хранятся в таблице владельца
OwnsOne/OwnsManyнастраивают связи с принадлежащими сущностями; используйтеToTable(), чтобы разделить таблицы
Часто задаваемые вопросы
Урок «Глобальные фильтры запросов и принадлежащие сущности» бесплатный?
Да — полный текст урока «Глобальные фильтры запросов и принадлежащие сущности» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Глобальные фильтры запросов и принадлежащие сущности»?
Реализуйте изоляцию арендаторов и мягкое удаление с помощью глобальных фильтров запросов, а объекты-значения отображайте через типы принадлежащих сущностей. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Глобальные фильтры запросов и принадлежащие сущности»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Жадная, ленивая и явная загрузка
- Сырой SQL, хранимые процедуры и интерполяция
- Компилируемые запросы и производительность
- Глобальные фильтры запросов и принадлежащие сущности