Skip to content

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.

InterfaceFilter key (GranitFilterNames)Filter expressionDefault
ISoftDeletableSoftDelete!e.IsDeletedEnabled
IActiveActivee.IsActiveEnabled
IProcessingRestrictableProcessingRestrictable!e.IsProcessingRestrictedEnabled
IMultiTenantMultiTenante.TenantId == currentTenant.IdEnabled
IPublishablePublishablee.IsPublishedEnabled

Filters are re-evaluated per query via DataFilter’s AsyncLocal state — EF Core extracts FilterProxy boolean properties as parameterized expressions.

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.

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 apply
var 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);

For entities implementing ITranslatable<T>, ApplyGranitConventions automatically configures:

  • Foreign key: Translation.ParentId → Parent.Id with cascade delete
  • Unique index: (ParentId, Culture)
  • Culture column: max length 20 (BCP 47)

Query extensions for translated entities:

// Eager load translations for a culture
var patients = await db.Patients
.IncludeTranslations<Patient, PatientTranslation>("fr")
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Filter by translation property
var results = await db.Patients
.WhereTranslation<Patient, PatientTranslation>("fr", t => t.DisplayName.Contains("Jean"))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);