feat: add TsInterfaceName property to GenerateDtosFor attribute - #408
Conversation
|
Hi @dkattan There's 2 other big PR's you created still in draft. Are these related or dependant? This one is marked as ready? What's the plan here? |
|
@dkattan ? |
Hey sorry went on vacation and lost stream. As I try to adopt Facet I discover myself/AI adding features to Facet that may be over-fitted to my scenario. Like in this case, we use ReinforcedTypings with a custom TS client generator. But I feel like most sane projects use OpenAPI or GraphQL or OData. Also, this PR appears to be a dumping ground for a ton of independently mergeable features. Let me get it split out. |
40fda61 to
0667f44
Compare
0667f44 to
ff5a668
Compare
PR SplitThis PR has been split into focused, independently mergeable PRs:
This PR (#408) now focuses on the primary feature: TsInterfaceName property on GenerateDtosFor attribute, plus the supporting DTO generation features (RenameProperties, DtoPreset, PropertySuffix, GenerateReadOnlyProperties). 🤖 Generated with Copilot |
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>
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>
…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>
…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).
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>
…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>
When set, the generator emits [TsInterface(Name = ...)] on the generated DTO class for Reinforced.Typings. This allows controlling the TypeScript interface name from the Facet attribute without requiring a hand-written attribute in the partial class. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ff5a668 to
dc18293
Compare
Summary
Adds a TsInterfaceName property to GenerateDtosForAttribute. When set, the generator emits a TsInterface attribute on the generated DTO class for Reinforced.Typings.
Changes
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com