Home C# / .NET EF Core Advanced Querying Patterns: Master Complex Queries in C#
Advanced 3 min · July 13, 2026

EF Core Advanced Querying Patterns: Master Complex Queries in C#

Learn advanced EF Core querying patterns in C#: compiled queries, raw SQL, global query filters, and more.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.

Follow
Production
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic knowledge of C# and .NET
  • Familiarity with Entity Framework Core basics (DbContext, DbSet, migrations)
  • Understanding of LINQ and SQL
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use compiled queries for high-performance repeated queries.
  • Leverage raw SQL and FromSqlRaw for complex operations.
  • Apply global query filters for multi-tenancy and soft delete.
  • Use AsNoTracking for read-only scenarios to improve speed.
  • Avoid N+1 queries by using Include and ThenInclude eagerly.
✦ Definition~90s read
What is EF Core Advanced Querying Patterns?

EF Core advanced querying patterns are techniques to optimize and secure data access in .NET applications, including compiled queries, raw SQL, global filters, and loading strategies.

Think of EF Core as a smart librarian.
Plain-English First

Think of EF Core as a smart librarian. Basic queries are like asking for a book by title. Advanced patterns are like asking for all books by authors who wrote in a specific year, sorted by popularity, but only if they are available. You can also write notes directly to the librarian (raw SQL) for special requests. Global filters are like a rule that the librarian always ignores damaged books unless you specifically ask for them.

Entity Framework Core (EF Core) is a powerful ORM that simplifies data access in .NET applications. However, as your application grows, you'll encounter scenarios where basic LINQ queries fall short. Performance bottlenecks, complex joins, multi-tenancy, and soft delete requirements demand advanced querying patterns. In this tutorial, we'll dive deep into EF Core's advanced querying capabilities: compiled queries for high-performance repeated execution, raw SQL for complex operations, global query filters for automatic data scoping, and more. You'll learn how to optimize your queries, avoid common pitfalls like the N+1 problem, and handle real-world production scenarios. We'll also walk through a production incident where a missing global filter caused a data leak, and provide a debugging guide to quickly diagnose query issues. By the end, you'll be equipped to write efficient, maintainable, and secure data access code in C#.

1. Compiled Queries for High Performance

Compiled queries allow EF Core to cache the query plan, reducing overhead for frequently executed queries. This is especially useful for queries that are executed many times with different parameters. Use EF.CompileQuery or EF.CompileAsyncQuery to create a delegate that can be reused. The delegate takes a DbContext and parameters, returning the result. Example: a query to fetch orders by customer ID. The compiled query is thread-safe and can be stored as a static field. Note that compiled queries cannot use Include with ThenInclude in some versions; test thoroughly.

CompiledQueryExample.csCSHARP
1
2
3
4
5
6
7
8
9
private static readonly Func<AppDbContext, int, IQueryable<Order>> GetOrdersByCustomer = 
    EF.CompileQuery((AppDbContext context, int customerId) =>
        context.Orders.Where(o => o.CustomerId == customerId).OrderBy(o => o.OrderDate));

public IQueryable<Order> GetOrders(int customerId)
{
    using var context = new AppDbContext();
    return GetOrdersByCustomer(context, customerId);
}
💡When to use compiled queries
📊 Production Insight
In production, compiled queries can reduce CPU usage by up to 30% for high-traffic endpoints. However, they add complexity; measure before optimizing.
🎯 Key Takeaway
Compiled queries improve performance by caching the query plan. Use them for repeated queries.

2. Raw SQL Queries with FromSqlRaw and FromSqlInterpolated

Sometimes LINQ cannot express the query efficiently, or you need to use database-specific features. EF Core provides FromSqlRaw and FromSqlInterpolated to execute raw SQL. FromSqlRaw is for parameterized queries (use with caution to avoid SQL injection). FromSqlInterpolated handles interpolation safely. These methods return IQueryable<T> so you can compose further LINQ operators. Example: a full-text search query. Note that raw SQL queries bypass global query filters unless you include them manually. Also, the result set must match the entity shape.

