0Pricing
C# Academy · Урок

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

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

«Глобальные фильтры запросов и принадлежащие сущности» — бесплатный урок 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 CustomerId

OwnsMany для коллекций объектов-значений

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 — локальная установка не требуется.

Все уроки этого курса

  1. Жадная, ленивая и явная загрузка
  2. Сырой SQL, хранимые процедуры и интерполяция
  3. Компилируемые запросы и производительность
  4. Глобальные фильтры запросов и принадлежащие сущности
← Назад к C# Academy