feat: ExcludeNavigationProperties on [GenerateDtos] — EF model-manifest driven - #399
Draft
dkattan wants to merge 29 commits into
Draft
feat: ExcludeNavigationProperties on [GenerateDtos] — EF model-manifest driven#399dkattan wants to merge 29 commits into
dkattan wants to merge 29 commits into
Conversation
dkattan
marked this pull request as draft
July 9, 2026 10:37
dkattan
force-pushed
the
feature/generate-dtos-verbosity
branch
from
July 9, 2026 10:43
bbc3211 to
45ad7fc
Compare
dkattan
force-pushed
the
feature/generate-dtos-verbosity
branch
from
July 9, 2026 14:03
97885fa to
5f5dfde
Compare
Contributor
Author
|
Rebased onto #401's branch so the preview-publish guard applies here too — that's why this PR temporarily shows the one-line 🤖 Addressed by Claude Code |
dkattan
commented
Jul 9, 2026
dkattan
force-pushed
the
feature/generate-dtos-verbosity
branch
from
July 9, 2026 21:25
a7856f1 to
48afc1e
Compare
Skips properties whose type (or collection element type, for any IEnumerable other than string - arrays included, dictionary key/value types unwrapped) is a class or interface declared in the same assembly as the source model - removing ORM navigation and back-reference properties from generated DTOs without hand-listing each one in ExcludeProperties. Scalars, enums, framework types, primitive collections, classes from other assemblies, and user-defined value types (e.g. strongly-typed ID structs) are always kept. IncludeProperties is the escape hatch: names listed there survive every automatic and explicit exclusion, for aggregate children (task parameters, order lines) that the heuristic would otherwise drop. Documented limitations, pinned by tests: non-collection wrapper generics (Lazy<T>, Task<T>) and entities declared in a different assembly are not detected. Composes with flags-combined OutputType: one attribute can emit a nav-free Interface + PartialClass pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ristic to authoritative
Facet.Extensions.EFCore now ships design-time services (FacetDesignTimeServices +
a MigrationsScaffolder subclass) that write a {ContextName}.facetmodel manifest
beside the migrations model snapshot on every 'dotnet ef migrations add/remove',
recording per entity which properties EF maps as data (scalar/complex) and which
are navigations, owned references, or skip navigations.
The GenerateDtos generator reads these manifests as AdditionalFiles: for source
types listed there, ExcludeNavigationProperties keeps exactly the mapped data
properties — value-converted columns survive and EF-ignored properties drop,
both unlike the heuristic — while unlisted types keep the heuristic behavior.
The transform now only marks heuristic candidates; the final member set is
resolved in the generation stage where AdditionalFiles are visible.
The manifest format is line-based on purpose: the netstandard2.0 generator
cannot assume a JSON library in every compiler host without packaging one into
the analyzer, and the flat records diff better in review anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SGF (SourceGenerator.Foundations 2.0.16) hoists GenerateDtosGenerator behind a generated wrapper carrying [Generator]: callbacks get exception isolation and a logger, and — the load-bearing feature — referenced assemblies are embedded into Facet.dll as resources and resolved at generator load time, which is the one sanctioned way for an analyzer to carry NuGet dependencies into compiler hosts. That dissolves the reason the manifest format was line-based, so it is now JSON (*.facetmodel.json), written with Utf8JsonWriter and read with JsonDocument. The rewrite also fixes three review findings in the old reader: files now parse atomically into a local buffer (a malformed or future-version manifest is ignored in full — never half-applied, where an entity left with an accidental empty keep-set would drop every property), TryGetKeepSet exposes an ImmutableHashSet instead of the internal mutable set, and equality is structural over the parsed content (formatting-only edits no longer invalidate the incremental cache) rather than fingerprinting the raw text. Embedding is trimmed to what the generator actually needs at runtime: Workspaces and its System.Composition closure are excluded (IDE hosts provide them; embedding Roslyn-family assemblies risks type identity), keeping the analyzer at ~4 MB instead of ~11 MB. Driver-based tests instantiate the generated internal hoist (InternalsVisibleTo) per SGF's testing story. Verified against a large real-world consumer codebase: cold CLI build with the generator consumed as a ProjectReference analyzer, 128-entity JSON manifest parsed via embedded System.Text.Json, generated contracts byte-identical to the line-format output, and no generator log noise in build output. Suite: 932/932. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SGF embeds Facet.Attributes.dll into Facet.dll and resolves it at generator load time, so the analyzer is self-contained even when consumed as a ProjectReference. Verified with a cold build (compiler server shut down, obj/bin wiped) — previously that path failed with CS8784 because Roslyn's analyzer loader does not probe sibling files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The only silent state left is having no manifests at all (tier 1, by design). Once manifests exist, every degradation is reported: - FAC103 (error): a *.facetmodel.json file is malformed — ignored in full, never half-applied, and never quietly. - FAC104 (error): a manifest declares an unsupported version (Facet / Facet.Extensions.EFCore package mismatch). - FAC105 (warning): an ExcludeNavigationProperties source type is missing from every manifest — stale manifest or type-name mismatch; suppressible at the attribute for genuinely non-entity types. - FAC106 (warning): a settable property on a listed type appears in none of the manifest's categories — the manifest predates the property, which would otherwise silently vanish from DTOs. To make FAC106 sound, the manifest now records everything the model has an opinion on: new 'ignored' (explicitly ignored members, recovered from the convention surface across the inheritance chain — 69 real ones in a large consumer model, all FAC106 false positives otherwise) and 'service' (ILazyLoader-style) categories. Computed get-only properties are exempt: the model never maps them. IncludeProperties remains explicit user intent. FAC101–FAC106 now anchor to the [GenerateDtos] attribute's location instead of Location.None, so they are #pragma-suppressible per type and land as squiggles in the IDE; the model carries a cache-safe SourceLocationInfo instead of a Location. Strictness needs no new API: WarningsAsErrors escalates FAC105/106. Docs: FAC101–106 added to the analyzer rules reference, release tracking, and the GenerateDtos guide. Suite: 942/942, including a real-codebase negative test (property added without regenerating → FAC106 at the attribute). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Startup project' alone left the reader guessing at a file. Name the conventional homes (any compiled .cs file, typically Properties/AssemblyInfo.cs), show the zero-new-file csproj <AssemblyAttribute> form, and note the startup project must transitively reference Facet.Extensions.EFCore so dotnet ef can load the assembly named in the attribute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the buildable form of 'error when the manifest glob matches nothing'. The generator cannot see an AdditionalFiles glob that matched zero files — an empty glob passes nothing to the compiler, indistinguishable from no glob — so strictness is signalled with a build property: <Facet_RequireEfModelManifest>true</Facet_RequireEfModelManifest> When set, the same-assembly heuristic is off and manifest coverage is mandatory: an ExcludeNavigationProperties source type with no manifest entry (no manifest supplied at all, or this type absent) is FAC107 — an error — instead of the silent FAC105-advisory fallback. Read via a CompilerVisibleProperty (registered in Facet.props) threaded through AnalyzerConfigOptionsProvider; default false, so the two-tier behavior is unchanged unless opted in. Docs rewritten from the immybot wiring experience: - the three-project triangle (startup / migrations / DTO project) with a role table and the cross-project AdditionalFiles relative path — the previous local 'Migrations/*.facetmodel.json' only worked in a single-project layout; - what writes the manifest (migrations add/remove only) and the first-run bootstrap, so 'I wired it up and nothing changed' is answered; - the design-time-vs-runtime model trap for programmatic generation (use IDesignTimeModel.Model or every [NotMapped] becomes a spurious FAC106); - the drift-is-a-build-failure story (RequireEfModelManifest + WarningsAsErrors) framed as a CI guarantee, not a caveat; - multi-context manifests each write their own file and merge. FAC107 in analyzer rules doc + release tracking. 5 new strict-mode tests (no-manifest -> FAC107, uncovered -> FAC107, covered -> quiet, non-ExcludeNav unaffected, FAC105 replaced not doubled). Suite 946/946. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heuristic The same-assembly type-shape heuristic is gone. ExcludeNavigationProperties is now driven solely by the EF model manifest: a source type with no manifest entry is FAC105, an error, not a silent fallback to a guess. Rationale (Darren's call): the heuristic was a known-fragile hack (value- converted columns wrongly dropped, cross-assembly entities wrongly kept, [NotMapped] scalars wrongly kept). Shipping it as a default and removing it later would break adopters — and since none of this is released yet, the honest move is to not ship the fragile path at all. EF is the ORM Facet users overwhelmingly reach for; requiring its model up front is the right constraint. Removed: - IsNavigationProperty / IsSameAssemblyDomainType and the transform's heuristic-marking pass; HeuristicNavigationProperties off the model. - The opt-in Facet_RequireEfModelManifest property + its CompilerVisibleProperty and AnalyzerConfigOptions plumbing, and FAC107 — all subsumed: manifest coverage is unconditional now, so FAC105 (promoted from warning to error) is the single 'not covered' diagnostic. FAC106 stays a warning (stale- manifest freshness; escalate via WarningsAsErrors). - Heuristic-era tests (GenerateDtosNavigationEdgeCaseTests, the reflection-based GenerateDtosNavigationExclusionTests) and their compile-time entities, whose behavior is fully covered by the driver-based manifest tests. Docs rewritten so the manifest is THE mechanism, not an upgrade: a Mermaid diagram of the single-project vs three-project layouts (answering 'where do the DTOs live' — in the [GenerateDtos] project, always), FAC105-as-error, no heuristic language. Suite 934/934; analyzer self-contained (STJ + Attributes still embedded). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ic' contradiction The manifest is just a file with two ways to produce it; the docs claimed migrations were the only way and then, separately, described a programmatic path — leaving readers unsure which is true or when they'd use the tool. Reframed both docs so the migration hook and the public writer are presented as two callers of the same API: - the hook writes it automatically as part of a migration you'd add anyway (and is what makes the committed file a drift guard); - FacetEfModelManifest.Write is the no-migration path, with its use cases named up front — first-time bootstrap (model unchanged, so 'migrations add' says 'no changes'), workflows that avoid dotnet ef, and CI drift-check tests. Explicitly: you never have to invent a no-op migration to get a manifest. Dropped the 'only thing that writes it' / 'regenerated only when migrations are' absolutes that created the contradiction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tos guide
The heuristic-removal rewrite collapsed it to a prose mention ('or generated
from the csproj with an <AssemblyAttribute> item'); put the runnable XML block
back so the no-new-file registration path is copy-pasteable, matching the
Facet.Extensions.EFCore README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-rebase onto master with Tim-Maes#400 (assembly-level [GenerateDtosFor]) and Tim-Maes#397 (Patch wire format) merged: two facts pin the integration seam. An assembly- level registration with ExcludeNavigationProperties gets the same manifest- driven shaping as the class-level attribute (shared BuildModels pipeline), and an uncovered assembly-level type reports FAC105 anchored at the [assembly:] attribute. Rebase notes: BuildModels threads the Compilation for the Patch wire-support detection, and every model copy-constructor preserves the SupportsSystemTextJson/SupportsNewtonsoftJson flags across the merged field set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dkattan
force-pushed
the
feature/generate-dtos-verbosity
branch
from
July 10, 2026 08:05
48afc1e to
df95c5f
Compare
dkattan
added a commit
to dkattan/Facet
that referenced
this pull request
Jul 10, 2026
… archive Curated port of the 2025-09 fluent navigation work from the closed PR Tim-Maes#47 branch (feature/enhanced-dto-generation-and-ef-updates), brought onto current master so it is preserved, reviewable, and compiling — not rotting on a stale base. Inert by construction: IsPackable=false, referenced by nothing, generator wired into no consumer. Ported verbatim: ChainUseDiscovery (usage-driven shape generation, avoids the 2^N navigation-combination explosion), the Emission suite (shape interfaces, per-navigation capability interfaces generic in the navigation's shape, fluent builders, selectors), ModelRoot/FacetDtoInfo/EfJsonReader/FacetConfiguration, and the facet-fluent-navigation specifications. Curation applied: FacetEfGenerator moved into the project and its hand-rolled AppDomain.AssemblyResolve hook removed (SourceGenerator.Foundations, adopted in 'Tim-Maes#399', provides that properly); single netstandard2.0 target (multi-targeting served the unported MSBuild-task hosting); ExportEfModelTask/efmodel.json infra deliberately left behind — the *.facetmodel.json model manifest supersedes it. specifications/facet-fluent-navigation/RESURRECTION.md records provenance, the port table, inherited unfinished work (SelectorsEmitter placeholders, terminal NotImplementedExceptions, the generated-file ordering issue), and the modernization path (manifest-reader retarget, SGF hosting, projection completion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback: the docs assumed the consumer adopts mid-migration and handwaved producing the initial facetmodel.json in the most probable case — an existing app with a migration history and no model change pending, where 'migrations add' alone would create an unwanted migration. The zero-code answer was implemented but never documented: the hook fires on 'migrations remove' too, so an add/remove pair bootstraps the manifest and leaves no migration behind (remove re-scaffolds the snapshot and the hook rewrites the manifest beside it; nothing touches a database). Both doc sites now lead the bootstrap story with that pair, with the programmatic writer kept for dotnet-ef-free workflows and CI drift checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pter's starting point Ground-up rework of the manifest walkthrough. It now assumes the realistic starting state — an existing app with a DbContext and migration history, no model change in flight — and walks the four setup steps in order: install Facet.Extensions.EFCore, register design-time services in the startup project, bootstrap the first manifest with an add/remove migration pair, point AdditionalFiles at it, and flip the attribute on. Accuracy fixes surfaced by an adversarial review of the draft: name the package install explicitly, state that FAC103/FAC104 are file-level while FAC105/FAC106 anchor to the attribute, give the concrete FAC106 remedy (dotnet ef migrations add), note FAC106 also covers get-only collections, qualify the IncludeProperties override (Create DTOs still drop Id), correct the no-snapshot edge on last-migration removal, and describe the --startup-project default as the current directory's project. The diagnostics table now lives only in the Facet.Extensions.EFCore README; docs/09 keeps a summary and links there and to the analyzer rules page, so the two can't drift. Also removes the unused EfModelManifest.HasEntities member and corrects the manifest file extension in FacetDesignTimeServices XML docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rojects, add FacetEfDesignTime MSBuild opt-in Wiring an EF model manifest into a project's AdditionalFiles is now the project-level switch for DTO shaping: every [GenerateDtos]/[GenerateDtosFor] attribute that leaves ExcludeNavigationProperties unset is shaped by the model, held to the same FAC105/FAC106 coverage rules as an explicit opt-in. An explicit value wins in both directions — false is the per-type escape hatch for non-entity source types. Projects without a manifest are unchanged, and a rejected-only manifest set does not flip the default, so FAC103/FAC104 stand alone instead of being buried under a FAC105 cascade. The obsolete [GenerateAuditableDtos] attribute is exempt: it cannot express the opt-out, so it keeps its legacy unshaped behavior. Registration also drops to one line: Facet.Extensions.EFCore now ships a buildTransitive targets file where <FacetEfDesignTime>true</FacetEfDesignTime> emits the DesignTimeServicesReference assembly attribute. EF reads that attribute from both the DbContext project's assembly and the startup assembly and de-duplicates, so the property works in either project; it is deliberately opt-in so a package reference alone never changes what migrations write. A coded warning (FACET001) flags the property under GenerateAssemblyInfo=false, where assembly attributes cannot be emitted. EfModelManifest equality now keys manifest presence as a bool rather than an exact accepted-file count, so consolidating per-context manifest files without changing content does not invalidate cached generator output. FAC105's message states whether shaping came from the explicit flag or the wired-manifest default, and names the full remedy chain including the design-time services registration. Docs rewritten around the three-step adoption flow, including an honest account of the one silent failure mode: under the default, an AdditionalFiles glob that matches nothing means no shaping and no diagnostics — pinning ExcludeNavigationProperties = true on one representative entity is the documented tripwire. Verified end to end from a cold NuGet consumer: pack, FacetEfDesignTime opt-in respected both ways, manifest written on migrations add and surviving remove, implicit shaping with zero per-attribute config, FAC105 on an uncovered type with the defaulted message, and the explicit-false escape hatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…itionalFiles Single-project adopters now touch exactly one knob. The buildTransitive targets glob the project's own *.facetmodel.json files (DefaultItemExcludes keeps bin/obj out) under the same FacetEfDesignTime property that emits the design-time registration attribute: write manifests, consume manifests, shaping on. The glob matches nothing until the first migration runs, which is harmless — dotnet ef builds the project before it writes anything. Cross-project setups keep the hand-written <AdditionalFiles> glob, which cannot be automated away: a cross-project manifest path is knowledge only the consuming project has, and nothing forces it to reference the DbContext project at all. Duplicate wiring (auto-glob plus a manual glob matching the same file) is a pinned no-op — the manifest reader merges per file by union. The FACET001 remediation is reworked to match the property's two halves: under GenerateAssemblyInfo=false only the attribute half is lost, so the warning now says to keep the property (the AdditionalFiles half still works), register via a source-file attribute, and demote the coded warning once done. Re-verified end to end from packed artifacts: with no AdditionalFiles in the consuming csproj, -getItem:AdditionalFiles shows exactly the package glob's match, generated DTOs are shaped, and double-wiring builds identically with no new diagnostics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dkattan
added a commit
to dkattan/Facet
that referenced
this pull request
Jul 10, 2026
… archive Curated port of the 2025-09 fluent navigation work from the closed PR Tim-Maes#47 branch (feature/enhanced-dto-generation-and-ef-updates), brought onto current master so it is preserved, reviewable, and compiling — not rotting on a stale base. Inert by construction: IsPackable=false, referenced by nothing, generator wired into no consumer. Ported verbatim: ChainUseDiscovery (usage-driven shape generation, avoids the 2^N navigation-combination explosion), the Emission suite (shape interfaces, per-navigation capability interfaces generic in the navigation's shape, fluent builders, selectors), ModelRoot/FacetDtoInfo/EfJsonReader/FacetConfiguration, and the facet-fluent-navigation specifications. Curation applied: FacetEfGenerator moved into the project and its hand-rolled AppDomain.AssemblyResolve hook removed (SourceGenerator.Foundations, adopted in 'Tim-Maes#399', provides that properly); single netstandard2.0 target (multi-targeting served the unported MSBuild-task hosting); ExportEfModelTask/efmodel.json infra deliberately left behind — the *.facetmodel.json model manifest supersedes it. specifications/facet-fluent-navigation/RESURRECTION.md records provenance, the port table, inherited unfinished work (SelectorsEmitter placeholders, terminal NotImplementedExceptions, the generated-file ordering issue), and the modernization path (manifest-reader retarget, SGF hosting, projection completion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dkattan
added a commit
to dkattan/Facet
that referenced
this pull request
Jul 10, 2026
Written after rebasing this branch onto the manifest work (Tim-Maes#399), while both configuration surfaces are still unshipped and can be designed together. INTERACTION-WITH-MODEL-MANIFEST.md maps each prototype-era piece to its Tim-Maes#399 replacement (efmodel.json/EfJsonReader/ModelRoot, the AssemblyResolve hook, the debug-file diagnostics), and establishes the load-bearing findings: - A fluent shape interface's member set is exactly the manifest keep-set, so one manifest entry feeds both features and one FacetEfDesignTime knob produces the data for both. - Navigation targets and cardinality do not need schema support — they resolve from Roslyn symbols once the manifest names the navs. Entity keys do: they are pure model configuration, and the prototype's GetByIdAsync hard-codes a literal "Id" today. `keys` is an additive field (unknown properties are ignored by design), so it lands with the fluent feature, not in the alpha. - IncludeProperties and .WithX() are contract-time vs query-time inclusion, not competing knobs; chain-use discovery keeps manifest presence from lighting up fluent machinery by surprise. - Open decision recorded for builder emission (manifest-driven behind one enable knob vs attribute-driven), with [GenerateAuditableDtos] discovery dropped either way, consistent with its exemption in Tim-Maes#399. Also corrects RESURRECTION.md's claim that the manifest already records navigation targets and cardinality — v1 records names only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ring layout FacetEfDesignTime in a consuming project now also collects *.facetmodel.json from each direct ProjectReference via a BeforeTargets=CoreCompile target — the knob stays local to the project it affects while the path comes from a reference the user already maintains, so the usual layered setup (DTO project references the DbContext project) needs no hand-written glob and has nothing to typo. A target rather than an evaluation-time item because MSBuild only expands wildcards derived from item metadata inside targets; referenced projects' bin/obj are excluded, and overlaps with the other wiring forms merge idempotently in the reader. The README walkthrough's wiring step now shows a concrete solution tree and ranks the four layouts: same-project (automatic), direct reference (automatic with the same property), any layout via one Directory.Build.props line anchored to $(MSBuildThisFileDirectory) — with an explanation that its power is per-project directory-walk imports, not property inheritance — and the plain relative glob. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dkattan
added a commit
to dkattan/Facet
that referenced
this pull request
Jul 10, 2026
… archive Curated port of the 2025-09 fluent navigation work from the closed PR Tim-Maes#47 branch (feature/enhanced-dto-generation-and-ef-updates), brought onto current master so it is preserved, reviewable, and compiling — not rotting on a stale base. Inert by construction: IsPackable=false, referenced by nothing, generator wired into no consumer. Ported verbatim: ChainUseDiscovery (usage-driven shape generation, avoids the 2^N navigation-combination explosion), the Emission suite (shape interfaces, per-navigation capability interfaces generic in the navigation's shape, fluent builders, selectors), ModelRoot/FacetDtoInfo/EfJsonReader/FacetConfiguration, and the facet-fluent-navigation specifications. Curation applied: FacetEfGenerator moved into the project and its hand-rolled AppDomain.AssemblyResolve hook removed (SourceGenerator.Foundations, adopted in 'Tim-Maes#399', provides that properly); single netstandard2.0 target (multi-targeting served the unported MSBuild-task hosting); ExportEfModelTask/efmodel.json infra deliberately left behind — the *.facetmodel.json model manifest supersedes it. specifications/facet-fluent-navigation/RESURRECTION.md records provenance, the port table, inherited unfinished work (SelectorsEmitter placeholders, terminal NotImplementedExceptions, the generated-file ordering issue), and the modernization path (manifest-reader retarget, SGF hosting, projection completion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dkattan
added a commit
to dkattan/Facet
that referenced
this pull request
Jul 10, 2026
Written after rebasing this branch onto the manifest work (Tim-Maes#399), while both configuration surfaces are still unshipped and can be designed together. INTERACTION-WITH-MODEL-MANIFEST.md maps each prototype-era piece to its Tim-Maes#399 replacement (efmodel.json/EfJsonReader/ModelRoot, the AssemblyResolve hook, the debug-file diagnostics), and establishes the load-bearing findings: - A fluent shape interface's member set is exactly the manifest keep-set, so one manifest entry feeds both features and one FacetEfDesignTime knob produces the data for both. - Navigation targets and cardinality do not need schema support — they resolve from Roslyn symbols once the manifest names the navs. Entity keys do: they are pure model configuration, and the prototype's GetByIdAsync hard-codes a literal "Id" today. `keys` is an additive field (unknown properties are ignored by design), so it lands with the fluent feature, not in the alpha. - IncludeProperties and .WithX() are contract-time vs query-time inclusion, not competing knobs; chain-use discovery keeps manifest presence from lighting up fluent machinery by surprise. - Open decision recorded for builder emission (manifest-driven behind one enable knob vs attribute-driven), with [GenerateAuditableDtos] discovery dropped either way, consistent with its exemption in Tim-Maes#399. Also corrects RESURRECTION.md's claim that the manifest already records navigation targets and cardinality — v1 records names only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed entities Emits an info-level diagnostic after generation showing how many EF model manifest entities have [GenerateDtos] configured versus the total available. Lists up to 10 uncovered entity names, then a count. Only fires when a manifest is wired in and there are uncovered entities. Exposes GetEntityNames() on EfModelManifest so the generator can iterate the full entity key set without breaking encapsulation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Owned entities (EF Core IsOwned()) are never independent DTO targets — they appear only as nested properties of their owning entity. The manifest writer now records 'isOwned' and the reader excludes owned entities from GetEntityNames() and EntityCount, so FAC107's coverage metric no longer counts them as uncovered.
FAC108 reports one diagnostic per uncovered or partially covered EF
manifest entity. It reads *.facetmodel.json AdditionalFiles (same as
the generator's FAC107 summary) and scans both class-level
[GenerateDtos] and assembly-level [assembly: GenerateDtosFor]
attributes, tracking DtoTypes granularity — an entity with Create
but not Update produces 'has Create configured, but Update missing'.
The code fixer offers 'Generate DTOs for {EntityName}', inserting an
[assembly: GenerateDtosFor(typeof(Entity), Types=Create|Update,
OutputType=PartialClass, ExcludeAuditFields=true)] attribute into the
file that already contains other assembly-level GenerateDtosFor
attributes.
8 new tests cover: uncovered entities, message format, no manifest,
fully configured entity, partial coverage, owned type exclusion,
severity, and source anchoring.
Analyzer: use ContainsSyntaxTree to verify entity locations are in the current compilation before anchoring diagnostics — Roslyn rejects diagnostics with locations in referenced assemblies. Pass EntityFullName via diagnostic properties so the code fixer can resolve types precisely. Code fixer: use GetTypeByMetadataName with the full name from diagnostic properties instead of fuzzy GetSymbolsWithName matching, which matched unrelated types sharing a simple name (e.g. User → Microsoft.Graph.User).
Diagnostics with Location.None can't be fixed by Roslynator or IDE code fixers — the fixer needs a document to modify. Anchor FAC108 to the first [assembly: GenerateDtosFor] attribute location (typically FacetGeneration.cs) so the fixer knows which document to modify. Also pass EntityFullName via diagnostic properties for precise type resolution in the code fixer (avoids fuzzy simple-name matching).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allows renaming entity properties in generated DTOs via 'EntityProp:DtoProp' pairs. The generated property uses the DTO name but maps to the entity name in constructors and projections. Not yet working in SGF hoist pipeline — needs further investigation into why the renameMap isn't applied during member creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add DtoPreset enum (ResponsePartial, RequestPartial, InterfaceRequest) that bundles common property defaults. Explicit attribute values always override preset defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renamed properties (e.g. CreatedDate→CreatedDateUtc) were being filtered out by EF model manifest shaping because the keep/include sets contain the source entity name, not the renamed DTO name. Now checks both m.Name and m.SourcePropertyName against the keep and include sets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scores handwritten DTOs for Facet migration difficulty: - Info severity: easy migration (all get/set auto-properties) - Warning severity: needs behavior changes (read-only or computed props) Features: - Shadow cache (ConditionalWeakTable<Compilation, ConcurrentDictionary>) inspired by Vogen's BoundedCache pattern, keyed by Compilation with SymbolEqualityComparer for proper symbol equality - Fail-fast: throws InvalidOperationException on unexpected manifest state - Gratuitous logging with severity levels (DEBUG-only via Conditional attribute) - DTO-to-entity name matching by stripping common prefixes/suffixes - Skips entity classes themselves and already-Facet-configured types - 10 unit tests covering easy/medium/no-diagnostic/multiple-DTO scenarios Includes devloop-fac109.sh inner dev loop script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eration for partials
- PropertySuffix: auto-append suffix (e.g. 'UTC') to all DateTime/DateTimeOffset
property names, eliminating RenameProperties boilerplate for audit date fields
- GenerateReadOnlyProperties: emit { get; init; } instead of { get; set; },
suppressing 'required' modifiers and '= default!;' initializers
- Constructor generation for ResponsePartial preset: generate entity-to-DTO
constructor with partial void OnInitialized(SourceType) hook for computed props
- Generated constructor is 'internal' for partial classes to avoid S3427 overlap
with partial constructors that have optional parameters
- Fix source property name bug in constructor/FromSource/Projection (use
SourcePropertyName instead of DTO name when properties are renamed)
- Suppress [SetsRequiredMembers] when GenerateReadOnlyProperties is true
- #pragma warning disable CS8618 around generated class for read-only properties
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prevents double-suffixing (e.g. MarkedForDeletionAtUtc → MarkedForDeletionAtUtcUTC) when an entity property already has the suffix in its name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 'RequestBody' and 'Body' to suffix list, track DtoKind (Response, Create, Update) from prefix, adjust line savings estimate per kind, update diagnostic message to show kind label, add per-kind configured filtering so entities with only Response configured still surface Create/Update candidates. Add 3 new tests for Create/Update request DTO matching and configured filtering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #396. ORM entities carry navigation and back-reference properties (
Tenant? Owner,List<Order> Orders, …) that don't belong in request DTOs — and today each one must be hand-listed inExcludeProperties. On wide entities that means 30+nameof(...)entries per attribute, kept in sync by hand as navigations are added.Manifest shaping drops them automatically, following the EF Core model's own navigation designation — not a type-shape heuristic. And in a project that wires a model manifest into
<AdditionalFiles>, shaping is the default: every[GenerateDtos]attribute that leavesExcludeNavigationPropertiesunset is shaped, an explicit value wins in both directions, and= falseis the per-type opt-out for non-entity source types.The DTO keeps exactly the properties EF maps as data (scalar columns, complex/value-object members, primitive collections) and drops navigations, skip navigations, owned references, and
[NotMapped]members. Because it reads the real model, it's correct where a type-shape guess can't be: a same-assembly class stored through a value converter is kept (the model maps it as a column), and a scalar-looking property the model ignores is dropped.IncludePropertiesforces aggregate children (edited with their parent) back in.How it works: the EF model manifest
Facet.Extensions.EFCoreships design-time services that write a manifest ({ContextName}.facetmodel.json) beside the migrations model snapshot on everydotnet ef migrations add/remove, by subclassingMigrationsScaffolderand walking the finalizedIModel. Per entity it records which members EF maps as data (scalar/complex) and which arenav/owned/skipnav/ignored/service. It's committed like the snapshot and stays exactly as fresh as it — the same workflow that keeps migrations honest keeps DTO shapes honest. Register the services with one property in theDbContextproject's csproj — the package ships abuildTransitivetargets file that expands it to the assembly attribute below (EF reads that attribute from theDbContextproject's assembly or the startup assembly, and de-duplicates, so either placement works):The same property also wires the project's own manifests into the compilation as AdditionalFiles, so a single-project setup has exactly one knob: write manifests, consume manifests, shaping on. It is deliberately opt-in — a package reference alone never changes what migrations write or what the compiler consumes — and a coded warning (FACET001) flags the property under
GenerateAssemblyInfo=false, where assembly attributes cannot be emitted.The generator consumes it as an AdditionalFile in the project that declares
[GenerateDtos]— pre-wired byFacetEfDesignTimewhen that is theDbContextproject itself, or one explicit cross-project glob otherwise (a cross-project manifest path is knowledge only the consuming project has, so that step cannot be automated away; duplicate wiring is a pinned no-op — the reader merges per file by union):In a layered solution three different projects are involved (startup / migrations /
[GenerateDtos]), so the glob usually reaches across projects — the README has a Mermaid diagram of the layout and a role table. The manifest is versioned JSON, written withUtf8JsonWriterand read withJsonDocument; each file is parsed atomically (a malformed or future-versionfile is ignored in full, never half-applied), unknown properties are skipped so the schema can grow, and reader equality is structural so formatting-only edits don't bust the incremental cache.No heuristic, no silent fallback
There is deliberately no type-shape heuristic. An earlier revision of this branch shipped one as a zero-setup default; it was removed before merge because it's fragile in exactly the ways the manifest fixes (value-converted columns, cross-assembly entities,
[NotMapped]scalars), and shipping a fragile default only to remove it later would strand adopters. EF is the ORM Facet users overwhelmingly reach for, so requiring its model up front is the right constraint. Consequently every failure is a diagnostic — nothing is ever silently guessed:version(package mismatch)= truethis also catches a mistyped<AdditionalFiles>glob (matches-nothing is silently empty)Wiring the manifest is the project-level switch: a project with a manifest shapes by default, a project without one copies properties as-is, and a rejected-only manifest set does not flip the default — FAC103/FAC104 stand alone instead of being buried under a FAC105 cascade. The obsolete
[GenerateAuditableDtos]is exempt from the default; it cannot express the opt-out. One caveat is documented honestly with a tripwire: under the default, a glob that matches nothing means no wired manifest, no shaping, and no diagnostics — pinning= trueon one representative entity turns that into a hard error.To make FAC106 sound, the manifest records everything the model has an opinion on, including
ignored(explicitly ignored members, recovered from the convention surface across the inheritance chain) andservice(ILazyLoader-style). Computed get-only properties are exempt — the model never maps them. On a large real-world consumer model that recovered 69 ignored members across 128 entities, each a FAC106 false positive otherwise. FAC101–FAC106 anchor to the[GenerateDtos]attribute (a cache-safe location capture on the model), so they land as squiggles and are#pragma-suppressible. FAC105 is already an error; escalate FAC106 for CI with<WarningsAsErrors>$(WarningsAsErrors);FAC106</WarningsAsErrors>and a PR that changes the model without regenerating the manifest can't merge green.Verified end to end against the real-world consumer: the design-time hook produces a 128-entity manifest; a wide entity's two ~37-entry
ExcludePropertieslists collapse to a handful of scalar excludes; generated contracts match the model's designation exactly; and a deliberately broken<AdditionalFiles>path on an explicitly shaped type fails the build with FAC105 rather than silently mis-shaping DTOs. A separate cold-consumer end-to-end (local NuGet feed → fresh console app →dotnet ef→ build) verified the packaged flow: theFacetEfDesignTimeopt-in respected in both directions, the manifest written onmigrations addand survivingremove, implicit shaping with zero AdditionalFiles in the csproj (-getItem:AdditionalFilesshows exactly the package glob's match), FAC105 with the defaulted-mode message on an uncovered type, the= falseescape hatch, and identical output under double wiring.Generator framework: SourceGenerator.Foundations
Reading JSON inside a netstandard2.0 analyzer is possible because this PR also adopts SourceGenerator.Foundations (2.0.16,
PrivateAssets="all"— nothing leaks to consumers). SGF hoists the generator behind a generated wrapper and embeds referenced assemblies (System.Text.Jsonand friends, evenFacet.Attributes.dll) intoFacet.dll, resolving them at load time — the sanctioned way for an analyzer to carry NuGet dependencies into every compiler host, including VS's .NET Framework one — which also retires the manual<Analyzer Include>registration ProjectReference consumers previously needed forFacet.Attributes(cold builds died with CS8784). It brings exception isolation around every callback and a logger (SgfLogLevel). Embedding is trimmed to the runtime closure (Workspaces +System.Compositionexcluded — IDE hosts provide them; embedding Roslyn-family assemblies risks type identity), keeping the analyzer ~4 MB. Driver-based tests instantiate the generated internal hoist (InternalsVisibleTo).Tests
Suite 962/962 (rebased onto master with #397 and #400 merged; includes two facts pinning the integration seam — an assembly-level
[GenerateDtosFor]registration gets the same manifest-driven shaping through the shared pipeline, and an uncovered assembly-level type reports FAC105 anchored at the[assembly:]attribute — plus eleven pinning the wired-manifest default: unset-flag shaping with and without a manifest, implicit FAC105 naming the defaulted mode and the escape hatch, explicit-false opt-out, an empty-but-valid manifest still counting as wired, rejected-only manifests not flipping the default, FAC106 on implicitly shaped types, and the[GenerateAuditableDtos]exemption in both shape and diagnostics). Coverage: generator (driver + in-memory AdditionalFiles) — manifest keep-set drops navs and[NotMapped]scalars, value-converted entity-typed columns survive,IncludePropertiesoverrides a manifest drop, an uncovered type drops nothing (keeps all members) and is FAC105, malformed/unsupported files are ignored atomically with FAC103/FAC104 reported, multi-context manifests union per type; diagnostics — FAC105 error for no-manifest and type-absent (once per type despite flags expansion, attribute-anchored), FAC106 for stale manifest, quiet for computed properties / complete manifests / non-ExcludeNavigationPropertiestypes; manifest writer (real Sqlite model) — scalars/complex/primitive collections kept, nav/owned/skipnav/ignored categorized, shadow members absent, deterministic output; end-to-end through EF's real design-time pipeline —FacetDesignTimeServicesswaps in the scaffolder andScaffoldMigration+Saveproduces the manifest beside the snapshot.🤖 Generated with Claude Code