Skip to content

fix(ef): read entities across project boundaries and stop guessing Guid over a max length - #184

Merged
HandyS11 merged 2 commits into
developfrom
fix/ef-discovery-radius-and-id-type
Aug 7, 2026
Merged

fix(ef): read entities across project boundaries and stop guessing Guid over a max length#184
HandyS11 merged 2 commits into
developfrom
fix/ef-discovery-radius-and-id-type

Conversation

@HandyS11

@HandyS11 HandyS11 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Pre-release validation for v1.1.0 against eShopOnWeb and ardalis/CleanArchitecture surfaced two defects in the DbContext path. The ModelSnapshot path was already correct throughout, and served as the ground truth for every comparison below.

1. Entity discovery never crossed a project boundary

EntityFileDiscovery.BuildSearchDirectories searched only the context directory and its immediate parent, while base-class discovery already walked up to the workspace root. In the standard layered layout — entities in src/Core, DbContext in src/Infrastructure/Data/ — that meant no entity CLR file was ever read, so columns could only come from explicit fluent Property() calls.

Before After
eShopOnWeb CatalogContext 2 of 4 relationships, no Order.OrderDate, no CatalogItem.Description, Guid keys all 4 relationships, correct int keys, both columns present
CleanArchitecture Contributor.PhoneNumber empty PhoneNumber {} box PhoneNumber_CountryCode/Number/Extension, matching the snapshot

Discovery now walks outward to the enclosing solution root, one level at a time so the nearest declaration of a name still wins — jumping straight to the root regressed the complex-ecommerce golden by matching a same-named Money elsewhere in the repo.

Widening the radius exposed two collisions that had to be fixed with it, both found only on the real repositories:

  • Configuration classes keep the narrow radius. They are collected by shape, not by name, so a wider one pulled in entities from unrelated solutions nested in the same repository: the six config classes of CleanArchitecture's MinimalClean tree added Cart, CartItem, GuestUser, Order, OrderItem and Product to an ERD whose only DbSet is Contributor.
  • EF migration classes are skipped. dotnet ef migrations add PhoneNumber emits public partial class PhoneNumber : Migration beside the DbContext — nearer than the value object it is named for — so the owned navigation resolved to the migration and bound as Nullable. Detected by base type or [Migration] rather than by folder name, which is configurable and often renamed.

2. A max length could not overrule a guessed Guid

A property whose name ends in Id and whose CLR type is unresolvable was guessed as a Guid, so eShopOnWeb's string BuyerId rendered as the self-contradictory Guid BuyerId "required, max:256". A max length cannot apply to a value type, so it is proof the guess is wrong.

EfProperty.IsTypeInferred now records whether a type was guessed, so only guesses are corrected: a CLR-declared Guid with HasMaxLength is left exactly as declared (pinned by its own test). The same flag fixes a related gap — HasColumnType's override guard only caught the string fallback, so an explicit column type could not correct a guessed Guid either.

Also: library README quickstarts

The shipped NuGet READMEs resolved renderers registered only as IDiagramRenderer<T> (InvalidOperationException for anyone copying them) and referenced five APIs that do not exist: GetGraphAsync, AnalyzeAsync, ISolutionParser.ParseAsync, two AnalysisOptions properties and three SolutionStats properties. Every snippet was compiled against the real libraries and every documented service resolved from a live container.

Verification

  • 1131 tests green (baseline 1122; 9 new), dotnet format clean, Release build clean
  • Goldens unchanged — the one that moved was a regression introduced and fixed within this branch
  • All 13 real-world diagrams parse under Mermaid v11 in-browser; ERD runtime on eShopOnWeb unchanged (~1.0s)

Residual differences from the snapshot are genuinely unknowable from a context file: Vogen-converted Contributor.Id/Status, and HasMaxLength(ContributorName.MaxLength) behind a const reference.

Note

Validate outside the system temp directory: EntityFileDiscovery skips the parent search under Path.GetTempPath() to isolate parallel test runs, so a clone in /tmp exercises different behaviour than a real checkout.

