Skip to content

Releases: vuglll/EFDoctor

EFDoctor 0.6.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 23:45
9ced926

Added

  • Code fixes in the EFDoctor.Analyzers package. The IDE offers a quick action for a rule's mechanical remediation. The first is EFD029's: replace the discarding OrderBy with ThenBy, or OrderByDescending with ThenByDescending, offered when the receiver is already ordered so the result compiles. EFD002's fix replaces a count compared for existence with Any, 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 a CancellationToken in scope. The fixes ship in EFDoctor.CodeFixes.dll beside the analyzers, and the command-line build ignores them.
  • EFD015: a Contains filter whose list was loaded from another unbounded query on the same DbContext only for that filter, such as var ids = db.Customers.Select(c => c.Id).ToList(); db.Orders.Where(o => ids.Contains(o.CustomerId)). Keeping the first query as an IQueryable composes one statement with a subquery. Reported with advisory confidence at Info severity, under the Performance category. 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, or FindAsync) that leaves out its CancellationToken while a CancellationToken parameter is in scope, so cancelling the request doesn't cancel the database command. Reported with advisory confidence at Info severity, under the Reliability category. An explicit CancellationToken.None or default is not reported.
  • EFD024: an Include of a collection navigation whose every use in the method, after the query is materialized, is filtered in memory, such as Include(b => b.Posts) followed only by blog.Posts.Where(p => p.Published). A filtered Include would apply the filter in SQL. Reported with advisory confidence at Info severity, under the Performance category. 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 same DbContext, such as var order = db.Orders.First(…); db.Update(order);. The change tracker already detects the change, and Update marks every column modified, so SaveChanges writes the full row. Reported with advisory confidence at Info severity, under the Performance category. Detached, untracked, and projected entities are not reported.
  • EFD031: a LINQ operator composed over FromSqlRaw, FromSql, or FromSqlInterpolated whose SQL starts with EXEC, EXECUTE, or CALL, such as FromSqlRaw("EXEC dbo.GetOrders").Where(…). EF Core can't wrap a stored-procedure call in a subquery, so the query throws at runtime. Reported at Warning severity with high confidence, under the Correctness category. 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 by SaveChanges on the same DbContext, with nothing in between attaching the entity. The context doesn't track the entity, so SaveChanges silently saves nothing. Reported at Warning severity with high confidence, under the Correctness category, for a single entity or the elements of a list changed in a foreach. Any other use of the entity between the load and the save, such as Update, 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 a DbContext in every public constructor, or whose factory resolves one from the root provider. The scoped context is then shared for the application's lifetime. Reported at Warning severity with high confidence, under the Reliability category. IDbContextFactory<T>, IServiceScopeFactory, and contexts registered as singletons are not reported.
  • EFD034: Database.EnsureCreated() or EnsureCreatedAsync() on a DbContext type that the same project manages with migrations: a Migration or ModelSnapshot class for it, or a Migrate call on it. EnsureCreated records no migration history, so a later Migrate fails, or new migrations are never applied. Reported at Warning severity with high confidence, under the Correctness category, when analysis of the project completes. Contexts without migrations, and provider switches that call Migrate in the other branch, are not reported.
  • EFD035: two SQL Server column types in the model snapshot. An nvarchar(max) or varchar(max) column used in an index or key is reported with high confidence, because SQL Server can't index it and the migration fails. A decimal left at the default decimal(18,2), with no HasPrecision, [Precision], or [Column(TypeName)], is reported with advisory confidence, because values beyond two decimal places are silently rounded. Category Correctness.
  • 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 with Email. The database may have to scan the table. Reported with advisory confidence at Info severity, under the Performance category, for equality, range, StartsWith, and Contains filters 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 == null and SeriesId != null, or e and !e: together they keep every row, so a filtered Include can't narrow it.
  • EFD036 combines the filters of one query. Where(s => s.SeriesId == id).AnyAsync(s => s.ValidUntilUtc >= now) is no longer reported when SeriesId leads an index, and a query with two unindexed Where calls is reported once, not once per call.
  • EFD001 doesn't treat a foreach over an option type (Option<T>, Maybe<T>, or Optional<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 the efdoctor CLI, in any project it scans. A .globalconfig can set efdoctor.split_query_by_default = true for 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 no DbContext.
  • Test-project detection keys on a test framework's core assembly (xunit.core, xunit.v3.core, nunit.framework, the Visual Studio test framework, or Microsoft.NET.Test.Sdk). A production project that references only an assertion library such as xunit.assert, directly or through another project, was classified as a test project and silently skipped.

