Skip to content

Register what a context declares, and stop counting the wizard scaffold - #55

Merged
peopleworks merged 1 commit into
mainfrom
fix/package-base-context-and-wizard-scaffold
Aug 28, 2026
Merged

Register what a context declares, and stop counting the wizard scaffold#55
peopleworks merged 1 commit into
mainfrom
fix/package-base-context-and-wizard-scaffold

Conversation

@peopleworks

Copy link
Copy Markdown
Owner

Closes #53 and #54, both filed by @MBrekhof on 2026-08-27 from a xaflogic wiki run over four of his own applications. Both were reproduced on a throwaway fixture before a line was changed, and the second was checked against the six real applications rather than against a fixture that would have agreed with whatever the code already did.

#53 — a context on a package base registered nothing at all

DbSetRoster.FindContextClasses reached a context by walking base names from a set seeded with DbContext and grown only by classes declared in the analyzed source. A base that lives in a package is never declared here, so it was never reached — and the roster is the only way a plain EF Core entity with no XAF base is accepted at all.

The everyday case is IdentityDbContext<TUser>, the base every ASP.NET Core Identity template writes:

ORM=EF Core | entities=0 | []                       <- before
ORM=EF Core | entities=2 | [Document, DuetThread]   <- after

Worse than the issue reports it: the ORM was still announced as EF Core, so the answer was not "I could not tell" but "this is an EF Core application with no entities", said in the same voice as everything that was actually read.

Fixed with the first of the three shapes the issue proposes: a class that declares DbSet<T> properties is a context, whatever it derives from. That is the stronger signal — it is the application stating a table, it has to be right for the application to run, and it is the only one that does not need the base class in hand.

The base walk is deleted rather than kept alongside, because it was exactly redundant: a class it found without DbSet<T> properties contributed nothing to the roster anyway. A hand-written AuditedDbContext : DbContext and a context on a package base stop being two cases, one of which worked.

It stays a property, never a mention of the generic. The existing LegacyRepository fixture — which writes DbSet<AuditEntry> as a local inside a method body — still contributes nothing, and its test still passes untouched.

New fixture ProfileDbContext.cs: a context whose base and its type argument are declared nowhere in the fixture, which is the shape both existing roster fixtures already passed.

#54 — the wizard scaffold counted as classes you had modelled twice

The rule that makes a class yours — its own source was read in one of the projects — is the right rule, and it is exactly why the scaffold gets in. The Project Wizard writes ApplicationUser and ApplicationUserLoginInfo into every solution created with v21.1 or later, so their source is read.

Two probe applications sharing nothing but their author reported recurring=2.

A class is now shown as carried by the framework when every application declares it with a DevExpress security contract and with the same properties, and it is then left out of the count, the map, the overlap grid and the vocabulary. It is still on the page, under its own heading, with the contract that earned it the label printed on the card so a reader can disagree.

Both halves are load-bearing, and the six real applications prove the second one rather than a fixture asserting it:

Class In Verdict Why
ApplicationUser 4 apps still counts pwLegalOffice added Email, Photo, Tasks — shapes differ
ApplicationUserLoginInfo 4 apps framework untouched in all four

Classes modelled more than once: 17 → 16. Exactly one class moved, and it is the right one. A developer who extends the scaffold really has built something, and that difference is what the property comparison already exists to show.

On the name list

The issue argues this avoids the kind of list the wiki refuses to keep. That is half true and worth saying out loud: the rule still needs to know which interfaces are DevExpress security contracts. It is a different kind of list — framework API surface that DevExpress documents and cannot rename without breaking every application implementing it, rather than the class names one template happened to emit this year — but it is a list, and the code says so where it defines it. The names were checked against the DevExpress documentation, not written from memory.

It also means a class is never dismissed on the contract alone: the documented Employee : Person, ISecurityUser, ... shape is a business object that happens to log in, and it keeps counting the moment two applications shape it differently.

Three more consumers of that number were wrong, with the suite green

