Compile-time EF Core LINQ performance analyzer for .NET — a high-signal Roslyn analyzer that catches N+1 queries, client-side evaluation, premature materialization, sync-over-async, missing AsNoTracking, raw SQL injection risks, and other DbContext query issues in the editor and CI—not production.
Entity Framework Core and LINQ compile cleanly even when the query shape will hurt production. An N+1 Find inside a loop, ToList() before Where, a local method that forces client-side evaluation, sync-over-async on DbContext, or FromSqlRaw($"...") only fail under load, at 3 AM, or as a security incident.
Runtime profilers and code review miss what static analysis can prove from your IQueryable chains and EF Core API usage.
LinqContraband reports proven EF Core LINQ and DbContext pitfalls early:
- N+1 database execution inside loops (
Find, materializers, explicit load) - premature materialization (
ToList/AsEnumerablebefore filters) - client-side evaluation risk from non-translatable local methods
- sync-over-async EF Core calls in async methods
- missing or misused AsNoTracking (tracking tax, silent writes, mixed modes)
- Cartesian explosion and missing/excessive Include paths
- SaveChanges inside loops and nested SaveChanges
- raw SQL injection patterns (
FromSqlRaw/ExecuteSqlRawinterpolation) - DbContext lifetime and concurrent same-context operations
- unbounded materialization, projection waste, and pagination without OrderBy
When the analyzer cannot prove an EF-backed query shape statically, it stays quiet. High-signal feedback, not noisy guesses.
Scan a solution without changing it (.NET 10 SDK):
dnx LinqContraband.ScanThe scanner builds the solution in the current directory with the analyzers injected. It then prints which rules fired, how often, and in which files, and writes a SARIF file you can upload to GitHub code scanning. On .NET 8 or 9, install it once with dotnet tool install -g LinqContraband.Scan and run linqcontraband-scan.
dotnet add package LinqContrabandThat adds the latest release. To edit the project file by hand instead, use the version shown on the NuGet badge:
<PackageReference Include="LinqContraband" Version="x.y.z" PrivateAssets="all" />No runtime dependency is added to your app. LinqContraband runs as a Roslyn analyzer during build and in supported IDEs (Visual Studio, Rider, VS Code / C# Dev Kit) and CI.
Install only from NuGet or from this repository. LinqContraband ships as the LinqContraband analyzer package and the LinqContraband.Scan .NET tool, both on NuGet. It is not distributed as a standalone ZIP installer or executable; treat third-party ZIP downloads as untrusted.
- Official package: nuget.org/packages/LinqContraband
- Canonical source: github.com/georgepwall1991/LinqContraband
- Documentation: georgepwall1991.github.io/LinqContraband
// LC002: ToList() pulls every order into memory, then filters in C#.
var slow = db.Orders.ToList().Where(o => o.DueDate < today);
// Fix (offered as a code fix): filter in SQL, then materialize.
var fast = db.Orders.Where(o => o.DueDate < today).ToList();// LC007: one database round trip per customer (N+1).
foreach (var id in customerIds)
customers.Add(db.Customers.Find(id));
// Fix: one query for the whole set.
customers = db.Customers.Where(c => customerIds.Contains(c.Id)).ToList();Every diagnostic's help link in your IDE opens the rule's page, which shows the problem, the fix, and the cases the rule deliberately leaves alone.
Product-flow diagrams from real sample diagnostics and shipped LC message formats:
- Reference the package with
PrivateAssets="all". - Keep writing EF Core LINQ as usual (
DbSet,IQueryable,Include,SaveChanges). - Build as usual, in the IDE or with
dotnet build. The analyzers run as part of the compiler. - Fix any
LC00xwarnings (many have code fixes). - Optionally promote critical rules to error in
.editorconfig(see Configuration below).
| Area | What LinqContraband does |
|---|---|
| N+1 queries | Flags database execution inside loops and SaveChanges-in-loop write amplification. |
| Materialization | Catches premature ToList/AsEnumerable and redundant second materializers. |
| Translation | Reports local methods and non-translatable string/date patterns that risk client-side evaluation. |
| Tracking | Guides AsNoTracking, silent-write, and mixed tracking-mode hazards. |
| Loading | Detects Cartesian explosion, missing Include, deep ThenInclude, excessive eager loading. |
| Async | Sync-over-async, missing CancellationToken, async stream buffering, concurrent DbContext use. |
| Raw SQL | Interpolated FromSqlRaw/ExecuteSqlRaw and constructed SQL string risks. |
| Modeling | Missing primary keys and explicit foreign-key properties when statically provable. |
- .NET / Roslyn hosts: Visual Studio, Rider, VS Code (C# Dev Kit), and
dotnet build/ CI - EF Core: Modern Entity Framework Core versions used with C#
IQueryable/DbContextAPIs - Package kind: Development dependency analyzer (
PrivateAssets="all"); no app runtime package
56 rules, 39 with automatic code fixes. Each rule links to its full page: what it flags, why it matters, how to fix it, and where it deliberately stays quiet.
| Rule | What it catches | Default severity | Code fix |
|---|---|---|---|
| LC001 | Client-side evaluation risk: Local method usage in IQueryable | Warning | Yes |
| LC002 | Premature query continuation after materialization | Warning | Yes |
| LC003 | Prefer Any() over Count() existence checks | Warning | Yes |
| LC004 | Deferred Execution Leak: IQueryable passed as IEnumerable | Warning | Yes |
| LC005 | Multiple OrderBy calls | Warning | Yes |
| LC006 | Cartesian Explosion Risk: Multiple Collection Includes | Warning | Yes |
| LC007 | N+1 Problem: Database execution inside loop | Warning | Yes |
| LC008 | Sync-over-Async: Synchronous EF Core method in Async context | Warning | Yes |
| LC009 | Performance: Missing AsNoTracking() in Read-Only path | Info | Yes |
| LC010 | N+1 Write Problem: SaveChanges inside loop | Warning | Yes |
| LC011 | Design: Entity missing Primary Key | Warning | Yes |
| LC012 | Optimize: Use ExecuteDelete() instead of RemoveRange() | Warning | Yes |
| LC013 | Disposed Context Query | Warning | Manual |
| LC014 | Avoid String.ToLower() or ToUpper() in LINQ queries | Warning | Manual |
| LC015 | Deterministic Pagination: OrderBy required before Skip/Take | Warning | Yes |
| LC016 | Avoid DateTime.Now/UtcNow in LINQ queries | Warning | Yes |
| LC017 | Performance: Consider using Select() projection | Info | Yes |
| LC018 | Avoid FromSqlRaw with interpolated strings | Warning | Yes |
| LC019 | Conditional Include Expression | Warning | Manual |
| LC020 | Avoid untranslatable string comparison overloads | Warning | Yes |
| LC021 | Avoid IgnoreQueryFilters | Warning | Yes |
| LC022 | Nested collection materialization inside projection | Info | Yes |
| LC023 | Use Find/FindAsync for primary key lookups | Info | Yes |
| LC024 | GroupBy with Non-Translatable Projection | Warning | Manual |
| LC025 | Avoid AsNoTracking with Update/Remove | Warning | Yes |
| LC026 | Missing CancellationToken in async call | Info | Yes |
| LC027 | Missing Explicit Foreign Key Property | Info | Yes |
| LC028 | Deep ThenInclude Chain | Warning | Manual |
| LC029 | Redundant identity Select | Info | Yes |
| LC030 | Potential DbContext lifetime mismatch | Info | Manual |
| LC031 | Unbounded Query Materialization | Info | Manual |
| LC032 | Use ExecuteUpdate for provable bulk scalar updates | Info | Yes |
| LC033 | Use FrozenSet for provably read-only membership caches | Info | Yes |
| LC034 | Avoid ExecuteSqlRaw with interpolated strings | Warning | Yes |
| LC035 | Missing Where before bulk execute | Info | Manual |
| LC036 | DbContext captured by thread work item | Warning | Manual |
| LC037 | Avoid constructed raw SQL strings | Warning | Manual |
| LC038 | Avoid excessive eager loading | Info | Manual |
| LC039 | Avoid repeated SaveChanges on the same context | Info | Manual |
| LC040 | Avoid mixing tracking modes on the same context | Info | Manual |
| LC041 | Single entity query over-fetches one consumed property | Info | Yes |
| LC042 | Complex query should be tagged | Info | Yes |
| LC043 | Prefer await foreach over buffering async streams | Info | Yes |
| LC044 | AsNoTracking query mutated then SaveChanges — silent data loss | Warning | Manual |
| LC045 | Missing Include: navigation accessed on materialized entity | Warning | Yes |
| LC046 | Concurrent EF Core operations on the same DbContext | Warning | Manual |
| LC047 | ExecuteDelete bypasses the tracked delete pipeline | Warning | Yes |
| LC048 | Tracked update can overwrite a concurrent change | Warning | Manual |
| LC049 | Include is ignored by a Select projection | Info | Yes |
| LC050 | OrderBy before Distinct is discarded | Warning | Yes |
| LC051 | ToAsyncEnumerable() runs an EF Core query synchronously | Warning | Yes |
| LC052 | Model data uses a value that changes on every run | Warning | Manual |
| LC053 | Global query filter silently replaced by another HasQueryFilter | Warning | Yes |
| LC054 | Migrate called inside a user transaction | Warning | Yes |
| LC055 | OnModelCreating override skips the base configuration | Warning | Yes |
| LC056 | LINQ composed over a stored procedure call | Warning | Yes |
Browse the same rules grouped by failure mode in the rule catalog, or start from a topic guide:
- N+1 query detector and SaveChanges in loops
- Client-side evaluation and premature materialization
- Include and eager loading and projection
- AsNoTracking and change tracking and DbContext lifetime
- Async queries and CancellationToken
- Raw SQL injection, ExecuteUpdate, and pagination OrderBy
- EF Core query performance checklist
Pick a preset with one line in your project file (or Directory.Build.props):
<PropertyGroup>
<LinqContrabandPreset>security</LinqContrabandPreset>
</PropertyGroup>| Preset | What it does |
|---|---|
security |
SQL injection rules (LC018, LC034, LC037, plus EF Core's own EF1002 and EF1003, which LC018 and LC034 defer to) fail the build. |
critical |
security plus the runtime-failure and silent data-loss rules (LC013, LC019, LC036, LC044, LC046, LC047, LC048, LC054, LC055, LC056) fail the build. |
strict |
Every warning rule fails the build and every advisory rule becomes a warning. |
essentials |
Advisory (Info) rules are turned off; warning rules keep their defaults. |
Combine presets with ;, for example security;essentials. Your own .editorconfig entries still win over a preset.
Or set any rule's severity yourself in .editorconfig:
[*.cs]
dotnet_diagnostic.LC001.severity = error
dotnet_diagnostic.LC002.severity = error
dotnet_diagnostic.LC003.severity = warning
# Optional rule-specific thresholds (defaults: 4 and 3)
dotnet_code_quality.LC038.include_threshold = 6
dotnet_code_quality.LC042.query_operator_threshold = 5Advisory rules default to Info, so they show up as IDE hints without drowning out the higher-confidence warnings. dotnet build does not print Info diagnostics; to see them on the command line, use the strict preset (it raises them to warnings) or write a SARIF log with -p:ErrorLog=linqcontraband.sarif.
LinqContraband also hides the .NET SDK's culture and string-comparison warnings (CA1862, CA1304, CA1305, CA1307, CA1309, CA1310, CA1311) inside EF Core query lambdas, where their suggested StringComparison or CultureInfo overloads cannot be translated to SQL and would make the query throw. In-memory LINQ keeps them. To keep one inside queries as well, add its suppression id (LCS plus the CA number, such as LCS1862) to <NoWarn>; see LC014.
LinqContraband runs inside the normal dotnet build, so a CI job needs no database, service container, or extra tool:
- run: dotnet restore
- run: dotnet build --configuration Release --no-restoreUse the security or critical preset (see Configuration) to block pull requests on the rules that matter most, or
promote individual rules to error in .editorconfig. The
CI guide covers a gradual rollout.
Found a new way to smuggle bad queries? Open an issue or send a pull request. The contributing guide explains how to add or change a rule.
License: MIT