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. This is 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, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 4 lessons in total.
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
Learn C# with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 93
- Lessons
- 346
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. 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. The C# Academy course includes 4 lessons in total.
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, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
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