Query Filters — Soft Delete, Multi-Tenant, Active
ApplyGranitConventions registers one named HasQueryFilter per applicable
interface on each entity type (EF Core 10). Filters are independent — bypass one
without affecting the others.
| Interface | Filter key (GranitFilterNames) | Filter expression | Default |
|---|---|---|---|
ISoftDeletable | SoftDelete | !e.IsDeleted | Enabled |
IActive | Active | e.IsActive | Enabled |
IProcessingRestrictable | ProcessingRestrictable | !e.IsProcessingRestricted | Enabled |
IMultiTenant | MultiTenant | e.TenantId == currentTenant.Id | Enabled |
IPublishable | Publishable | e.IsPublished | Enabled |
Filters are re-evaluated per query via DataFilter’s AsyncLocal state — EF Core
extracts FilterProxy boolean properties as parameterized expressions.
Runtime filter control — service scope
Section titled “Runtime filter control — service scope”Use IDataFilter to bypass a filter for all queries in the current async flow:
public class PatientAdminService(IDataFilter dataFilter, AppDbContext db){ public async Task<List<Patient>> GetAllIncludingDeletedAsync( CancellationToken cancellationToken) { using (dataFilter.Disable<ISoftDeletable>()) { return await db.Patients .ToListAsync(cancellationToken) .ConfigureAwait(false); } // Filter re-enabled automatically }}Scopes nest correctly — inner Enable/Disable calls restore the outer state
on disposal.
Per-query bypass (EF Core 10)
Section titled “Per-query bypass (EF Core 10)”Use IgnoreQueryFilters with one or more filter keys to bypass specific filters
for a single query, without affecting other queries in the same flow:
// Bypass only soft delete — multi-tenant and other filters still applyvar deletedPatients = await db.Patients .IgnoreQueryFilters([GranitFilterNames.SoftDelete]) .Where(p => p.IsDeleted) .ToListAsync(cancellationToken) .ConfigureAwait(false);
// Bypass all named filters (equivalent to IgnoreQueryFilters() without args)var allPatients = await db.Patients .IgnoreQueryFilters([ GranitFilterNames.SoftDelete, GranitFilterNames.MultiTenant]) .ToListAsync(cancellationToken) .ConfigureAwait(false);Translation conventions
Section titled “Translation conventions”For entities implementing ITranslatable<T>, ApplyGranitConventions automatically
configures:
- Foreign key:
Translation.ParentId → Parent.Idwith cascade delete - Unique index:
(ParentId, Culture) - Culture column: max length 20 (BCP 47)
Query extensions for translated entities:
// Eager load translations for a culturevar patients = await db.Patients .IncludeTranslations<Patient, PatientTranslation>("fr") .ToListAsync(cancellationToken) .ConfigureAwait(false);
// Filter by translation propertyvar results = await db.Patients .WhereTranslation<Patient, PatientTranslation>("fr", t => t.DisplayName.Contains("Jean")) .ToListAsync(cancellationToken) .ConfigureAwait(false);Common pitfalls
Section titled “Common pitfalls”See also
Section titled “See also”- Persistence overview — setup, isolated DbContext, host-owned migrations
- Interceptors — audit, soft delete, concurrency, events
- Zero-downtime migrations — Expand & Contract pattern