Documentation

EF Core Client-Side Evaluation Analyzer

Use LinqContraband as an EF Core client-side evaluation analyzer for local methods in IQueryable, early AsEnumerable or ToList boundaries, StringComparison overloads, column case conversion, and non-translatable GroupBy projections.

EF Core Client-Side Evaluation Analyzer

LinqContraband helps teams catch EF Core query shapes that cannot stay cleanly in SQL. It reports row-dependent local methods inside IQueryable, early AsEnumerable or ToList boundaries, provider-sensitive string comparison overloads, column case conversion, and risky GroupBy projections before they become production runtime failures or large in-memory scans.

Install the official NuGet package:

dotnet add package LinqContraband

Why Client-Side Evaluation Still Matters

Modern EF Core usually throws when a non-translatable query expression appears before the final projection. That is better than silently loading a table, but it is still late feedback: the query compiles, reaches production-shaped data, and fails or gets “fixed” by moving too much work into memory.

var users = await db.Users
    .Where(user => IsActiveAdult(user)) // LC001: local method in IQueryable
    .ToListAsync(cancellationToken);

Prefer SQL-translatable expressions, captured constants, mapped functions, or an explicit late client boundary:

var minimumBirthDate = today.AddYears(-18);

var users = await db.Users
    .Where(user => user.IsActive && user.DateOfBirth <= minimumBirthDate)
    .ToListAsync(cancellationToken);

Rules Covered By This Guide

Rule What it catches Review direction
LC001: local method in IQueryable Row-dependent source helpers inside translation-critical query lambdas. Rewrite as a translatable expression, mapped database function, computed column, or explicit late client boundary.
LC002: premature materialization ToList, ToArray, or AsEnumerable before provider-safe filtering, sorting, projection, or aggregation. Move SQL-safe query work before materialization.
LC014: string case conversion ToLower or ToUpper on column-derived values inside EF queries. Use collation, normalized columns, or provider-specific functions deliberately.
LC020: StringComparison overloads Contains, StartsWith, or EndsWith overloads with StringComparison inside query expressions. Use database collation, normalized search data, or a provider-specific API instead of .NET comparison semantics.
LC024: non-translatable GroupBy projection Grouping projections that materialize or inspect group elements instead of reducing to server aggregates. Keep GroupBy projections aggregate-only, or materialize intentionally before complex grouping.

Common Client-Evaluation Traps

Local helper inside a query

var users = db.Users
    .Where(user => CalculateAge(user.DateOfBirth) >= 18) // LC001
    .ToList();
var minimumBirthDate = now.AddYears(-18);

var users = db.Users
    .Where(user => user.DateOfBirth <= minimumBirthDate)
    .ToList();

AsEnumerable too early

var users = db.Users
    .AsEnumerable()
    .Where(user => user.IsActive) // LC002
    .ToList();
var users = db.Users
    .Where(user => user.IsActive)
    .ToList();

StringComparison inside IQueryable

var users = db.Users
    .Where(user => user.Email.Contains(term, StringComparison.OrdinalIgnoreCase)) // LC020
    .ToList();
var users = db.Users
    .Where(user => user.NormalizedEmail.Contains(normalizedTerm))
    .ToList();

Case conversion on a database column

var user = db.Users
    .FirstOrDefault(user => user.Email.ToLower() == email.ToLower()); // LC014

Prefer an indexed normalized column or a reviewed database collation strategy:

var user = db.Users
    .FirstOrDefault(user => user.NormalizedEmail == normalizedEmail);

GroupBy projection that pulls group items

var totals = db.Orders
    .GroupBy(order => order.CustomerId)
    .Select(group => new
    {
        group.Key,
        Orders = group.ToList() // LC024
    });
var totals = db.Orders
    .GroupBy(order => order.CustomerId)
    .Select(group => new
    {
        group.Key,
        Count = group.Count(),
        Total = group.Sum(order => order.Total)
    });

Safer Translation Patterns

  • Keep Where, OrderBy, Select, GroupBy, and aggregates provider-side until the final materializer.
  • Move row-independent calculations into captured locals before the query.
  • Replace row-dependent helper methods with translatable expressions, [DbFunction] methods, computed columns, or provider-supported projectables.
  • Use normalized columns or explicit database collation for case-insensitive search.
  • Keep complex client-only work behind a visible ToListAsync or AsEnumerable boundary after filtering and bounds.
  • Suppress narrowly when a small table or deliberate client boundary is an accepted trade-off.

Review Checklist

  1. Does any query lambda call a source-defined helper that depends on the row?
  2. Does AsEnumerable, ToList, or ToArray appear before filters, ordering, projection, or aggregation that SQL could handle?
  3. Are string search semantics handled by database collation or normalized data rather than StringComparison overloads?
  4. Does any query lower or upper-case a column before comparison?
  5. Do GroupBy projections reduce to aggregates instead of materializing each group?

CI Severity Starter

[*.cs]

# Client-side evaluation and translation risk
dotnet_diagnostic.LC001.severity = warning
dotnet_diagnostic.LC002.severity = warning
dotnet_diagnostic.LC014.severity = warning
dotnet_diagnostic.LC020.severity = warning
dotnet_diagnostic.LC024.severity = warning

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