Analyzer rule
LC001: Local Method Usage in IQueryable
LC001 flags your own helper methods inside EF Core IQueryable lambdas that SQL cannot translate, which forces client-side evaluation or a runtime error.
- Default severity
- Warning
- Code fix
- Available
- Category
- Performance
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC001.severity = error
LC001: Local Method Usage in IQueryable
In Plain Terms
Imagine hiring a translator to translate a book into Spanish, but you used made-up slang words they don’t know. They can’t finish the job, so they hand you the entire dictionary and say “You figure it out.” You have to read the whole dictionary just to find one word.
What It Flags
LC001 reports source-defined helper methods inside translation-critical IQueryable lambdas when the helper depends on the query row.
var users = db.Users
.Where(u => CalculateAge(u.DateOfBirth) > 18)
.ToList();
EF Core cannot translate CalculateAge unless you explicitly map it to SQL. Depending on provider/version and query shape, the query either throws at runtime or crosses into client evaluation after loading more data than intended.
Why It Matters
The expensive part is not the helper call itself. The problem is that the helper sits inside the expression tree EF Core must translate to SQL. If the provider cannot translate the method, filtering, ordering, grouping, or aggregation may happen outside the database.
That can turn a selective query into an unbounded read:
var activeAdults = db.Users
.Where(u => IsActive(u) && CalculateAge(u.DateOfBirth) >= 18)
.ToList();
Preferred Fixes
Move row-independent calculations outside the query and compare with queryable values.
var minBirthDate = now.AddYears(-18);
var users = db.Users
.Where(u => u.DateOfBirth <= minBirthDate)
.ToList();
Use a translatable expression, mapped database function, computed column, value converter, or provider-specific API when the logic must run in SQL.
var users = db.Users
.Where(u => DatabaseFunctions.IsAdult(u.DateOfBirth))
.ToList();
If you intentionally want client-side filtering, make that boundary explicit and as late as possible.
var users = db.Users
.Where(u => u.IsEnabled)
.AsEnumerable()
.Where(u => IsPreferred(u))
.ToList();
The code fix uses this last shape. It inserts AsEnumerable() at the query source for extension syntax and rewrites static Queryable syntax to the matching Enumerable call with an AsEnumerable() source argument:
var query = Queryable.Where(db.Users, u => IsPreferred(u));
becomes:
var query = System.Linq.Enumerable.Where(db.Users.AsEnumerable(), u => IsPreferred(u));
For fully qualified static calls, the fix preserves the System.Linq qualifier. For bare Queryable calls or aliases to System.Linq.Queryable, it uses System.Linq.Enumerable so a user-defined Enumerable type cannot capture the fixed call. Reordered named static calls keep their argument order while the actual query sequence parameter receives the AsEnumerable() boundary, including source: on filtering operators and outer: on Join/GroupJoin. Upstream static operators that are still translatable stay on Queryable; for example, a static Take feeding the fixed Where remains server-side and the boundary is placed after that Take. Complex source expressions are parenthesized before the boundary, so an awaited source becomes (await GetUsersAsync()).AsEnumerable() instead of calling AsEnumerable() on the task. Ordered static chains are rewritten as a chain when required, so Queryable.ThenBy(Queryable.OrderBy(...), ...) becomes System.Linq.Enumerable.ThenBy(System.Linq.Enumerable.OrderBy(...), ...) instead of erasing the ordered source before ThenBy. Static ThenBy over an extension OrderBy source similarly rewrites the extension source to AsEnumerable().OrderBy(...); if the ordered source is an opaque variable, the fixer does not offer the unsafe rewrite. Static wrappers that do not have an Enumerable counterpart, such as Queryable.AsQueryable(...), stay on Queryable while the nested operator that contains the local helper is fixed.
The fixer is conservative for nested correlated query lambdas. LC001 still reports helpers inside nested subqueries, including helpers that reference only an outer query range variable, but it does not offer a partial AsEnumerable() rewrite when the diagnostic belongs to a nested query invocation. Moving only that inner boundary can leave an Enumerable subquery embedded in the outer IQueryable predicate instead of making the intended client-evaluation boundary clear.
The fix is not offered where the query must stay an IQueryable<T> after the local method, because AsEnumerable() turns it into an IEnumerable<T> and the code would stop compiling:
- the result is assigned to an
IQueryable<T>variable, such asquery = query.Where(u => IsPreferred(u));, passed to anIQueryable<T>parameter, or returned as one; - a later call in the chain needs a query, such as
ToArrayAsync(),Include(...)or a project extension onIQueryable<T>, or passes a storedExpression<...>to aQueryableoperator; - the query is kept in a
varlocal that is later used in one of those ways.
It is also not offered when the local method works on a sub-query of the row, such as g.Items.AsQueryable().Where(...).Restrict(r).Count() inside a projection. Moving the whole outer query to the client would run that projection in memory, where navigations such as g.Items are not loaded. LC001 still reports all of these; rewrite them by hand.
Treat that fix as an explicit client-evaluation fallback, not as the best performance answer. For large tables, prefer a SQL-translatable rewrite.
Reported Query Positions
LC001 reports local row-dependent helpers in positions that EF Core normally needs to translate:
- Filters and predicates:
Where,Any,All,Count,First,Single,Last,SkipWhile,TakeWhile - Ordering:
OrderBy,OrderByDescending,ThenBy,ThenByDescending - Joins and grouping keys:
Join,GroupJoin,GroupBy - Aggregate selectors:
Sum,Average,Min,Max
It intentionally does not report top-level Select projection helpers. Modern EF Core can often run final projection code after the database query shape has already been translated.
Safe Cases
LC001 stays quiet when the helper does not depend on the query row:
var normalized = Normalize(status);
var users = db.Users.Where(u => u.Status == normalized);
It also stays quiet for methods marked as explicitly translatable:
[Microsoft.EntityFrameworkCore.DbFunction]
public static bool IsAdult(DateTime dateOfBirth) => throw new NotSupportedException();
and for methods that a query-expansion library rewrites before EF Core sees the query: EntityFrameworkCore.Projectables.ProjectableAttribute, LINQKit’s [Expandable], NeinLinq’s [InjectLambda] and DelegateDecompiler’s [Computed]. Lookalike attributes from other namespaces do not suppress the diagnostic.
A method mapped in the model with HasDbFunction is trusted the same way as one marked [DbFunction]:
modelBuilder.HasDbFunction(typeof(Rules).GetMethod(nameof(Rules.IsAdult)));
modelBuilder.HasDbFunction(() => Rules.IsAdult(default));
The MethodInfo can also come from a local or a readonly field whose initializer is that GetMethod (or GetRuntimeMethod) call, as long as nothing assigns it again:
var asText = typeof(SpatialFunctions).GetMethod(nameof(SpatialFunctions.AsText));
modelBuilder.HasDbFunction(asText!).HasName("ST_AsText");
LC001 reads these mappings from this project’s source. GetMethod with a constant name trusts every overload of that name on the type; a name only known at run time, a local that is reassigned (or passed by ref/out), a mutable field, a MethodInfo returned by a helper method or property, or a mapping in another project, is not seen.
If your project has its own translation attribute, list it in .editorconfig (full names, separated by commas, because ; and # start a comment there; the Attribute suffix is optional):
[*.cs]
dotnet_code_quality.LC001.trusted_attributes = MyCompany.Data.SqlTranslatable
Methods from EF Core, its providers and the provider plugins it knows are trusted too: the Microsoft.EntityFrameworkCore, Npgsql and NetTopologySuite namespaces, and pgvector’s Pgvector.EntityFrameworkCore distance functions, so a vector search such as db.Items.OrderBy(c => c.Embedding.CosineDistance(vector)) stays quiet. When a plugin or your own IMethodCallTranslator translates methods from another namespace, list that namespace (its child namespaces are included; MyCompany.Translators does not cover MyCompany.TranslatorsExtra):
[*.cs]
dotnet_code_quality.LC001.trusted_namespaces = MyCompany.Translators, NodaTime
Queries built over an in-memory collection run on LINQ to Objects, so LC001 stays quiet when the chain provably starts at AsQueryable() over an array or concrete collection (List<T>, HashSet<T>, …), over a value typed as ICollection<T>, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ISet<T> or IReadOnlySet<T> (no EF query type implements them), over the result of a System.Linq.Enumerable operator such as Select or Where whose every input sequence, and every sequence a SelectMany collection selector returns, is itself in memory, or at new EnumerableQuery<T>(...). This is the shape unit tests, in-memory repositories and MockQueryable-style fakes use:
var users = new List<User> { ... }.AsQueryable().BuildMock();
var adults = users.Where(u => IsAdult(u)); // no LC001: LINQ to Objects
A source the analyzer cannot prove in-memory still reports: an IQueryable parameter, field or property, a DbSet, AsQueryable() over an IEnumerable<T> (which may be a DbSet at runtime) or over an Enumerable operator whose source is not proven in memory (operators are lazy, so db.Users.AsEnumerable().Where(...).AsQueryable() still enumerates the EF query), or a local that is assigned more than once.
LC001 only reports a method that ends up inside an expression tree. A method called in a delegate lambda that runs before the query is built, such as SimpleIdServer’s _representations.Select(r => Enrich(r)).AsQueryable().Where(...) (Enumerable.Select takes a Func), is ordinary code and stays quiet. A delegate lambda nested inside a query lambda (db.Users.Where(u => u.Tags.Any(t => Matches(t, u.Name)))) is part of the expression tree and still reports.
Scope
LC001 is local to the current lambda and query invocation. It does not prove whole-program SQL translation, inspect provider model configuration in another assembly, or decide whether client evaluation is acceptable for a small table. When client evaluation is intentional, keep the AsEnumerable() boundary obvious and document the reason in the calling code.