skills/data/ef-queries/SKILL.md
Use when writing EF Core queries — LINQ, raw SQL, compiled queries, or fixing N+1 patterns.
npx skillsauth add faysilalshareef/dotnet-ai-kit ef-queriesInstall 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.
AsNoTracking() for all read-only queriesSelect() at the database level — avoid loading full entities// Project to DTO at database level — only selected columns are queried
var orders = await db.Orders
.AsNoTracking()
.Where(o => o.Status == OrderStatus.Submitted)
.OrderByDescending(o => o.CreatedAt)
.Select(o => new OrderSummaryResponse(
o.Id,
o.CustomerName,
o.Total,
o.CreatedAt))
.ToListAsync(ct);
internal sealed class ListOrdersHandler(AppDbContext db)
: IRequestHandler<ListOrdersQuery, PagedList<OrderSummaryResponse>>
{
public async Task<PagedList<OrderSummaryResponse>> Handle(
ListOrdersQuery request, CancellationToken ct)
{
var query = db.Orders.AsNoTracking();
// Dynamic filtering
if (!string.IsNullOrEmpty(request.CustomerName))
query = query.Where(o =>
o.CustomerName.Contains(request.CustomerName));
if (request.Status.HasValue)
query = query.Where(o => o.Status == request.Status.Value);
var totalCount = await query.CountAsync(ct);
var items = await query
.OrderByDescending(o => o.CreatedAt)
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(o => new OrderSummaryResponse(
o.Id, o.CustomerName, o.Total, o.CreatedAt))
.ToListAsync(ct);
return new PagedList<OrderSummaryResponse>(
items, totalCount, request.Page, request.PageSize);
}
}
// BAD — cartesian explosion with multiple Includes
var order = await db.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.FirstOrDefaultAsync(o => o.Id == id, ct);
// GOOD — split into separate queries
var order = await db.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery()
.FirstOrDefaultAsync(o => o.Id == id, ct);
// Configure globally
options.UseSqlServer(connectionString, sql =>
{
sql.UseQuerySplittingBehavior(
QuerySplittingBehavior.SplitQuery);
});
public sealed class OrderQueries
{
// Compiled query — parsed once, reused on every call
public static readonly Func<AppDbContext, Guid, CancellationToken,
Task<Order?>> GetById =
EF.CompileAsyncQuery((AppDbContext db, Guid id,
CancellationToken ct) =>
db.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
public static readonly Func<AppDbContext, OrderStatus,
CancellationToken, IAsyncEnumerable<Order>> GetByStatus =
EF.CompileAsyncQuery((AppDbContext db, OrderStatus status,
CancellationToken ct) =>
db.Orders
.AsNoTracking()
.Where(o => o.Status == status)
.OrderByDescending(o => o.CreatedAt));
}
// Usage
var order = await OrderQueries.GetById(db, orderId, ct);
// Unmapped types with raw SQL (.NET 8+)
var summaries = await db.Database
.SqlQuery<OrderSummaryDto>(
$"""
SELECT o.Id, o.CustomerName, o.Total,
COUNT(i.Id) AS ItemCount
FROM Orders o
LEFT JOIN OrderItems i ON o.Id = i.OrderId
WHERE o.Status = {status}
GROUP BY o.Id, o.CustomerName, o.Total
ORDER BY o.Total DESC
""")
.ToListAsync(ct);
// Parameterized — EF Core handles SQL injection protection
var orders = await db.Orders
.FromSqlInterpolated(
$"SELECT * FROM Orders WHERE CustomerName LIKE {pattern}")
.ToListAsync(ct);
// Entity configuration — automatically applied to all queries
builder.HasQueryFilter(o => !o.IsDeleted);
// Bypass filter when needed
var allOrders = await db.Orders
.IgnoreQueryFilters()
.ToListAsync(ct);
// Check existence — stops at first match
var exists = await db.Orders
.AnyAsync(o => o.Id == orderId, ct);
// Count with filter
var count = await db.Orders
.CountAsync(o => o.Status == OrderStatus.Pending, ct);
// BAD — loads all entities just to count
var count = (await db.Orders.ToListAsync(ct)).Count;
// Batch update — no entity loading
await db.Orders
.Where(o => o.Status == OrderStatus.Draft
&& o.CreatedAt < cutoffDate)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, OrderStatus.Cancelled)
.SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow), ct);
// Batch delete
await db.Orders
.Where(o => o.IsDeleted && o.DeletedAt < archiveDate)
.ExecuteDeleteAsync(ct);
Select projection)AsNoTracking() on queries that don't need change trackingToList() before Where() (evaluates entire table in memory)AsNoTracking() usage in query handlersSelect() projections vs full entity loadingAsSplitQuery() usageEF.CompileAsyncQuery compiled queriesExecuteUpdateAsync / ExecuteDeleteAsync batch operationsAsNoTracking() to all read-only queriesSelect() projections in query handlersAsSplitQuery() to queries with multiple IncludesEF.CompileAsyncQueryExecuteUpdateAsync batch operations| Scenario | Approach |
|----------|----------|
| Simple read | AsNoTracking() + Select() projection |
| Hot path | Compiled query |
| Multiple includes | AsSplitQuery() |
| Complex aggregation | Raw SQL with SqlQuery<T> |
| Bulk update | ExecuteUpdateAsync |
| Bulk delete | ExecuteDeleteAsync |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.