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.
20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.
- ✓Basic knowledge of C# and .NET
- ✓Familiarity with Entity Framework Core basics (DbContext, DbSet, migrations)
- ✓Understanding of LINQ and SQL
- 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.
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.
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.
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.
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.
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.
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.
The Multi-Tenant Data Leak: A Missing Global Filter
- 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.
AsSplitQuery() for large related data sets. Ensure correct navigation properties.SELECT ... FROM ... WHERE ...EXPLAIN ANALYZE <query>| File | Command / Code | Purpose |
|---|---|---|
| CompiledQueryExample.cs | private static readonly Func | 1. Compiled Queries for High Performance |
| RawSqlExample.cs | var searchTerm = "EF Core"; | 2. Raw SQL Queries with FromSqlRaw and FromSqlInterpolated |
| GlobalFilterExample.cs | protected override void OnModelCreating(ModelBuilder modelBuilder) | 3. Global Query Filters for Multi-Tenancy and Soft Delete |
| LoadingPatternsExample.cs | var orders = await context.Orders | 4. Eager Loading vs. Explicit Loading vs. Lazy Loading |
| SplitQueryExample.cs | var orders = await context.Orders | 5. Split Queries to Avoid Cartesian Explosion |
| QueryTagExample.cs | var orders = await context.Orders | 6. Query Tags for Debugging and Monitoring |
Key takeaways
Common mistakes to avoid
4 patternsUsing 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 Questions on This Topic
Explain how global query filters work and give an example.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.
That's ASP.NET. Mark it forged?
3 min read · try the examples if you haven't