feat: source generator for Brighter handler/mapper/transform registration - #4138
feat: source generator for Brighter handler/mapper/transform registration#4138slang25 wants to merge 30 commits into
Conversation
…ransform registration Adds a Roslyn incremental source generator that emits a partial method body registering handlers, message mappers and transforms discovered in the current compilation, replacing runtime AutoFromAssemblies reflection scanning. Includes: - [BrighterRegistrations] marker attribute on a partial static method - [ExcludeFromBrighterRegistration] opt-out attribute - IBrighterBuilder.Transforms callback for explicit transform registration - HelloWorld sample wired up via AddFromThisAssembly() Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract IsRegistrationCandidate / ClassifyType / TryClassifyGenericInterface / IsTransformInterface from DiscoverRegistrations - Extract IsOpenGeneric helper from EmitHandlers conditional - Replace 7-arg MarkerSymbols constructor with object initializer in Resolve Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Following the Kathleen Dollard incremental-generator pattern: keep semantic model reads in one place, project to a Roslyn-free intermediate model, and write source from that model as a pure function. The writer becomes trivially unit-testable and the model is structurally equatable so the incremental pipeline can cache it. Structure: - Model/RegistrationModel.cs — pure-data records describing the emit - Model/EquatableArray.cs — value-equality wrapper for caching - SemanticModelReader.cs — single point that touches Roslyn symbols - RegistrationWriter.cs — pure model -> source text - MarkerSymbols.cs / Diagnostics.cs — extracted concerns - BrighterRegistrationsGenerator.cs — thin orchestrator/pipeline only - IsExternalInit.cs — polyfill for records on netstandard2.0 Tests (new tests/Paramore.Brighter.SourceGenerators.Tests project): - RegistrationWriterTests — 8 unit tests on the pure writer - BrighterRegistrationsGeneratorTests — 4 validation tests using Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing, asserting full generated source and BRGEN001 diagnostic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n-auto-assemblies # Conflicts: # Brighter.slnx
- TryBuildModel returns a BuildResult struct instead of two out params (5 args -> 3 args) - TryClassifyGenericInterface delegates mapper classification to a TryAddMapper helper, lowering cyclomatic complexity - Introduce a small Same(ISymbol?, ISymbol?) wrapper around SymbolEqualityComparer.Default.Equals to tidy the call sites Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rework the generator so no ISymbol, Compilation, SemanticModel, or SyntaxNode ever escapes a transform. Every value flowing through the incremental graph is now a value-equatable record, which lets Roslyn skip transforms and the source-output step when an edit doesn't change the semantically relevant shape of the compilation. Pipeline: - ForAttributeWithMetadataName.transform projects IMethodSymbol -> MethodCandidate (record holding either a MethodTarget or a DiagnosticInfo). - CreateSyntaxProvider for `class ... : Base` transforms to EquatableArray<DiscoveredEntry> (zero, one, or many records per class). - The discovery batches are Collect()ed and Select()ed into a single flattened, sorted EquatableArray for stable, cache-friendly output. - methodCandidates.Combine(discovered) -> RegisterSourceOutput builds a RegistrationModel from pure data and emits via the unchanged writer. DiagnosticInfo + LocationInfo carry the (path, TextSpan, LinePositionSpan) needed to reconstruct a real Diagnostic at source-output time without holding non-cacheable Roslyn objects in the cache. Tests: - IncrementalCachingTests drives CSharpGeneratorDriver with trackIncrementalGeneratorSteps and asserts on IncrementalStepRunReason: trailing-comment edits and unrelated class additions yield only Cached / Unchanged outputs; adding a real handler yields Modified plus the new handler in the generated source. 15/15 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ference A second pipeline synthesises an `internal static class BrighterAssemblyRegistrations` with an `AddFromThisAssembly()` extension on IBrighterBuilder, populated from the same DiscoveredEntry stream as the attribute-based path. Consumers no longer need to hand-write a partial class. Gating: - The generator only emits the auto class when the MSBuild property BrighterAutoRegistration=true is visible via AnalyzerConfigOptionsProvider. - The new build/Paramore.Brighter.SourceGenerators.props sets that property and declares it CompilerVisible. Because NuGet only applies build/ to *direct* PackageReferences, transitive consumers won't see the property and the auto class won't be generated for them. - Users opt out per project with <BrighterAutoRegistration>false</BrighterAutoRegistration>. Writer: - RegistrationModel/MethodTarget gain an IsPartial flag (default true). The writer omits `partial` on both the class and the method when IsPartial=false, so the auto class is a normal static class. Sample: - HelloWorld drops the hand-written BrighterRegistrations.cs and just calls builder.Services.AddBrighter().AddFromThisAssembly(). It opts in via <BrighterAutoRegistration>true</BrighterAutoRegistration> + CompilerVisibleProperty, because ProjectReference scenarios don't pick up the package's build/ props automatically. Tests: - AutoRegistrationTests verifies property=true emits the class with discovered handlers, property=false suppresses emission, and property-missing (the transitive-consumer scenario) also suppresses emission. 18/18 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review: Source generator for Brighter handler/mapper/transform registrationOverall this is a well-structured generator — the strict separation of 1. Breaking change to
|
Generator surface: - IBrighterBuilder.Transforms is now a BrighterBuilderExtensions extension method, not an interface member, so the original PR no longer makes a binary-breaking change to the public IBrighterBuilder contract. - Closed-generic handlers emit r.Register<TRequest, TImpl>() via the public IAm(Async)SubscriberRegistry interfaces, so the cast to the concrete ServiceCollectionSubscriberRegistry only appears when at least one open generic is present (and then exactly once). - Generated source calls Transforms statically via the fully-qualified BrighterBuilderExtensions, so it doesn't depend on the consumer adding a `using`. - Auto-generated AddFromThisAssembly is `internal` (was `public` on an internal class, which was noisy). Reader / model hardening: - ExcludeFromBrighterRegistrationAttribute is folded into MarkerSymbols so the symbol is resolved once per compilation instead of per class. - IsPrimaryDeclaration sorts DeclaringSyntaxReferences by (FilePath, Start) explicitly rather than relying on undocumented Roslyn ordering. - ReadClass returns a DiscoveryBatch carrying both entries and diagnostics, so a discovery-time warning (BRGEN005) can travel through the cached pipeline alongside the entries. - New BRGEN005 warning fires when a generic class implements a Brighter mapper or transform interface (previously silently dropped). - BuildHintName appends an FNV-1a hash of the original display string so types differing only in non-identifier characters (e.g. Foo.Bar vs Foo_Bar) can't collide. - EquatableArray<T>.Equals/GetHashCode use EqualityComparer<T>.Default, so the public type doesn't NRE if a future caller passes a null element. - DescriptorFor throws on an unknown diagnostic id instead of defaulting to MustBePartial, catching missed updates at test time. Tests (27 passing): - End-to-end tests added for BRGEN002 / BRGEN003 / BRGEN004 / BRGEN005. - End-to-end test for mapper + async mapper + transform discovery. - End-to-end test for partial-class dedup (a handler split across two syntax trees registers exactly once). - End-to-end test that IsClassifiable filters out abstract and private nested handlers. - AutoRegistrationTests asserts Empty diagnostics on the happy path. - RegistrationWriter unit tests updated for the new emit shape and a new case verifies the implementation cast is emitted exactly once when both closed and open-generic handlers are present. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code Review: Source generator for Brighter handler/mapper/transform registrationSolid contribution overall — the architecture is exactly what you want from an incremental generator (Roslyn types confined to Correctness / behavioural concerns
Code quality
Tests
SecurityNo notable concerns. The generator reads only the user's compilation; emitted code uses fully-qualified PerformanceThe incremental pipeline is well-structured. The Nitpicks
Nice work overall. The "verified incrementality" via 🤖 Generated with Claude Code |
…n-auto-assemblies
Records the compile-time registration generator as an opt-in, additive alternative to assembly scanning. Ties the decision to #4159 (the unloaded-assembly blind spot it structurally fixes for first-party types) and #4160 / ADR 0061 (the runtime symptom it reduces reliance on). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — Source generator for handler/mapper/transform registrationOverall this is a high-quality, well-architected PR. The "read → intermediate model → write" split is followed faithfully: Feedback below, ordered by impact. 🔴 Default-on auto-registration collides with the documented manual
|
Replace the hand-rolled whitespace constants in RegistrationWriter with a CodeWriter that subclasses IndentedTextWriter and manages brace blocks (StartBlock/EndBlock), modelled on the ASP.NET Core source generators' CodeWriter. Pure structural change — generated output is byte-identical, as verified by the exact-snapshot generator tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review: source generator for handler/mapper/transform registrationOverall this is a high-quality, well-structured contribution. The Kathleen Dollard "read → model → write" split is followed cleanly, the model layer is genuinely Roslyn-free and value-equatable, and the incremental-caching tests verify the contract that matters (asserting on
A few things worth addressing, roughly in priority order. 1. Correctness — partial class whose base/interface list is on a non-primary file is silently droppedIn
These interact badly. Consider a
2. Limitation — only
|
Add a shared GeneratedSource helper (banner + tool name/version + the [System.CodeDom.Compiler.GeneratedCode] attribute), modelled on the ASP.NET Core source generators. The registration writer now emits the auto-generated banner and stamps [GeneratedCode] on the generated method; the post-init attributes file gets the same banner. Snapshot tests build their expected output from GeneratedSource so they track the production banner and the tool version dynamically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — Source generator for handler/mapper/transform registrationThorough, well-structured PR. The reader→model→writer separation is exactly the right shape for an incremental generator, the value-equatable record pipeline is done properly, and the test suite (28 tests, including Correctness / behavior
Packaging gap
Minor / cleanups
Nits
SecurityNo concerns — compile-time only, all emitted type names originate from the compilation symbol table (fully-qualified, Test coverageStrong. The one thing I'd add is an automated end-to-end test that the generated registrations actually resolve and dispatch through a real Reviewed statically; I was unable to run 🤖 Generated with Claude Code |
Name the four pipeline stages with WithTrackingName and rewrite the caching tests to assert the model stages are Cached/Unchanged (never Modified) on an unrelated edit, with the post-Collect stages specifically Cached. This catches the classic non-incremental bug where a pipeline value loses value equality — verified by temporarily breaking EquatableArray equality and watching the tests go red. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review: Source generator for handler/mapper/transform registrationI reviewed this against the repo's conventions (CLAUDE.md / Comments below are mostly forward-looking / nits — nothing I'd consider blocking. Code quality / correctness — looks solid
Things worth flagging before this ships (largely already acknowledged in ADR 0062)
Minor / nits
Test coverageStrong. The Nice work — this is a well-structured feature. 🤖 Generated with Claude Code |
- Pin Microsoft.CodeAnalysis.CSharp to 4.8.0 via VersionOverride (first .NET 8 Roslyn, the oldest in-support modern .NET) so the generator loads on that SDK and newer. 4.8.0's bundled analyzers don't trip the RS2002 that 4.3.1 did. - RegistrationModel now composes MethodTarget instead of duplicating its 12 fields (17 ctor args -> 6), removing the duplication and the Excess-Arguments finding. - DescriptorFor: switch -> dictionary lookup (cyclomatic 9 -> ~2). - IsClassifiable: split the 3-way || into single-condition guards (no complex conditional). - ClassifyEntries: extract TryReportNestedInOpenGeneric so it no longer carries a second block of nested conditionals (Bumpy Road). Pure structural change — generated output byte-identical; all 34 tests pass; generator and HelloWorld sample build clean against Roslyn 4.8.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — Source generator for handler/mapper/transform registrationReviewed the full diff (generator, DI changes, sample, tests). This is genuinely high-quality work — the architecture, incrementality discipline, and test depth are all well above the bar for a Roslyn generator. Comments below, grouped by severity. Nothing here is a blocker; the medium item is the only one I'd ask be addressed (even if only with a diagnostic) before merge. What's strong
Medium — manual
|
Undo two changes made purely to satisfy CodeScene thresholds where the original code was clearer: - AccessibilityModifier: back to a switch expression (a closed enum -> string map) instead of a static Dictionary + TryGetValue. - IsClassifiable: back to a single 'IsAbstract || IsImplicitClass || IsAnonymousType' guard instead of three separate ifs. These reintroduce the 'Complex Conditional' (IsClassifiable) and contribute to 'Overall Code Complexity' (SemanticModelReader) findings, which are false positives on this kind of code and should be suppressed in CodeScene rather than worked around. The genuine improvements from the complexity pass (RegistrationModel composing MethodTarget, the shared ClassifyInterface dedup) are kept. Output byte-identical; all 34 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Gates Failed
Enforce advisory code health rules
(2 files with Excess Number of Function Arguments, Complex Method, Complex Conditional)
Our agent can fix these. Install it.
Gates Passed
3 Quality Gates Passed
Reason for failure
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| SemanticModelReader.cs | 2 advisory rules | 9.39 | Suppress |
| RegistrationModel.cs | 1 advisory rule | 9.69 | Suppress |
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
| private static string AccessibilityModifier(Accessibility accessibility) => accessibility switch | ||
| { | ||
| Accessibility.Public => "public", | ||
| Accessibility.Internal => "internal", | ||
| Accessibility.Private => "private", | ||
| Accessibility.Protected => "protected", | ||
| Accessibility.ProtectedOrInternal => "protected internal", | ||
| Accessibility.ProtectedAndInternal => "private protected", | ||
| _ => "internal" | ||
| }; |
There was a problem hiding this comment.
❌ New issue: Complex Method
AccessibilityModifier has a cyclomatic complexity of 9, threshold = 9
| { | ||
| if (type.TypeKind != TypeKind.Class) | ||
| return false; | ||
| if (type.IsAbstract || type.IsImplicitClass || type.IsAnonymousType) |
There was a problem hiding this comment.
❌ New issue: Complex Conditional
IsClassifiable has 1 complex conditionals with 2 branches, threshold = 2
Code Review — Source generator for handler/mapper/transform registrationReviewed the full diff (29 files). High-quality, well-architected contribution. Strengths first, then issues by severity. Strengths
Issues1. (Major — but acknowledged) The NuGet distribution path described in the PR doesn't actually work yet. 2. (Minor bug / unguarded gap) A 3. (Minor) 4. (Design question) The auto-generated 5. (Nits)
VerdictArchitecturally excellent and well-tested. The only true follow-up blocker for the advertised feature is #1 (packaging), which the ADR already calls out. Items #2–#4 are worth addressing or documenting before this is positioned as the recommended replacement for Note: |
|
@slang25 I note that NServiceBus have been moving along with this: https://particular.net/blog/preparing-for-trimming-and-aot |
Registration artefact changes from generated imperative IBrighterBuilder method bodies to an inert catalog in core, applied by a new AddRegistrations extension in the DI package. Records the multi-assembly / domain-project motivation, the declared partial-class holder form, and moves the original design to Alternatives as superseded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review — Source generator for handler/mapper/transform registrationThanks for this — it's an unusually well-engineered PR. The incremental-generator architecture is textbook (Roslyn-free value-equatable pipeline, a pure Below is feedback grouped by significance. Most of the larger items are already tracked in ADR-0062's "Deferred follow-ups", so this is mostly confirmation + a couple of things worth watching. Architecture / design
Behavioural parity with
|
- Auto path becomes a synthesised default holder (BrighterRegistrations in
the root namespace); AddFromThisAssembly is dropped (Alternative 7) so
there is one registration verb and one generated output shape.
- Phase 2 designed: [RegistrationGroup] named groups (build-time evaluated,
may overlap) and opt-in GenerateBuilderExtensions fluent sugar with the
mechanical Add{Name}Registrations rule and its DI-coupling trade.
- AddRegistrations applies with set semantics: exact duplicates union as
no-ops, found via a prototype where overlapping groups double-registered
a handler and failed at dispatch.
- Catalog v1 surface gains the Matching/Without combinator pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two-project sample (core-only domain + composing host) that fakes the ADR 0062 generated output by hand so the proposed API can be felt at real call sites: flat catalog, named [RegistrationGroup] scoops, runtime combinators, and the Phase-2 fluent extensions. Variant 6 demonstrates the overlapping-groups union that motivated set semantics. Clearly marked throwaway; intended to be dropped from the branch before merge (kept as one commit so a single revert removes it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR Review —
|
Claude Code Review — PR #4138Reviewed against The review below assumes the two caveats the PR description already flags: (1) the branch still implements the superseded imperative design (ADR Alt 5) rather than the 🔴 Merge blockers / design status
🟡 Correctness / edge cases
🟢 Minor / nits
✅ Tests34 tests across writer unit tests, end-to-end generator/diagnostic tests (BRGEN001–006), auto-registration gating, partial-class dedup, record mappers, and tracked-stage incrementality — strong coverage for the current shape. Gaps to add when the design settles: multiple-holder-method, nested/generic holder, and (once packaging lands) an actual direct-vs-transitive PackageReference integration test. Overall: high-quality generator engineering, but not mergeable in its current form — the emit shape needs to catch up to the approved ADR 0062, and the prototype sample needs removing. Consider landing the reader/writer/model/caching foundation as a separate PR. 🤖 Generated with Claude Code |
Review: source generator for handler/mapper/transform registrationThanks for this — it's a genuinely high-quality piece of work. The reader→model→writer split, the value-equatable pipeline, and (especially) the verified incrementality via 🚦 Merge-readiness (author-acknowledged)The PR description already flags these, so I'm just confirming they're the gating items:
I'd suggest splitting the ADR (which is valuable and near-final) from the implementation so the design can land while the catalog migration continues. 🐛 /
|
…review # Conflicts: # Directory.Packages.props
…lies' into quito/source-gen-review
A [BrighterRegistrations] method declared in a nested or generic type was emitted as a top-level partial of the same simple name -- a different type that never implements the user's method (CS8795), with no diagnostic. Reject these placements with BRGEN008 rather than emit broken source, consistent with the generator's existing diagnostic-driven handling of unsupported shapes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review: source generator for handler/mapper/transform registration (1/2 — correctness & packaging)This is a genuinely well-built generator. The read → model → write separation is real, not just claimed: Noting the PR body's own caveat that this implements the superseded imperative design and that 🔴 1. Built-in pipeline handlers are never registered —
|
Review (2/2 — smaller notes, test coverage, summary)🟡 Smaller things
🧪 Test coverageWhat's covered is covered well — all seven diagnostics, records-as-mappers, partial-with-base-list-on-secondary-file, generic-split-across-partials dedup, abstract/private-nested filtering, and three real incrementality assertions including a positive control. Gaps:
SummaryArchitecture and incrementality work are solid and should carry over to the catalog design largely intact. The two I'd treat as blocking: item 1 (framework pipeline handlers), which breaks policy/logging attributes silently, and the missing round-trip test, which is the only thing that would systematically catch parity gaps like it. Items 2 and 3 are narrower but produce uncompilable output instead of a diagnostic, which is the failure mode a generator should never have. Happy to look again once the writer/pipeline moves to |
Review items from PR #4138: - Register Brighter's framework pipeline handlers from generated code via a new EnsureFrameworkHandlersRegistered builder method + off-interface extension, so [UsePolicy]/[RequestLogging]/[Fallback]/[Timeout] resolve - Reject file-local types in discovery (they can't be referenced from the generated file) - Report BRGEN009 when [BrighterRegistrations] is used but Brighter isn't referenced, instead of silently generating nothing - Normalize the attribute source's line endings to LF - Add WithTrackingName to the discovery-diagnostics stage and cover it in the incremental caching tests - Make analyzer support types internal with InternalsVisibleTo (strong-name key) and sign the test assembly - Make the analyzer project packable: dll under analyzers/dotnet/cs - Pin the test project to the generator's Roslyn 4.8.0 floor - ADR 0062: add YAML frontmatter, framework-handler parity note, off-interface-throw consequence, vendor-namespace note - New tests: open-generic end-to-end (unbound typeof arity), runtime round-trip parity vs assembly scanning, duplicate-mapper pinning Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review: source generator for handler/mapper/transform registration (1/2) Nice piece of work overall — the read → model → write split is textbook, Below: one correctness bug, a parity gap, a test blind spot, and a convention violation. Smaller notes follow in a second comment. 1.
Open-generic pipeline handlers (
Those go via Failure scenario, which is literally the new HelloWorld sample's call shape: builder.Services.AddBrighter() // registers FireSchedulerRequest -> FireSchedulerRequestHandler
.AddFromThisAssembly(); // EnsureFrameworkHandlersRegistered() registers it again
In fairness, 2. The generated path registers
public class GreetingHandler : RequestHandler<GreetingCommand> { }
internal class LegacyGreetingHandler : RequestHandler<GreetingCommand> { } // dead todayThat works under 3.
Asserting on observed dispatch (build the container and 4.
I get the pull — 5. Branch state — code doesn't implement its own ADR Recording your own note as a merge gate: the ADR is |
|
Review: source generator for handler/mapper/transform registration (2/2) — smaller findings Registration is not idempotent, and nothing warns. Two Generated line endings are platform-dependent.
Auto class lands in
Discovery predicate cost. Auto-registration path has no incrementality coverage. Style / conventions
Security / AOT Nothing concerning. Generated code is fully qualified with I wasn't able to build or run the test suite in this environment, so findings 1–3 in the previous comment come from reading the code rather than from a red test; #1 and #3 in particular are worth confirming with a quick Happy to re-review once the catalog migration lands — the pipeline architecture underneath is solid and shouldn't need to change much for it. |
Built throwaway solutions against a packed build of the generator to see how the feature behaves outside the test suite. Several cases either broke the build in ways a consumer could not opt out of, or — worse for a feature whose premise is "failures move to build time" — registered nothing and said nothing. Marker attributes move into core Paramore.Brighter instead of being emitted. The post-init file was unconditional, so it landed in every compilation the analyzer touched: it carried "#nullable enable", which is error CS8370 below C# 8 (the default LangVersion for netstandard2.0 and net48), it reached transitive consumers with no Brighter reference of their own, and two assemblies sharing internals declared conflicting copies (CS0436). Emitted registration source drops the directive too, since it uses no nullable annotations — so the auto form now works at LangVersion 7.3. The auto form is suppressed when there is nothing to register. An empty AddFromThisAssembly() compiles, reads at the call site as though it registered the solution's handlers, and registers nothing; leaving it out makes that a compile error instead. SubscriberRegistry.Add treats an exact-duplicate (request, handler) pair as a no-op. Without it the two mechanisms the ADR calls "additive" were not: AutoFromAssemblies() alongside a generated method registered shared handlers twice and failed at dispatch. The routing overload is untouched, and two different handlers for one request still collide. Five diagnostics for cases that were silent: BRGEN010 (auto-registration on but Brighter not fully referenced — the layered domain library), BRGEN011 (another assembly exposes BrighterAssemblyRegistrations via InternalsVisibleTo, making every call ambiguous), BRGEN012 (a non-event request with more than one handler, counted separately for sync and async), BRGEN013 (a non-boolean BrighterAutoRegistration value) and BRGEN014 (the compilation already declares the auto class itself). Also: protected internal nested handlers are now discovered, since the generated holder shares their assembly and can name them. The ADR's trimming/AOT claim is weakened to "paves the way". A PublishAot app using the generated registration builds, links, and then throws on the first Send because the handler constructor was trimmed; the remaining blockers are recorded there and tracked separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prototype answered its question — the conclusions are recorded in ADR 0062 — but it is not referenced from Brighter.slnx, so nothing builds it and it would quietly rot. It also ships hand-written .g.cs files, which read as generator output without being any. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review: source generator for handler/mapper/transform registrationI read the generator, the model/writer split, the DI and core changes, ADR 0062, and all five test files. This is unusually high-quality work: the read → model → write split genuinely holds ( 1. The set-semantics fix is only half-applied — mappers still throw on exact duplicates
But services.AddBrighter().AutoFromAssemblies().AddFromThisAssembly();— throws 2. Handler-visibility divergence breaks the parity claim
So the generator registers handlers the scanner never would. An assembly with a public 3.
|
Five findings from the review on BrighterCommand#4138, most consequential first. Mapper registration is now a set, like handler registration. `SubscriberRegistry.Add` was changed to treat a repeated (request, handler) pair as a no-op, but `ServiceCollectionMessageMapperRegistryBuilder.Add`/`AddAsync` still threw on a repeated key even when the mapper type was identical — so the union case that change exists to enable (`AutoFromAssemblies().AddFromThisAssembly()`, or two generated holders called from one composition root) threw at startup for any assembly with a mapper. An exact re-add is now a no-op; a *different* mapper for the same message is still a conflict and still throws. Non-public handlers are no longer registered. The generator registered `internal` and `protected internal` handlers that the reflection scanner skips — and that core's own pipeline validation rejects as an error ("Brighter only supports public handler types"). An assembly with a public handler plus an internal test double for the same command worked under `AutoFromAssemblies()` and would have started failing `Send` with "More than one handler was found" purely by switching mechanisms. Handlers now match the scanner exactly; skipped ones report the new BRGEN015 rather than disappearing quietly. Mappers and transforms are unfiltered, as before. `EnsureFrameworkHandlersRegistered` is gone, call and public API both. Every route to an IBrighterBuilder runs through `BrighterHandlerBuilder`, which already scans core Brighter for the pipeline handlers, so the generated call was a redundant reflection sweep of the core assembly at every startup, once per generated method. Smaller: BRGEN013 raised to an error (a typo'd BrighterAutoRegistration otherwise leaves a green build registering nothing); BRGEN012 reported once per compilation instead of once per registration holder; `CodeWriter` pins NewLine to "\n" so emitted source doesn't vary by OS; the descriptor lookup is derived from `Diagnostics` by reflection rather than restated by hand; `EquatableArray.GetHashCode` caches; the no-namespace branch no longer indents a class that has no enclosing block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
Adds
Paramore.Brighter.SourceGenerators: a Roslyn incremental source generator that emits handler / message-mapper / transform registrations at compile time, as an alternative to runtimeAutoFromAssembliesreflection scanning. The generator follows the recommended "read → intermediate model → write" structure and runs as a properly incremental pipeline (verified via tests onIncrementalStepRunReason).Consumers can either:
internal static class BrighterAssemblyRegistrationsis auto-generated with anAddFromThisAssembly()extension onIBrighterBuilder. The opt-in flows via abuild/props file so it applies only to direct PackageReferences, not transitive ones. Override per-project with<BrighterAutoRegistration>false</BrighterAutoRegistration>.static partialmethod marked with[BrighterRegistrations]and the generator fills in the body.[ExcludeFromBrighterRegistration]opts a single type out either way. A newIBrighterBuilder.Transforms(...)callback onServiceCollectionBrighterBuilderis added so transforms can be registered explicitly (symmetric withHandlers/MapperRegistry). The HelloWorld sample exercises both: the auto-generated path plus aNoOpTransformerfor transform discovery.Related Issues
Type of Change
Checklist
Additional Notes
Replaces #4127 (which was opened from my fork).
Architecture follows the Kathleen Dollard incremental-generator pattern:
SemanticModelReaderis the only place that touches Roslyn symbols and projects everything to Roslyn-free records;RegistrationWriteris a pureRegistrationModel → stringfunction and is exhaustively unit-tested without aCompilation. Diagnostics are carried through the pipeline asDiagnosticInfo+LocationInfo(value-equatable) and rebuilt at source-output time.Pipeline incrementality is verified, not just structural:
IncrementalCachingTestsdrivesCSharpGeneratorDriverwithtrackIncrementalGeneratorSteps: trueand asserts onIncrementalStepRunReason— trailing-comment edits and unrelated class additions yield onlyCached/Unchangedoutputs; adding a real handler yields exactly oneModifiedsource output containing the new handler.Design update (2026-07-12)
API exploration (prompted by the multi-assembly / domain-project problem with
AddFromThisAssembly, and informed by NServiceBus 10.2's source-generated registration) has landed on a revised design, now recorded in ADR 0062:RegistrationCatalog— an inert data type in coreParamore.Brighter, so handler-owning assemblies need no DI/Polly reference. The generator emits catalog data only; all application logic lives in the library.AddRegistrations(params RegistrationCatalog[])— one registration verb in the DI package, applying with set semantics (exact duplicates union as no-ops).[BrighterRegistrations] public static partial class OrdersRegistrations;; the zero-config auto path synthesises a default holder instead of theAddFromThisAssembly()sugar (dropped: see ADR Alternative 7).[RegistrationGroup]named convention scoops and opt-inGenerateBuilderExtensionsfluent sugar.samples/PROTOTYPE-RegistrationCatalog/is a clearly-marked throwaway two-project sample that fakes the generated output so the proposed API can be felt at real call sites — it will be reverted off the branch before merge.