🤖 Generated with Claude Code

…id over a max length

Pre-release validation against eShopOnWeb and ardalis/CleanArchitecture surfaced two
defects in the DbContext path (the ModelSnapshot path was already correct).

Entity discovery searched only the context directory and its immediate parent, while
base-class discovery already walked up to the workspace root. In the standard layered
layout — entities in `src/Core`, DbContext in `src/Infrastructure/Data/` — no entity CLR
file was ever read, so columns came only from explicit fluent `Property()` calls:
eShopOnWeb lost `Order.OrderDate` and `CatalogItem.Description` plus two of its four
relationships, and CleanArchitecture's `Contributor.PhoneNumber` rendered as an empty box.
Discovery now walks outward to the enclosing solution root, one level at a time so the
nearest declaration of a name still wins.

Widening it exposed two collisions that had to be fixed with it:

- Configuration classes are collected by shape, not by name, so the wider radius pulled in
  entities from unrelated solutions nested in the same repository — the six config classes
  of CleanArchitecture's MinimalClean tree added Cart, GuestUser, Order and friends to an
  ERD whose only DbSet is Contributor. Config discovery keeps the narrow radius.
- `dotnet ef migrations add PhoneNumber` emits `class PhoneNumber : Migration` beside the
  DbContext, nearer than the value object it is named for, so the owned navigation resolved
  to the migration and bound as `Nullable`. Migration classes are now skipped, detected by
  base type or `[Migration]` rather than by folder name.

Separately, a property whose name ends in `Id` and whose CLR type is unresolvable was
guessed as a `Guid`, so eShopOnWeb's string `BuyerId` rendered as the self-contradictory
`Guid BuyerId "required, max:256"`. A max length cannot apply to a value type, so it now
corrects a guessed type to `string`. `EfProperty.IsTypeInferred` records whether a type was
guessed, so only guesses are ever corrected — a CLR-declared type is left alone, and an
explicit `HasColumnType` now also outranks a guessed `Guid` rather than only the `string`
fallback.

Also fixes the library README quickstarts, which resolved renderers registered only as
`IDiagramRenderer<T>` and referenced five APIs that do not exist (`GetGraphAsync`,
`AnalyzeAsync`, `ISolutionParser.ParseAsync`, two `AnalysisOptions` properties and three
`SolutionStats` properties). Every snippet is now compiled and resolved against the real
container.

9 new tests; 1131 green; all 13 real-world diagrams parse under Mermaid v11.
Copilot AI lite review requested due to automatic review settings August 7, 2026 20:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two EF DbContext-path analysis defects found during real-world validation (layered solutions and Fluent API type inference), and updates library READMEs so sample code matches the actual public APIs and DI registrations.

Changes:

  • Expand EF entity CLR file discovery to walk outward up to the enclosing solution root (while keeping configuration discovery on a narrow radius) and skip EF migration classes during entity matching.
  • Track whether an EF property type was inferred/guessed, and allow HasMaxLength / HasColumnType to correct only guessed types (preventing “Guid + max length” contradictions).
  • Update NuGet/library README quickstarts to compile and to resolve renderers/services via the correct APIs and registrations.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/ProjGraph.Tests.Unit.EntityFramework/CrossProjectEntityDiscoveryTests.cs Adds regression coverage for cross-project entity discovery, config leakage prevention, and migration-class collision avoidance.
