Documentation

EF Core CancellationToken Analyzer

Use LinqContraband as an EF Core missing CancellationToken analyzer for ToListAsync, FirstOrDefaultAsync, SaveChangesAsync, and other async database calls.

EF Core CancellationToken Analyzer

LinqContraband is an EF Core CancellationToken analyzer for .NET teams that want async database calls to respect request cancellation and worker shutdown. It flags ToListAsync, FirstOrDefaultAsync, SaveChangesAsync, and other EF Core async calls when a usable token is in scope but the call ignores it.

Install the official analyzer package:

dotnet add package LinqContraband

Why Missing CancellationToken Matters

Async EF Core code can still waste database and connection-pool resources after the caller has gone away. A cancelled HTTP request, abandoned background job, or stopping hosted service should not leave expensive query work running just because the token was dropped at the repository boundary.

The missing-token shape looks like this:

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

The safer shape passes the operation token to the EF Core async API:

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

If the broader issue is a synchronous EF Core API inside an async method, use the EF Core async query analyzer guide as the starting point.

Rule Covered By This Guide

Rule What it detects Review direction
LC026: missing CancellationToken EF Core async calls that omit a token, pass default, or pass CancellationToken.None while a usable token is in local scope. Pass the operation token through the query, save, find, and bulk-write API unless intentionally decoupling the database work from caller cancellation.

Common EF Core CancellationToken Problems

ToListAsync without the request token

public async Task<IReadOnlyList<Order>> RecentOrders(DateTime cutoff, CancellationToken cancellationToken)
{
    return await db.Orders
        .Where(order => order.CreatedAt >= cutoff)
        .ToListAsync(); // LC026
}
public async Task<IReadOnlyList<Order>> RecentOrders(DateTime cutoff, CancellationToken cancellationToken)
{
    return await db.Orders
        .Where(order => order.CreatedAt >= cutoff)
        .ToListAsync(cancellationToken);
}

Predicate overloads missing the final token argument

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

SaveChangesAsync ignoring cancellation

public async Task RenameUser(User user, string name, CancellationToken cancellationToken)
{
    user.Name = name;
    await db.SaveChangesAsync(); // LC026
}
public async Task RenameUser(User user, string name, CancellationToken cancellationToken)
{
    user.Name = name;
    await db.SaveChangesAsync(cancellationToken);
}

Explicit ignored token values

await db.Users.ToListAsync(default); // LC026
await db.Users.ToListAsync(CancellationToken.None); // LC026
await db.Users.ToListAsync(cancellationToken: default); // LC026
await db.Users.ToListAsync(cancellationToken);
await db.Users.ToListAsync(cancellationToken: cancellationToken);

Token Selection

LC026 only reports when a usable CancellationToken is already available at the invocation site. The code fix chooses:

  1. A token named cancellationToken.
  2. A token named ct.
  3. The first available token the compiler exposes at that location.

Eligible tokens can come from method parameters, lambda parameters, locals, fields, or readable properties.

private CancellationToken RequestAborted { get; }

public async Task<List<User>> LoadUsers()
{
    return await db.Users.ToListAsync(RequestAborted);
}

When several domain-specific tokens are in scope, LinqContraband keeps the rule local and conservative. Rename the intended token to cancellationToken or ct, pass the chosen token manually, or suppress the diagnostic when the query should deliberately outlive the caller.

Boundaries

LC026 does not create a new token source and does not report when no token is available in scope. A new CancellationTokenSource at the call site would not connect the database operation to the caller’s cancellation boundary.

The rule is intentionally limited to EF Core async APIs with a CancellationToken parameter. It does not trace tokens through service abstractions or decide whether a field such as shutdownToken, requestAborted, or jobToken represents the right business boundary.

CI Severity Starter

Start as a suggestion while existing async paths are cleaned up, then promote to warning when request and worker handlers are expected to pass tokens consistently:

[*.cs]

# Async EF Core cancellation
dotnet_diagnostic.LC026.severity = suggestion

# Related async execution rules
dotnet_diagnostic.LC008.severity = warning
dotnet_diagnostic.LC043.severity = suggestion

Use the EF Core query analyzer CI guide when missing-token diagnostics should show up on every pull request.