EF Core query patterns. LINQ queries, raw SQL, compiled queries, projection with Select, split queries, and performance optimization. Trigger: EF query, LINQ, raw SQL, compiled query, projection, N+1.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
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 |