EFDoctor 0.5.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 20:31
a948cd7

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 as Any or CountAsync), 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 at Warning severity with high confidence, under the Correctness category. 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 the DbContext, 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 using scope that disposes its DbContext, or the service scope it was resolved from. The caller's enumeration throws ObjectDisposedException. Reported at Warning severity with high confidence, under the Reliability category. 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 for FromSqlRaw and ExecuteSqlRaw. 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 as Include. The writes may be in branches, loops, switch sections, 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, or Union, 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.
  • context.Products.AsQueryable() is now analyzed like context.Products by every rule. It calls EF Core's own DbSet<T>.AsQueryable() method, which the query-chain proof didn't recognize, so queries that started with it were skipped.

EFDoctor 0.4.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 18:12
fc01785

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.Analyzers NuGet 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. .editorconfig changes any rule's severity.
    • Test projects are skipped, as in the CLI. Set the MSBuild property EFDoctorAnalyzeTestProjects to true to analyze one.
  • 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 Select projection in the query would fetch only those columns and track nothing. Advisory, at Info severity, under the Performance category. 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 same DbContext instance. Bulk operations bypass the change tracker, so those entities keep their old values. Reported under the Correctness category: with high confidence at Warning severity 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 after ChangeTracker.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(), or LongCount(), 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 ruleTitle in 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 FirstOrDefault and awaited FirstOrDefaultAsync on an entity query with no projection when the result is only checked for null, 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 as ToList().Count > 0. Before, it recommended Count, and following that advice produced a shape that EFD002 reports.
  • The default diagnostic severity of EFD006, EFD009, EFD010, EFD013, and EFD020 is now Info instead of Warning, because their findings have medium confidence. CLI reports are unchanged: these findings are still reported at Warning severity.
  • The analyzers compile against Roslyn 4.8 instead of 5.9.0.

EFDoctor 0.3.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 20:49
6fa269a

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 DbContext instance, which makes EF Core throw "A second operation was started on this context instance" depending on timing. Reported at Warning severity with high confidence under the Reliability category, for operations passed together to Task.WhenAll/Task.WhenAny, an operation started while an earlier task local is still pending, and operations projected by Select over a captured context into a task combinator. The same instance is proven by symbol, so separate contexts from IDbContextFactory are never reported.
  • EFD029: a second OrderBy or OrderByDescending that discards an earlier ordering in the same query, where ThenBy was meant. EF Core translates only the last OrderBy, so the query sorts by the replacing key alone. Reported at Warning severity with high confidence under the Correctness category. A second ordering after Skip, Take, Distinct, or a projection, and a default ordering overridden through a local, are not reported.

EFDoctor 0.2.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 18:02
4e5c080

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 EFDoctorRepositoryUrl property 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 an if, 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 a global.json governs the target, EFDoctor now checks SDK resolution first by running dotnet --version in the target's directory, with its output captured.
  • The CLI now analyzes a multi-targeted project whose TargetFrameworks list 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 an EFDoctor warning on standard error. If no EF Core project can be analyzed, the run exits with 2.
  • Queries that use AsTracking(), IgnoreQueryFilters(), or IgnoreAutoIncludes() 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 with AsSplitQuery(), 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 an if/else whose 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 example Any(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 as if (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, and HasIndex(...).IsUnique()), and from EF Core's <Navigation>Id foreign-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 and Info severity instead of Medium and Warning. The finding is still reported, because such a query usually loads one row or the children of one known parent.

Added

  • EFD009: ToLower() or ToUpper() applied to a column in an EF Core predicate, which prevents an index seek. Reported at Warning severity with medium confidence under the Performance category, with provider-aware remediation.
  • EFD014: Skip pagination with no preceding OrderBy, producing non-deterministic pages.
  • EFD021: EF Core DbContext held in a static field, which is not thread-safe and outlives the intended unit of work. Reported at Warning severity with high confidence under the Reliability category.
  • EFD022: StringComparison string overload used inside an EF Core query predicate, which EF Core cannot translate to SQL and which throws at runtime. Reported at Warning severity with high confidence under the Correctness category.
  • EFD020: an EF Core IQueryable local enumerated more than once in the same method, re-executing the query against the database each time. Reported at Warning severity with medium confidence under the Performance category.
  • EFD010: a synchronous EF Core database call (ToList, Count, SaveChanges, Find, and peers) inside an async method or lambda where an EF *Async counterpart exists. Reported at Warning severity with medium confidence under the Performance category.
  • EFD023: Contains, EndsWith, or a leading-wildcard EF.Functions.Like on a column in an EF Core predicate, which cannot seek an index. Reported at advisory confidence and Info severity under the Performance category, with provider-aware remediation.
  • EFD025: Include path that duplicates, or is already covered by, another Include path in the same query. Reported at Info severity with high confidence under the Maintainability category.

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 as Guid, DateTimeOffset, and their nullable forms, or with .Equals(). Before, these were reported as unbounded even when the key was proven.