LinqContraband 5.14.0
See the version list below for details.
dotnet add package LinqContraband --version 5.14.0
NuGet\Install-Package LinqContraband -Version 5.14.0
<PackageReference Include="LinqContraband" Version="5.14.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="LinqContraband" Version="5.14.0" />
<PackageReference Include="LinqContraband"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add LinqContraband --version 5.14.0
#r "nuget: LinqContraband, 5.14.0"
#:package LinqContraband@5.14.0
#addin nuget:?package=LinqContraband&version=5.14.0
#tool nuget:?package=LinqContraband&version=5.14.0
LinqContraband
<div align="center">
![]()
Stop Smuggling Bad Queries into Production
</div>
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.
The problem
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.
What it catches
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.
Try it on your code first
Scan a solution without changing it (.NET 10 SDK):
dnx LinqContraband.Scan
The 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.
Install
dotnet add package LinqContraband
That 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
A taste
// 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.
See it work
Product-flow diagrams from real sample diagnostics and shipped LC message formats:
1. Build / IDE diagnostics (EF Core LINQ)
2. Before / after code fix (premature materialization)
3. Product loop — analyzer in IDE and CI
30-second path
- 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).
Feature snapshot
| 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. |
Compatibility
- .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
Rules
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
Configuration
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 = 5
Advisory 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.
Run it in CI
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-restore
Use 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.
Contributing
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
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.15.0 | 0 | 9/25/2026 |
| 5.14.0 | 0 | 9/25/2026 |
| 5.13.0 | 106 | 9/25/2026 |
| 5.12.0 | 41 | 9/24/2026 |
| 5.11.0 | 42 | 9/24/2026 |
| 5.10.0 | 170 | 9/23/2026 |
| 5.8.1 | 98 | 9/22/2026 |
| 5.8.0 | 1,522 | 9/10/2026 |
| 5.7.65 | 9,633 | 8/17/2026 |
| 5.7.64 | 116 | 8/17/2026 |
| 5.7.63 | 107 | 8/17/2026 |
| 5.7.62 | 114 | 8/17/2026 |
| 5.7.61 | 111 | 8/16/2026 |
| 5.7.60 | 111 | 8/16/2026 |
| 5.7.59 | 122 | 8/13/2026 |
| 5.7.58 | 111 | 8/10/2026 |
| 5.7.57 | 113 | 8/10/2026 |
| 5.7.56 | 113 | 8/10/2026 |
| 5.7.55 | 117 | 8/10/2026 |
| 5.7.54 | 110 | 8/10/2026 |
LC009 is quieter on real code: it no longer reports queries whose entities the method returns to its caller (set dotnet_code_quality.LC009.report_returned_entities = true to report them again), nor a Join or GroupJoin that projects only scalars, and it sees writes through a ?? new fallback; on Kavita its reports fall from 157 to 34. Code-fix safety fixes from applying the fixes to real applications: LC009 withholds AsNoTracking() when entities reached through a navigation are passed on or stored; LC002 no longer moves filters on computed or [NotMapped] properties into SQL; fixes that add a using (LC001, LC004, LC006, LC007, LC008, LC009, LC012, LC015, LC017, LC032, LC033, LC041, LC042, LC045, LC047 and LC051) skip one a global using already provides and place it below a copyright header; LC001, LC006, LC015 and LC017 keep the inserted call on the source line in multi-line queries; LC043 keeps comments above the removed declaration; LC042 no longer duplicates a #pragma; LC026 no longer passes an outer token into a static lambda or local function; LC008 is not offered inside a lock or unsafe context.