tests/ProjGraph.Tests.Unit.EntityFramework/ColumnTypeInferenceTests.cs Adds regression coverage for correcting guessed Guid types when HasMaxLength / HasColumnType provide stronger evidence.
src/ProjGraph.Lib/README.md Updates top-level library quickstarts to use current service APIs and renderer resolution patterns.
src/ProjGraph.Lib.EntityFramework/README.md Updates EF library quickstarts to resolve renderers via IDiagramRenderer<EfModel> and fixes example usings.
src/ProjGraph.Lib.EntityFramework/Infrastructure/FluentPropertyWalker.cs Makes HasMaxLength and HasColumnType correctly override only inferred types and preserves authoritative column-type decisions.
src/ProjGraph.Lib.EntityFramework/Infrastructure/EntityFileDiscovery.cs Widens entity discovery across solution roots, adds a narrow-radius helper for config scanning, and skips migration classes when matching entity names.
src/ProjGraph.Lib.EntityFramework/Infrastructure/EfPropertyOverrides.cs Adds override support for the new IsTypeInferred flag.
src/ProjGraph.Lib.EntityFramework/Infrastructure/EfPropertyFactory.cs Introduces and propagates IsTypeInferred for name-based type guessing and subsequent overrides.
src/ProjGraph.Lib.EntityFramework/Infrastructure/EfModelAnalyzer.cs Ensures configuration discovery remains narrow-radius to avoid leaking unrelated-solution entities.
src/ProjGraph.Lib.EntityFramework/Infrastructure/Constants/EfAnalysisConstants.cs Adds migration-related constant names used by migration filtering.
src/ProjGraph.Lib.EntityFramework/Application/IEntityFileDiscovery.cs Extends the interface with BuildLocalSearchDirectories for narrow-radius discovery cases.
src/ProjGraph.Lib.Dependencies/README.md Updates stats quickstart output to match current SolutionStats surface.
src/ProjGraph.Lib.Core/README.md Updates solution parsing quickstart to current parser abstractions (ISlnxParser + GetProjectPaths).
src/ProjGraph.Lib.Core/Infrastructure/WorkspaceRootResolver.cs Adds FindEnclosingSolutionRoot to locate a solution boundary without stopping at .csproj markers.
src/ProjGraph.Lib.ClassDiagram/README.md Updates class diagram quickstarts to directory-based analysis and correct renderer/service resolution.
src/ProjGraph.Core/Models/EfModel.cs Adds EfProperty.IsTypeInferred to distinguish guessed vs. authoritative types in the EF model.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +262 to +269
var derivesFromMigration = typeDecl.BaseList?.Types
.Any(baseType => baseType.Type is IdentifierNameSyntax { Identifier.Text: EfAnalysisConstants.CommonNames.Migration }) == true;

var hasMigrationAttribute = typeDecl.AttributeLists
.SelectMany(list => list.Attributes)
.Any(attribute => attribute.Name.ToString() is EfAnalysisConstants.CommonNames.Migration
or EfAnalysisConstants.CommonNames.MigrationAttribute);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 1b81865. Both checks now reduce to the right-most identifier via the existing SimpleTypeName helper, which I extended to unwrap AliasQualifiedNameSyntax and to recurse through nested qualified names rather than reading only qualified.Right.

Reproduced the gap first: added a fully qualified designer half to the migration fixture ([Microsoft.EntityFrameworkCore.Migrations.Migration("…")] plus a qualified base type), which failed before the fix and passes after — so the collision guard is pinned against generated code that does not write Migration unqualified.

…self-contained

Addresses review on #184.

`IsMigrationClass` only recognised an unqualified `: Migration` base type and an exactly
matching `[Migration]` attribute name, so a migration written with qualified names — which
generated code gives no guarantee against — slipped through and the entity-name collision
it guards could reappear. Both checks now reduce to the right-most identifier through the
existing `SimpleTypeName` helper, extended to unwrap alias-qualified names and to recurse
through nested qualified names. Covered by adding a fully qualified designer half to the
migration-collision fixture, which reproduced the gap before the fix.

The unrelated-solution fixture called `Assembly.GetExecutingAssembly()` without a
`using System.Reflection;`, so the analysed source was not valid C# on its own;
`typeof(ShopContext).Assembly` is self-contained. Configuration application is detected by
method name, so the test still exercises the same path — re-verified by reverting the
config-discovery fix and confirming the test fails.

1131 green, format clean.
@HandyS11
HandyS11 merged commit 515cab1 into develop Aug 7, 2026
6 checks passed
@HandyS11
HandyS11 deleted the fix/ef-discovery-radius-and-id-type branch August 7, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants