Documentation

EF Core Async Query Analyzer

Use LinqContraband as an EF Core async query analyzer for sync-over-async EF Core calls, missing cancellation tokens, SaveChangesAsync loops, and async stream buffering.

EF Core Async Query Analyzer

LinqContraband helps teams review async EF Core query paths before they reach production. It catches synchronous database calls inside async methods, missing cancellation tokens on async EF APIs, SaveChangesAsync inside loops, and async stream buffering that should stay streaming.

For the focused missing-token workflow, use the EF Core CancellationToken analyzer guide.

Install the official NuGet package:

dotnet add package LinqContraband

Why Async EF Core Calls Need Review

Async code can still block request threads if it calls synchronous EF Core APIs. It can also ignore request cancellation or buffer an async stream into memory before looping once.

public async Task<List<User>> ActiveUsers(CancellationToken cancellationToken)
{
    return db.Users
        .Where(user => user.IsActive)
        .ToList(); // LC008: sync-over-async
}

Prefer the async EF Core API and pass the token that represents the current operation:

public async Task<List<User>> ActiveUsers(CancellationToken cancellationToken)
{
    return await db.Users
        .Where(user => user.IsActive)
        .ToListAsync(cancellationToken);
}

Rules Covered By The Async Guide

Rule What it catches Why it matters
LC008: sync-over-async Synchronous EF Core query, save, find, and bulk APIs inside async contexts. Blocking database I/O ties up request or worker threads that could be serving other work.
LC026: missing CancellationToken Async EF Core calls that omit a token, pass default, or pass CancellationToken.None while a usable token is in scope. Cancelled requests and stopping workers should not leave database queries running unnecessarily. See the EF Core CancellationToken analyzer guide for the focused rollout path.
LC043: async stream buffering Immediate ToListAsync or ToArrayAsync buffering of an IAsyncEnumerable<T> before a single loop. Buffering loses the memory and latency benefits of streaming.
LC010: SaveChanges inside loop SaveChanges or SaveChangesAsync inside for, foreach, while, and related loop shapes. Per-item commits create repeated transactions and partial-progress states.

Common Async EF Core Problems

Sync query inside async method

public async Task<int> CountUsers()
{
    return db.Users.Count(); // LC008
}
public async Task<int> CountUsers(CancellationToken cancellationToken)
{
    return await db.Users.CountAsync(cancellationToken);
}

Async query without cancellation

public async Task<User?> FindUser(int id, CancellationToken cancellationToken)
{
    return await db.Users.FirstOrDefaultAsync(user => user.Id == id); // LC026
}
public async Task<User?> FindUser(int id, CancellationToken cancellationToken)
{
    return await db.Users.FirstOrDefaultAsync(user => user.Id == id, cancellationToken);
}

Async stream buffering before one loop

var users = await stream.ToListAsync(); // LC043
foreach (var user in users)
{
    Send(user);
}
await foreach (var user in stream)
{
    Send(user);
}

SaveChangesAsync inside a loop

foreach (var order in orders)
{
    order.Status = OrderStatus.Sent;
    await db.SaveChangesAsync(cancellationToken); // LC010
}
foreach (var order in orders)
{
    order.Status = OrderStatus.Sent;
}

await db.SaveChangesAsync(cancellationToken);

Safer Async Patterns

  • Use EF Core’s async counterpart inside async code: ToListAsync, CountAsync, FirstOrDefaultAsync, SaveChangesAsync, FindAsync, ExecuteUpdateAsync, and ExecuteDeleteAsync.
  • Pass the available CancellationToken through async query and save APIs.
  • Do not wrap synchronous EF Core database calls in Task.Run to make them look async.
  • Use await foreach when an IAsyncEnumerable<T> only needs one pass.
  • Batch entity changes and call SaveChangesAsync once unless each item has a deliberate commit boundary.

Review Checklist

  • Does every async request, worker, or handler path avoid synchronous EF Core database APIs?
  • Do ToListAsync, CountAsync, FirstOrDefaultAsync, SaveChangesAsync, and similar calls receive the available token?
  • Are async streams processed with await foreach instead of buffered into memory for one loop?
  • Are write loops batching changes before a single SaveChangesAsync call?
  • Are deliberate exceptions documented with narrow suppressions instead of broad analyzer disablement?

CI Severity Starter

[*.cs]

# Async EF Core execution
dotnet_diagnostic.LC008.severity = warning
dotnet_diagnostic.LC026.severity = suggestion
dotnet_diagnostic.LC043.severity = suggestion
dotnet_diagnostic.LC010.severity = warning

Use the EF Core query analyzer CI guide to promote selected async rules from local warnings to pull-request checks.