Repository navigation
Releases: vuglll/EFDoctor
Release list
EFDoctor 0.6.0
Added
- Code fixes in the
EFDoctor.Analyzerspackage. The IDE offers a quick action for a rule's mechanical remediation. The first is EFD029's: replace the discardingOrderBywithThenBy, orOrderByDescendingwithThenByDescending, offered when the receiver is already ordered so the result compiles. EFD002's fix replaces a count compared for existence withAny, or its negation. EFD019's fix drops the materialization and reduces in the query, in async form when it was awaited. EFD010's fix awaits the async counterpart, adds the EF Core using directive when needed, and passes aCancellationTokenin scope. The fixes ship inEFDoctor.CodeFixes.dllbeside the analyzers, and the command-line build ignores them. - EFD015: a
Containsfilter whose list was loaded from another unbounded query on the sameDbContextonly for that filter, such asvar ids = db.Customers.Select(c => c.Id).ToList(); db.Orders.Where(o => ids.Contains(o.CustomerId)). Keeping the first query as anIQueryablecomposes one statement with a subquery. Reported with advisory confidence atInfoseverity, under thePerformancecategory. Bounded inner queries, lists used for anything else, and lists from another context are not reported. - EFD016: an EF Core async call (query terminal, bulk operation,
SaveChangesAsync, orFindAsync) that leaves out itsCancellationTokenwhile aCancellationTokenparameter is in scope, so cancelling the request doesn't cancel the database command. Reported with advisory confidence atInfoseverity, under theReliabilitycategory. An explicitCancellationToken.Noneordefaultis not reported. - EFD024: an
Includeof a collection navigation whose every use in the method, after the query is materialized, is filtered in memory, such asInclude(b => b.Posts)followed only byblog.Posts.Where(p => p.Published). A filteredIncludewould apply the filter in SQL. Reported with advisory confidence atInfoseverity, under thePerformancecategory. Navigations also used in full, and entities that leave the method, are not reported. - EFD026:
Update(entity)on an entity the same method loaded with tracking from the sameDbContext, such asvar order = db.Orders.First(…); db.Update(order);. The change tracker already detects the change, andUpdatemarks every column modified, soSaveChangeswrites the full row. Reported with advisory confidence atInfoseverity, under thePerformancecategory. Detached, untracked, and projected entities are not reported. - EFD031: a LINQ operator composed over
FromSqlRaw,FromSql, orFromSqlInterpolatedwhose SQL starts withEXEC,EXECUTE, orCALL, such asFromSqlRaw("EXEC dbo.GetOrders").Where(…). EF Core can't wrap a stored-procedure call in a subquery, so the query throws at runtime. Reported atWarningseverity with high confidence, under theCorrectnesscategory. Materializing the result first, and composable SQL such as a table-valued function, are not reported. - EFD032: a change to an entity loaded with
AsNoTracking()followed bySaveChangeson the sameDbContext, with nothing in between attaching the entity. The context doesn't track the entity, soSaveChangessilently saves nothing. Reported atWarningseverity with high confidence, under theCorrectnesscategory, for a single entity or the elements of a list changed in aforeach. Any other use of the entity between the load and the save, such asUpdate,Attach, or passing it to a helper, means it may be attached, and is not reported. - EFD033: a singleton or hosted-service registration (
AddSingleton,TryAddSingleton,AddHostedService) whose implementation takes aDbContextin every public constructor, or whose factory resolves one from the root provider. The scoped context is then shared for the application's lifetime. Reported atWarningseverity with high confidence, under theReliabilitycategory.IDbContextFactory<T>,IServiceScopeFactory, and contexts registered as singletons are not reported. - EFD034:
Database.EnsureCreated()orEnsureCreatedAsync()on aDbContexttype that the same project manages with migrations: aMigrationorModelSnapshotclass for it, or aMigratecall on it.EnsureCreatedrecords no migration history, so a laterMigratefails, or new migrations are never applied. Reported atWarningseverity with high confidence, under theCorrectnesscategory, when analysis of the project completes. Contexts without migrations, and provider switches that callMigratein the other branch, are not reported. - EFD035: two SQL Server column types in the model snapshot. An
nvarchar(max)orvarchar(max)column used in an index or key is reported with high confidence, because SQL Server can't index it and the migration fails. Adecimalleft at the defaultdecimal(18,2), with noHasPrecision,[Precision], or[Column(TypeName)], is reported with advisory confidence, because values beyond two decimal places are silently rounded. CategoryCorrectness. - EFD036: a query filter whose columns lead no index or key in the model snapshot of the same project, for example
Where(o => o.Email == email)when no index starts withEmail. The database may have to scan the table. Reported with advisory confidence atInfoseverity, under thePerformancecategory, for equality, range,StartsWith, andContainsfilters in the predicate's top-level&&terms. A predicate with any indexed filter column is not reported, and neither is an entity without a snapshot in the project.
Fixed
- EFD024 doesn't report a navigation that the method filters with complementary predicates, such as
SeriesId == nullandSeriesId != null, oreand!e: together they keep every row, so a filteredIncludecan't narrow it. - EFD036 combines the filters of one query.
Where(s => s.SeriesId == id).AnyAsync(s => s.ValidUntilUtc >= now)is no longer reported whenSeriesIdleads an index, and a query with two unindexedWherecalls is reported once, not once per call. - EFD001 doesn't treat a
foreachover an option type (Option<T>,Maybe<T>, orOptional<T>, as in LanguageExt) as a loop, since it runs at most once. A save inside one is reported only when a real loop encloses it. On the validation corpus this removes 23 false positives in ErsatzTV. - EFD006 doesn't report when the context splits queries by default with
UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery), configured in the same project or, for theefdoctorCLI, in any project it scans. A.globalconfigcan setefdoctor.split_query_by_default = truefor builds of a data project configured elsewhere. On the validation corpus this removes 65 false positives in ErsatzTV and Kavita. - The CLI no longer warns that EF Core types could not be resolved for an entity-model project that references only
Microsoft.EntityFrameworkCore.Abstractions, for mapping attributes such as[Index], and declares noDbContext. - Test-project detection keys on a test framework's core assembly (
xunit.core,xunit.v3.core,nunit.framework, the Visual Studio test framework, orMicrosoft.NET.Test.Sdk). A production project that references only an assertion library such asxunit.assert, directly or through another project, was classified as a test project and silently skipped.
EFDoctor 0.5.0
Two new rules that catch runtime exceptions, for 26 in all, and wider coverage for the existing rules: queries composed in branches, queries that start with AsQueryable(), and SqlQueryRaw.
Added
-
EFD028: a method of the application called inside a filter or ordering of an EF Core query (
Where,OrderBy,ThenBy, or a predicate terminal such asAnyorCountAsync), when the call reads the query's lambda parameter. EF Core can't translate it, and the query throws "could not be translated" when it runs. Reported atWarningseverity with high confidence, under theCorrectnesscategory. A call that reads only captured values is evaluated once as a query parameter, and is not reported. Methods mapped as database functions ([DbFunction],HasDbFunction, static methods on theDbContext, throw-only stubs) are not reported, nor are methods marked by EntityFrameworkCore.Projectables, NeinLinq, DelegateDecompiler, or LinqKit. Projections are not checked. -
EFD030: a deferred EF Core query returned from inside the
usingscope that disposes itsDbContext, or the service scope it was resolved from. The caller's enumeration throwsObjectDisposedException. Reported atWarningseverity with high confidence, under theReliabilitycategory. Materialized results, contexts that outlive the method, and iterator methods are not reported.
Changed
-
EFD012 now also reports dynamically built SQL passed to EF Core 8's
Database.SqlQueryRaw<T>, as it does forFromSqlRawandExecuteSqlRaw.Database.SqlQuery<T>with an interpolated string is safe, and is not reported. -
EFD004, EFD009, EFD010, EFD013, EFD019, EFD022, and EFD023 now follow a query local that is composed conditionally, the most common way queries are built from optional filters:
var query = db.Products.AsQueryable(); if (name != null) query = query.Where(p => p.Name.ToLower() == name); return query.ToList();
Such a local is followed when it is declared from a proven EF Core query, and every later write recomposes the local itself with element-preserving operators:
Where, ordering,Skip,Take,Distinct, or EF composition such asInclude. The writes may be in branches, loops,switchsections, or lambdas. These rules need only the query's origin, which such writes can't change. A predicate inside the branch is also analyzed now.- Evidence names the hop as
local 'query' (conditionally composed). - A branch that replaces the local with a different query, or recomposes it with
Select,Concat, orUnion, still ends the proof. - Rules that report something missing, such as EFD005's bound, EFD014's ordering, or EFD037's include, don't follow these locals, because a branch could add what's missing.
- Evidence names the hop as
-
context.Products.AsQueryable()is now analyzed likecontext.Productsby every rule. It calls EF Core's ownDbSet<T>.AsQueryable()method, which the query-chain proof didn't recognize, so queries that started with it were skipped.
EFDoctor 0.4.0
The rules now ship in two packages: the EFDoctor tool, and the new EFDoctor.Analyzers package for builds and the IDE. There are two new rules, for 24 in all, and EFD002 and EFD019 follow a result stored in a local.
Thanks to the people whose suggestions shaped this release:
- @jnyrup proposed the EFD002 and EFD019 changes and supplied the test cases for them (#13).
- u/anonuser1511 on r/dotnet asked for the analyzer package (#12).
- u/geesuth on r/dotnet suggested EFD037 (#10).
- u/Lonely-Unit6754 on r/dotnet suggested EFD038 (#11).
Added
- The
EFDoctor.AnalyzersNuGet package, which runs every rule inside the compiler: in each build, and in the IDE. Reference it from one project, or once for all projects through central package management. It needs the .NET 8 SDK or later, has no dependencies, and has the same version as the tool.- In a build, only high-confidence findings are warnings. Medium-confidence and advisory findings are suggestions, so adopting the package can't fail a warnings-as-errors build on a judgment call.
.editorconfigchanges any rule's severity. - Test projects are skipped, as in the CLI. Set the MSBuild property
EFDoctorAnalyzeTestProjectstotrueto analyze one.
- In a build, only high-confidence findings are warnings. Medium-confidence and advisory findings are suggestions, so adopting the package can't fail a warnings-as-errors build on a judgment call.
- Every rule's diagnostic has a help link to its page in
docs/rules/. - EFD037: an EF Core query that materializes complete entities into a local when the method reads only a few of their scalar properties, where a
Selectprojection in the query would fetch only those columns and track nothing. Advisory, atInfoseverity, under thePerformancecategory. It reports only when every use of the result in the method is a scalar read (foreach,Select, lambda reducers, counts, and index reads), at most half of the entity's scalar properties are read, and at least four are left unread. Entities that are returned, passed on, stored, or modified are never reported. - EFD038: an
ExecuteUpdate,ExecuteDelete, or async form that runs after the same method loaded entities of the same type, with tracking, into a local from the sameDbContextinstance. Bulk operations bypass the change tracker, so those entities keep their old values. Reported under theCorrectnesscategory: with high confidence atWarningseverity when the method then uses the loaded entities, loads the type again from the same context, or saves after modifying one of them, and with medium confidence otherwise. The same instance is proven by symbol, as in EFD027. Not reported afterChangeTracker.Clear(),Entry(…).Reload(), or a detach, for a no-tracking load, or when the two filters compare the same property with different constants.
Changed
- EFD019 now reports a materialized result stored in a local when the method uses the local only for its count: every read is
Count,Length,Any(),Count(), orLongCount(), and the local is never enumerated, indexed, passed, returned, or reassigned. For example,var products = db.Products.ToList(); return products.Count > 0;. EFD005 no longer reports those materializers, because EFD019 does. - EFD002's title is now "Query result used only to test existence", because the rule also covers an entity loaded only for a null check. It was "Count used only to test existence". The rule ID, messages, and severities of existing findings are unchanged; the
ruleTitlein console and JSON reports changes. - EFD002 now follows a count through a local:
var n = db.Products.Count(); return n > 0;is reported when every read of the local is an existence comparison. A local that is also used as a number is not reported. - EFD002 now reports
FirstOrDefaultand awaitedFirstOrDefaultAsyncon an entity query with no projection when the result is only checked fornull, directly or through a local. These findings have medium confidence, because the saving is at most one row's columns and change tracking. Projected queries,SingleOrDefault, and results that are used after the check are not reported. - EFD019 recommends
Any/AnyAsync, or its negation, when a count is only compared for existence, such asToList().Count > 0. Before, it recommendedCount, and following that advice produced a shape that EFD002 reports. - The default diagnostic severity of EFD006, EFD009, EFD010, EFD013, and EFD020 is now
Infoinstead ofWarning, because their findings have medium confidence. CLI reports are unchanged: these findings are still reported atWarningseverity. - The analyzers compile against Roslyn 4.8 instead of 5.9.0.
EFDoctor 0.3.0
Two new rules, both from the roadmap's "Next" tier, for 22 in all.
Added
- EFD027: two EF Core operations running at once on the same
DbContextinstance, which makes EF Core throw "A second operation was started on this context instance" depending on timing. Reported atWarningseverity with high confidence under theReliabilitycategory, for operations passed together toTask.WhenAll/Task.WhenAny, an operation started while an earlier task local is still pending, and operations projected bySelectover a captured context into a task combinator. The same instance is proven by symbol, so separate contexts fromIDbContextFactoryare never reported. - EFD029: a second
OrderByorOrderByDescendingthat discards an earlier ordering in the same query, whereThenBywas meant. EF Core translates only the lastOrderBy, so the query sorts by the replacing key alone. Reported atWarningseverity with high confidence under theCorrectnesscategory. A second ordering afterSkip,Take,Distinct, or a projection, and a default ordering overridden through a local, are not reported.
EFDoctor 0.2.0
The first release built from the public repository, and the first stable 0.x release: install it with dotnet tool install --global EFDoctor, without --prerelease. Rules, options, and output may still change before 1.0; see the roadmap.
Changed
- The tool package includes repository and project URLs, with Source Link, when it is built with the
EFDoctorRepositoryUrlproperty set. By default it still includes none. - Queries stored in a local variable are now analyzed. Every rule that proves an EF Core query chain follows a local whose value is statically determined, and analyzes
var query = …; query = query.OrderBy(…); query.ToList()like the equivalent inline query. The affected rules are EFD004, EFD005, EFD006, EFD009, EFD010, EFD013, EFD014, EFD017, EFD019, EFD020, EFD022, EFD023, and EFD025. Evidence names the followed local. A local reassigned inside anif, a loop, or a lambda is not followed. EFD006 and EFD025 still report each finding once when includes are split across a local. - In JSON mode, standard output now holds only the error envelope when no installed SDK satisfies the target's
global.json. Before, the .NET host appended the installed-SDK list to standard output. When aglobal.jsongoverns the target, EFDoctor now checks SDK resolution first by runningdotnet --versionin the target's directory, with its output captured. - The CLI now analyzes a multi-targeted project whose
TargetFrameworkslist is written across several lines, as in OpenIddict. Before, Roslyn's MSBuildWorkspace passed each untrimmed entry as the target framework, the design-time build failed, and the project couldn't be analyzed. EFDoctor normalizes the list during loading through a targets file that ships with the tool, and never modifies the analyzed repository. - The CLI no longer reports a misleading clean result when EF Core types can't be resolved. A project that uses EF Core but can't resolve
DbContext, because it isn't restored or its design-time build failed, gets anEFDoctor warningon standard error. If no EF Core project can be analyzed, the run exits with2. - Queries that use
AsTracking(),IgnoreQueryFilters(), orIgnoreAutoIncludes()are now analyzed. Before, these methods broke the inline query-chain proof, so EFD004, EFD005, EFD009, EFD010, EFD013, EFD014, EFD019, EFD020, EFD022, and EFD023 silently skipped every query that used them. - EFD006 no longer reports a query that calls
AsSingleQuery()explicitly. As withAsSplitQuery(), it records a deliberate loading choice, and EF Core itself stays silent in that case. - EFD010 no longer reports the synchronous arm of an explicit sync/async switch, such as
async ? await q.FirstOrDefaultAsync(p) : q.FirstOrDefault(p), or anif/elsewhose other branch awaits the same EF counterpart. - EFD020 now counts only real re-executions. A use inside another query's expression-tree lambda (for example
Where(x => ids.Contains(x.Id))) and a predicate terminal (for exampleAny(x => …)) no longer count as executions, and executions in the two arms of the same conditional count as one path. - EFD001 no longer reports a save that runs once per batch. That covers a loop over chunks (
Chunk(n)), a pager loop whose condition declares the page, a loop that loads a page and iterates it before saving, and a threshold flush such asif (processed >= BatchSize). A per-item save, including one nested inside a batch loop, is still reported. - EFD005 proves keys from fluent configuration in the analyzed project (single-column
HasKey,HasAlternateKey,HasForeignKey, andHasIndex(...).IsUnique()), and from EF Core's<Navigation>Idforeign-key convention. Queries filtered by such a key, for example loading the children of one known parent, are no longer reported. - EFD005 reports a query whose only bound is equality on a key recognized by name (
Id,<Entity>Id,*Id) at Advisory confidence andInfoseverity instead of Medium andWarning. The finding is still reported, because such a query usually loads one row or the children of one known parent.
Added
- EFD009:
ToLower()orToUpper()applied to a column in an EF Core predicate, which prevents an index seek. Reported atWarningseverity with medium confidence under thePerformancecategory, with provider-aware remediation. - EFD014:
Skippagination with no precedingOrderBy, producing non-deterministic pages. - EFD021: EF Core
DbContextheld in astaticfield, which is not thread-safe and outlives the intended unit of work. Reported atWarningseverity with high confidence under theReliabilitycategory. - EFD022:
StringComparisonstring overload used inside an EF Core query predicate, which EF Core cannot translate to SQL and which throws at runtime. Reported atWarningseverity with high confidence under theCorrectnesscategory. - EFD020: an EF Core
IQueryablelocal enumerated more than once in the same method, re-executing the query against the database each time. Reported atWarningseverity with medium confidence under thePerformancecategory. - EFD010: a synchronous EF Core database call (
ToList,Count,SaveChanges,Find, and peers) inside anasyncmethod or lambda where an EF*Asynccounterpart exists. Reported atWarningseverity with medium confidence under thePerformancecategory. - EFD023:
Contains,EndsWith, or a leading-wildcardEF.Functions.Likeon a column in an EF Core predicate, which cannot seek an index. Reported at advisory confidence andInfoseverity under thePerformancecategory, with provider-aware remediation. - EFD025:
Includepath that duplicates, or is already covered by, anotherIncludepath in the same query. Reported atInfoseverity with high confidence under theMaintainabilitycategory.
Fixed
- EFD005 no longer reports a false positive when a key-equality predicate compares an entity key against a property of another object (for example
Where(sa => sa.ProductId == product.ProductId)). Such a comparand is now recognized as a bounding scalar, so the canonical "hydrate children for a known parent" query is no longer flagged as unbounded. A comparison between two properties of the filtered entity itself remains unbounded. - EFD005 recognizes key equality written with a type's own
==operator, such asGuid,DateTimeOffset, and their nullable forms, or with.Equals(). Before, these were reported as unbounded even when the key was proven.