All three found by generating the page and reading it. None of them would have failed a test.

  • The CLI summary printed the unfiltered count, so the terminal and the page it had just written disagreed about the same corpus — 2 against 0.
  • The vocabulary card offered the scaffold as yours. Two applications sharing nothing else were told LoginProviderName and ProviderUserKey were house vocabulary — the same false claim as the classes card, two headings down. A class that does not count as modelled twice no longer supplies words that do. On the real corpus this promoted three actual house names into a list capped at sixty.
  • WriteSites bypassed the helper that pluralises, printing 1 properties beside a heading that got it right. That helper comment already says why it exists: "entity" reaching the page as "entitys" is the kind of thing a reader takes as evidence that nobody looked. Four call sites went around it.

One unrelated fix, included because it obstructs everyone

EntityAnalyzer.cs carried a literal NUL byte inside a string interpolation, used as a composite key separator. It compiled, and it made the largest file in the project binary to grep and ripgrep, which refuse to search it and report only binary file matches. It blocked two searches while this work was being done. Written as the \0 escape instead — the identical character, and the file is searchable again.

Verification

  • 525 tests, up from 514: one for the package base, six for the corpus rule, four against the generated HTML.
  • Six new tests assert against the page, not the corpus object, because every defect in the second half of this PR was invisible in the analysis result.
  • Generated over all six real applications before and after, and the delta above is measured rather than reasoned.

One thing for the reviewer

This is one commit for two issues. They came from the same report and land together, but they split cleanly into three (#53, #54, and the NUL) with each one green on its own — including the README test count, which steps 514 → 515 → 525 in that order. Say the word and I will rearrange it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W45tzJFX3NoSrk7svtQeKT

Both reported by @MBrekhof on 2026-08-27, found running `xaflogic wiki` over
four of his own applications. Both reproduced on a probe fixture before a line
was changed, and the second verified against the six real applications.

Closes #53. The DbSet roster reached a context by walking base names from a set
seeded with `DbContext` and grown only by classes declared in the analyzed
source, so a base living in a package was never reached. `IdentityDbContext<T>`
is the base every ASP.NET Core Identity template writes: an application with 56
`DbSet<T>` properties reported zero entities, silently, and announced itself as
EF Core while doing it. A class that declares `DbSet<T>` properties is now a
context whatever it derives from -- the stronger signal, and the only one that
does not need the base class in hand. The base walk is deleted rather than kept
alongside, because a class it found without `DbSet<T>` properties contributed
nothing to the roster anyway. `LegacyRepository`, which declares `DbSet<T>` as a
local inside a method body, is still not a registration.

Closes #54. The XAF Project Wizard writes `ApplicationUser` and
`ApplicationUserLoginInfo` into every solution created with v21.1 or later, so
the rule that makes a class yours -- its own source was read here -- let them
through, and any two XAF applications built since 2021 appeared to share them.
A class is now shown as carried by the framework when every application declares
it with a DevExpress security contract AND with the same properties, and it is
left out of the count, the map, the overlap grid and the vocabulary. Both halves
are load-bearing, and the real corpus proves it: `ApplicationUser` is in four of
the six and still counts, because pwLegalOffice added `Email`, `Photo` and
`Tasks`. Only `ApplicationUserLoginInfo` drops out, 17 to 16.

Three further consumers of that number were wrong with the suite green, and all
three were found by generating the page and reading it rather than by running
tests. The CLI summary printed the unfiltered count, so the terminal and the
page it had just written disagreed. The conventions card offered
`LoginProviderName` and `ProviderUserKey` as house vocabulary to two
applications sharing nothing else -- excluding them promoted three real house
names into a list capped at sixty. And `WriteSites` bypassed the helper that
pluralises, printing `1 properties` beside a heading that got it right; the
helper comment already says why that matters.

Unrelated and included because it obstructs everyone: `EntityAnalyzer.cs`
carried a literal NUL byte inside a string interpolation, used as a composite
key separator. It compiled, and it made the largest file in the project binary
to grep and ripgrep, which refuse to search it and say only "binary file
matches". Written as the escape instead, which is the identical character.

525 tests, up from 514.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W45tzJFX3NoSrk7svtQeKT
@peopleworks
peopleworks merged commit 954a24d into main Aug 28, 2026
6 checks passed
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.

A context deriving from IdentityDbContext<T> — any DbContext subclass from a package — registers no entities

1 participant