RawSqlExample.csCSHARP
1
2
3
4
5
6
var searchTerm = "EF Core";
var blogs = await context.Blogs
    .FromSqlInterpolated($@"SELECT * FROM Blogs 
        WHERE CONTAINS(Title, {searchTerm})")
    .Where(b => b.IsActive)
    .ToListAsync();
⚠ SQL Injection Risk
📊 Production Insight
Raw SQL queries are often used for reporting or full-text search. Ensure they are covered by integration tests.
🎯 Key Takeaway
Use raw SQL when LINQ is insufficient, but always parameterize inputs to prevent SQL injection.

3. Global Query Filters for Multi-Tenancy and Soft Delete

Global query filters are LINQ predicates applied automatically to all queries for an entity. They are defined in OnModelCreating using HasQueryFilter. Common use cases: multi-tenancy (filter by TenantId), soft delete (IsDeleted == false), and active records. Filters are applied to all queries including Include and navigation properties. To bypass a filter, use IgnoreQueryFilters. Example: a multi-tenant system where each user sees only their organization's data. The filter is applied at the DbContext level, typically using a scoped service for the tenant ID.

GlobalFilterExample.csCSHARP
1
2
3
4
5
6
7
8
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == _tenantId);
    modelBuilder.Entity<Customer>().HasQueryFilter(c => !c.IsDeleted);
}

// To bypass:
var allOrders = await context.Orders.IgnoreQueryFilters().ToListAsync();
🔥Filter Inheritance
📊 Production Insight
Always test that filters are applied to all query paths, especially raw SQL and Include chains.
🎯 Key Takeaway
Global query filters enforce cross-cutting concerns automatically. Use them for multi-tenancy and soft delete.

4. Eager Loading vs. Explicit Loading vs. Lazy Loading

EF Core supports three loading patterns for related data. Eager loading uses Include/ThenInclude to load related data in a single query. Explicit loading loads related data on demand using Load/Query. Lazy loading (requires proxy) loads navigation properties automatically when accessed. Each has trade-offs: eager loading can cause large joins, lazy loading leads to N+1 queries, explicit loading gives control. Best practice: use eager loading for known relationships, explicit loading for optional data, and avoid lazy loading in production. Example: loading orders with items and product details.

LoadingPatternsExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Eager loading
var orders = await context.Orders
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
    .ToListAsync();

// Explicit loading
var order = await context.Orders.FindAsync(1);
await context.Entry(order).Collection(o => o.OrderItems).LoadAsync();

// Lazy loading (requires proxy)
public class Order
{
    public virtual ICollection<OrderItem> OrderItems { get; set; }
}
⚠ Lazy Loading Pitfall
📊 Production Insight
Use AutoMapper's ProjectTo to select only needed columns, reducing data transfer and improving performance.
🎯 Key Takeaway
Choose loading strategy based on use case. Prefer eager loading for performance-critical paths.

5. Split Queries to Avoid Cartesian Explosion

When including multiple collections, EF Core generates a single query with joins that can cause a Cartesian explosion (rows multiplied). Split queries execute multiple queries, one per collection, reducing data duplication. Use AsSplitQuery() to enable. However, split queries increase round trips and may cause inconsistent data if not wrapped in a transaction. Example: loading orders with items and shipments. For large datasets, split queries are often faster. Note: split queries are not supported for some providers.

SplitQueryExample.csCSHARP
1
2
3
4
5
var orders = await context.Orders
    .Include(o => o.OrderItems)
    .Include(o => o.Shipments)
    .AsSplitQuery()
    .ToListAsync();
💡When to use split queries
📊 Production Insight
In production, test with realistic data volumes. Sometimes a single query with selective columns is better.
🎯 Key Takeaway
Split queries prevent Cartesian explosion at the cost of multiple round trips. Use judiciously.

6. Query Tags for Debugging and Monitoring

Query tags allow you to annotate generated SQL with a custom string, making it easier to identify queries in logs and database traces. Use TagWith() to add a tag. Tags appear as comments in the SQL. This is invaluable for debugging performance issues in production. Example: tagging a query with the calling method name. You can also use multiple tags. Tags are not SQL-injectable.

QueryTagExample.csCSHARP
1
2
3
4
5
6
var orders = await context.Orders
    .Where(o => o.CustomerId == customerId)
    .TagWith("GetOrdersByCustomer")
    .ToListAsync();
// Generated SQL: -- GetOrdersByCustomer
// SELECT ... FROM Orders WHERE CustomerId = @__customerId_0
🔥Query Tag Best Practices
📊 Production Insight
Combine with structured logging to correlate queries with user actions.
🎯 Key Takeaway
Query tags help identify queries in logs and traces. Use them for all production queries.
● Production incidentPOST-MORTEMseverity: high

The Multi-Tenant Data Leak: A Missing Global Filter

Symptom
Users reported seeing data from other organizations in their dashboards.
Assumption
The developer assumed that all queries automatically filtered by tenant ID because the DbContext was scoped per request.
Root cause
A new query was added using raw SQL (FromSqlRaw) that bypassed the global query filter, returning all rows regardless of tenant.
Fix
Applied the tenant filter explicitly in the raw SQL query and added a unit test to verify the filter is applied.
Key lesson
  • Always verify that global query filters are applied to all queries, including raw SQL.
  • Use interception or repository patterns to enforce cross-cutting concerns.
  • Write integration tests that check data isolation between tenants.
  • Consider using query tags to trace queries in logs.
  • Educate the team about the limitations of global filters.
Production debug guideSymptom to Action4 entries
Symptom · 01
Slow query performance
Fix
Enable EF Core logging to capture generated SQL. Use SQL Server Profiler or Azure SQL Analytics to identify missing indexes. Consider using compiled queries or AsNoTracking.
Symptom · 02
Data leak across tenants
Fix
Check global query filters are applied. Review raw SQL queries. Add query tags and log tenant ID. Implement repository pattern to enforce filters.
Symptom · 03
N+1 query problem
Fix
Use Include/ThenInclude for eager loading. Alternatively, use AutoMapper's ProjectTo to select only needed columns. Monitor with MiniProfiler.
Symptom · 04
Unexpected results with split queries
Fix
Review the generated SQL for multiple round trips. Use AsSplitQuery() for large related data sets. Ensure correct navigation properties.
★ Quick Debug Cheat SheetCommon EF Core query issues and immediate actions.
Slow query
Immediate action
Enable logging: optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)
Commands
SELECT ... FROM ... WHERE ...
EXPLAIN ANALYZE <query>
Fix now
Add index on foreign key columns.
N+1 queries+
Immediate action
Check for lazy loading. Use .Include() or .ThenInclude()
Commands
var orders = context.Orders.Include(o => o.OrderItems).ToList();
Fix now
Replace lazy loading with eager loading.
Data leak+
Immediate action
Verify global query filters are applied. Check raw SQL.
Commands
modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == _tenantId);
SELECT * FROM Orders WHERE TenantId = @tenantId
Fix now
Add filter to raw SQL: FROM Orders WHERE TenantId = @tenantId
PatternUse CasePerformanceComplexity
Compiled QueriesRepeated queries with same structureHigh (cached plan)Medium
Raw SQLComplex or database-specific queriesHigh (optimized SQL)High
Global FiltersMulti-tenancy, soft deleteLow overheadLow
Split QueriesMultiple collections to avoid Cartesian explosionMedium (more round trips)Medium
Eager LoadingKnown related dataHigh (single query)Low
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
CompiledQueryExample.csprivate static readonly Func> GetOrdersByCu...1. Compiled Queries for High Performance
RawSqlExample.csvar searchTerm = "EF Core";2. Raw SQL Queries with FromSqlRaw and FromSqlInterpolated
GlobalFilterExample.csprotected override void OnModelCreating(ModelBuilder modelBuilder)3. Global Query Filters for Multi-Tenancy and Soft Delete
LoadingPatternsExample.csvar orders = await context.Orders4. Eager Loading vs. Explicit Loading vs. Lazy Loading
SplitQueryExample.csvar orders = await context.Orders5. Split Queries to Avoid Cartesian Explosion
QueryTagExample.csvar orders = await context.Orders6. Query Tags for Debugging and Monitoring

Key takeaways

1
Compiled queries boost performance for repeated queries by caching the query plan.
2
Raw SQL is powerful but must be parameterized to prevent SQL injection.
3
Global query filters enforce multi-tenancy and soft delete automatically.
4
Choose loading strategy wisely
eager loading for performance, split queries for large collections.
5
Query tags help debug and monitor queries in production.

Common mistakes to avoid

4 patterns
×

Using lazy loading without realizing it causes N+1 queries.

×

Forgetting to parameterize raw SQL queries.

×

Assuming global query filters apply to raw SQL.

×

Overusing Include leading to Cartesian explosion.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how global query filters work and give an example.
Q02SENIOR
What are the trade-offs between compiled queries and regular LINQ querie...
Q03SENIOR
How would you handle multi-tenancy in EF Core?
Q01 of 03SENIOR

Explain how global query filters work and give an example.

ANSWER
Global query filters are LINQ predicates applied automatically to all queries for an entity. They are defined in OnModelCreating using HasQueryFilter. Example: modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == currentTenantId). They are applied to all queries including Include and navigation properties. Use IgnoreQueryFilters to bypass.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between FromSqlRaw and FromSqlInterpolated?
02
How do I debug a slow EF Core query?
03
Can I use global query filters with raw SQL?
04
What is the N+1 query problem?
05
How do I choose between eager loading and split queries?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
🔥

That's ASP.NET. Mark it forged?

3 min read · try the examples if you haven't

Previous
EF Core Interceptors and Events
19 / 35 · ASP.NET
Next
Blazor Component Lifecycle