.claude/skills/ef-query-optimization/SKILL.md
Use when diagnosing or improving slow EF Core queries, fixing N+1 problems, eliminating cartesian explosions from multi-level Includes, or when API endpoints are loading full entities when only a few columns are needed. Covers DTO projections with Select, AsNoTracking and AsNoTrackingWithIdentityResolution, AsSplitQuery to prevent cartesian explosions, compiled queries for hot paths, query tags for SQL profiler identification, filtered Includes, and slow-query interceptors. Domain: Data Access, Performance. Level: Advanced. Tags: ef-core, performance, query, sql, optimization.
npx skillsauth add klod68/littlerae ef-query-optimizationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
EF Core generates SQL that is often correct but suboptimal: N+1 queries from lazy loading, cartesian explosions from multi-level includes, full-entity materialization when only two columns are needed, and repeated query compilation overhead.
// ❌ Loads entire entity + all columns
var orders = await dbContext.Orders.ToListAsync(ct);
// ✅ Project only what's needed
var orderSummaries = await dbContext.Orders
.Select(o => new OrderSummaryDto
{
Id = o.Id,
Total = o.Total,
ItemCount = o.Items.Count
})
.ToListAsync(ct);
// ✅ No change tracking overhead
var orders = await dbContext.Orders
.AsNoTracking()
.Where(o => o.Status == OrderStatus.Active)
.ToListAsync(ct);
// ✅ When you need identity resolution without tracking
var orders = await dbContext.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Customer)
.ToListAsync(ct);
// ❌ Single query with multiple includes = cartesian product
var orders = await dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.ToListAsync(ct); // Items × Payments rows returned
// ✅ Split into separate queries
var orders = await dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery()
.ToListAsync(ct); // 3 separate queries, no explosion
private static readonly Func<AppDbContext, Guid, CancellationToken, Task<Order?>> GetOrderById =
EF.CompileAsyncQuery((AppDbContext db, Guid id, CancellationToken ct) =>
db.Orders
.AsNoTracking()
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
// Usage:
var order = await GetOrderById(dbContext, orderId, ct);
var orders = await dbContext.Orders
.TagWith("GetActiveOrdersForDashboard")
.AsNoTracking()
.Where(o => o.Status == OrderStatus.Active)
.ToListAsync(ct);
// SQL output includes: -- GetActiveOrdersForDashboard
// ✅ Only include active items
var orders = await dbContext.Orders
.Include(o => o.Items.Where(i => !i.IsCancelled))
.ToListAsync(ct);
internal sealed class SlowQueryInterceptor : DbCommandInterceptor
{
private readonly ILogger<SlowQueryInterceptor> _logger;
private readonly TimeSpan _threshold = TimeSpan.FromMilliseconds(500);
public SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
{
_logger = logger;
}
public override ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result,
CancellationToken ct = default)
{
if (eventData.Duration > _threshold)
{
_logger.LogWarning("Slow query ({Duration}ms): {Sql}",
eventData.Duration.TotalMilliseconds,
command.CommandText);
}
return ValueTask.FromResult(result);
}
}
| Symptom | Cause | Fix |
|---------|-------|-----|
| N+1 queries in loop | Lazy loading or missing Include | Add Include or project with Select |
| Huge result set | Missing Where or Take | Add filters and pagination |
| Cartesian explosion | Multiple Include on collections | Use AsSplitQuery() |
| Full entity in API response | No projection | Use Select to DTO |
| Repeated query compilation | Hot path without compiled query | Use EF.CompileAsyncQuery |
| Unidentifiable slow query | No query tag | Add TagWith("description") |
| Tracking overhead on reads | Missing AsNoTracking | Add AsNoTracking() to all read queries |
AsSplitQuery trade-off: Splits avoid cartesian explosions but create multiple round-trips. Benchmark both for your data shape.Include with navigation property filters. Cannot use interpolated FormattableString parameters.AsNoTrackingWithIdentityResolution: Slower than AsNoTracking but ensures the same entity instance is reused across navigations — use when the query joins the same entity multiple times.ToQueryString(): Use in development to see the generated SQL — never in production code.tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.