Global Query Filters & Owned Entities
Apply tenant isolation and soft-delete with global query filters, and map value objects using owned entity types.
Global Query Filters & Owned Entities is a free C# Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Are Global Query Filters?
Global query filters are WHERE predicates automatically applied to every LINQ query for a given entity type. They're perfect for soft delete, multi-tenancy, and row-level security — without repeating the condition everywhere.
Soft Delete with Global Filter
Add an IsDeleted flag and configure a global filter so deleted records are never returned unless you explicitly ignore the filter.
// 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();Multi-Tenancy with Global Filter
Use a filter that reads the current tenant from a scoped service. Every query is automatically scoped to the current tenant's data.
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 TenantIdIgnoring Global Filters
Sometimes you need to bypass the filter — e.g., admin queries for all tenants, or restoring a soft-deleted record. Use IgnoreQueryFilters() for that query only.
// 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();
}Filters on Navigation Properties
Global filters participate in JOINs too. If Posts has a filter, related Posts loaded via Include are also filtered automatically.
// 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 JOINOwned Entity Types: Concept
Owned entity types are classes that belong exclusively to a single owner entity. They have no primary key and are stored in the same table (by default). Perfect for value objects in 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; } = "";
}Configuring Owned Entities
Use OwnsOne to configure a single owned entity. EF Core prefixes columns with the navigation property name by default.
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, ...Owned Entities in a Separate Table
Use ToTable() inside OwnsOne to store owned entities in their own table — useful when the data is large or optional.
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 for Collections of Value Objects
OwnsMany configures a collection of owned entities — for example, a list of email addresses or phone numbers stored in a child table.
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"));Table-Per-Hierarchy vs Owned
Global filters also work well with TPH (Table-Per-Hierarchy) inheritance, where a discriminator column distinguishes subtypes. Apply a filter based on the discriminator for type-specific queries.
// 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'Real-World: SaaS Tenant Isolation
A complete SaaS multi-tenancy setup combines global query filters with a scoped tenant context, ensuring every query is automatically scoped without any per-query code.
// 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'Quick Check
Which method do you call to temporarily bypass a global query filter for a specific query?
Recap: Global Filters & Owned Entities
Key takeaways:
- Global query filters automatically add WHERE predicates to every query for an entity
- Ideal for soft delete, multi-tenancy, and row-level security
- Bypass with
IgnoreQueryFilters()for admin or restore operations - Owned entities model value objects — no PK, stored in owner's table
OwnsOne/OwnsManyconfigure owned relationships; useToTable()to split
Frequently asked questions
Is the “Global Query Filters & Owned Entities” lesson free?
Yes — the full text of “Global Query Filters & Owned Entities” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Global Query Filters & Owned Entities”?
Apply tenant isolation and soft-delete with global query filters, and map value objects using owned entity types. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Global Query Filters & Owned Entities” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Eager, Lazy & Explicit Loading
- Raw SQL, Stored Procedures & Interpolation
- Compiled Queries & Performance
- Global Query Filters & Owned Entities