Skip to content

feat: compile-time CQS handler registration via Roslyn source generator#15

Open
meinsiedler wants to merge 14 commits intomasterfrom
feature/compile-time-resolve
Open

feat: compile-time CQS handler registration via Roslyn source generator#15
meinsiedler wants to merge 14 commits intomasterfrom
feature/compile-time-resolve

Conversation

@meinsiedler
Copy link
Copy Markdown
Member

@meinsiedler meinsiedler commented Apr 12, 2026

Summary

This PR introduces \softaware.CQS.DependencyInjection.SourceGenerated\ — a compile-time alternative to \softaware.CQS.DependencyInjection\ that uses a Roslyn \IIncrementalGenerator\ instead of Scrutor-based runtime scanning to register CQS handlers and decorators.

Motivation

The existing \softaware.CQS.DependencyInjection\ package uses Scrutor to scan assemblies at startup and register handlers via reflection. The source-generated approach moves this work entirely to compile time, producing explicit \IServiceCollection\ registrations with no startup overhead and no runtime reflection.

What's new

New packages / projects

Project Description
\softaware.Cqs.DependencyInjection.SourceGenerator\ The \IIncrementalGenerator\ (not published separately — bundled into the SourceGenerated package as an analyzer)
\softaware.Cqs.DependencyInjection.SourceGenerated\ The NuGet package users reference. Contains the runtime API stubs and bundles the generator.
\softaware.Cqs.DependencyInjection.SourceGenerator.Tests\ 18 unit tests for the generator using \CSharpGeneratorDriver\
\softaware.Cqs.Benchmarks\ BenchmarkDotNet project comparing runtime vs. source-generated DI setup

Source generator behaviour

  • Reads \AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(...)))\ from the syntax tree
  • Discovers all \IRequestHandler\ implementations in the marker type's assembly
  • Evaluates generic type constraints to determine which decorators apply to each handler
  • Generates \CqsServiceRegistration.g.cs\ with explicit \ActivatorUtilities.CreateInstance\ chains
  • Generates \GeneratedRequestProcessor.g.cs\ for static type-pattern dispatch (registered as \IRequestProcessor)

Conditional decorator registration

Decorators inside \if/\switch\ blocks are supported. The generator marks them as conditional and emits \if (registry.IsEnabled(typeof(...)))\ guards. The \AddDecorators\ lambda is executed at runtime to populate a \CqsDecoratorRegistry\ singleton.

Diagnostics

ID Severity Description
\CQ0004\ Warning Convenience method (e.g. \AddTransactionCommandHandlerDecorator) detected. Use \AddRequestHandlerDecorator(typeof(...))\ instead.
\CQ0005\ Warning No \AddSoftawareCqs\ call found. The generator has nothing to generate.
\CQ0006\ Warning Core CQS types (\IRequestHandler, \IRequest, \IRequestProcessor) could not be resolved. Ensure \softaware.CQS\ is referenced.
\CQ0007\ Info Generation succeeded. Shows handler and decorator counts. Only visible in IDE Error List (with Info filter) or \dotnet build -v detailed.
\CQ0008\ Error Argument to \IncludeTypesFrom\ or \AddRequestHandlerDecorator\ is not a \ ypeof()\ expression.
\CQ0009\ Error Handler uses an open generic request type (e.g. \MyRequest). Not supported in this version.
\CQ0010\ Info \AddRequestHandlerDecorator\ inside a conditional block — will use runtime registry check.
\CQ0011\ Error Decorator has generic type parameters that could not be mapped to \TRequest/\TResult\ from \IRequestHandler<TRequest, TResult>.
\CQ0012\ Warning A method other than \AddRequestHandlerDecorator\ was called inside \AddDecorators. The call is ignored by the source generator.

API

The API is intentionally identical to the runtime package, with one constraint:

\\csharp
// Runtime package syntax — NOT supported by source generator
services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyHandler).Assembly));

// Source-generated syntax
services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyHandler)));
\\

Documentation

  • [\src/softaware.Cqs.DependencyInjection.SourceGenerated/README.md](src/softaware.Cqs.DependencyInjection.SourceGenerated/README.md) — full migration guide, limitations, and debugging tips
  • Main \README.md\ updated with a new subsection and table of contents

Testing

  • 18 generator tests covering handler discovery, decorator constraints, conditional decorators, open generic errors, and snapshot assertions (all green)
  • All 94 existing tests still pass (no regressions)
  • Benchmark project validates both DI approaches end-to-end via --validate\ flag

Benchmarks

Method Mean Error StdDev Gen0 Gen1 Allocated
'Generated: Execute SimpleCommand' 89.18 ns 1.434 ns 1.341 ns 0.0126 - 160 B
'Generated: Execute GetGreeting' 97.43 ns 1.799 ns 1.683 ns 0.0280 - 352 B
'Generated: Execute GetSquare' 97.67 ns 1.918 ns 1.883 ns 0.0242 - 304 B
'Generated: Execute AccessCheckedCommand' 116.20 ns 0.936 ns 0.875 ns 0.0172 - 216 B
'Runtime: Execute GetSquare' 861.13 ns 9.537 ns 8.455 ns 0.1554 - 1960 B
'Runtime: Execute SimpleCommand' 872.89 ns 15.315 ns 15.041 ns 0.1383 - 1744 B
'Runtime: Execute AccessCheckedCommand' 886.42 ns 7.956 ns 7.053 ns 0.1431 - 1800 B
'Runtime: Execute GetGreeting' 932.61 ns 17.561 ns 15.567 ns 0.1564 - 2008 B
'Generated: DI Container Build' 1,287.30 ns 18.194 ns 15.193 ns 0.6104 0.1011 7680 B
'Runtime: DI Container Build' 223,943.54 ns 1,809.969 ns 1,693.046 ns 5.8594 - 78085 B

@meinsiedler meinsiedler changed the title feat: compile-time CQS handler registration via source generator feat: compile-time CQS handler registration via Roslyn source generator Apr 12, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a compile-time (Roslyn source generator–based) alternative to the existing runtime/Scrutor-based CQS DI registration, plus supporting tests, benchmarks, and documentation updates.

Changes:

  • Introduces softaware.Cqs.DependencyInjection.SourceGenerated (consumer-facing package) and softaware.Cqs.DependencyInjection.SourceGenerator (bundled analyzer) to generate explicit handler/decorator registrations and a static-dispatch IRequestProcessor.
  • Adds a dedicated generator test project and a BenchmarkDotNet project to validate and measure runtime vs. generated DI setup.
  • Updates solution/pipeline/docs and moves test projects to .NET 10 tooling to align with the new additions.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/softaware.Cqs.Tests/softaware.Cqs.Tests.csproj Updates test TFM to .NET 10.
src/softaware.Cqs.Tests/Fakes/MockTelemetryChannel.cs Minor modernization (sealed + collection expression).
src/softaware.Cqs.Tests/CQ.Handlers/QueryHandlers/SimpleQueryHandler.cs Formatting-only change.
src/softaware.Cqs.Tests/CQ.Handlers/CommandHandlers/CommandWithDependencyHandler.cs Uses primary constructor + simplified field usage.
src/softaware.Cqs.sln Adds new projects and additional build configurations.
src/softaware.Cqs.DependencyInjection.SourceGenerator/softaware.Cqs.DependencyInjection.SourceGenerator.csproj New Roslyn generator project configuration.
src/softaware.Cqs.DependencyInjection.SourceGenerator/DiagnosticDescriptors.cs Defines generator diagnostics CQ0002–CQ0010.
src/softaware.Cqs.DependencyInjection.SourceGenerator/CqsSourceGenerator.cs Implements incremental generator, discovery, diagnostics, and codegen.
src/softaware.Cqs.DependencyInjection.SourceGenerator/CqsConfiguration.cs New config/DTO types for extracted configuration and discovered handlers.
src/softaware.Cqs.DependencyInjection.SourceGenerator/AnalyzerReleases.Unshipped.md Declares new analyzer rules for release tracking.
src/softaware.Cqs.DependencyInjection.SourceGenerator/AnalyzerReleases.Shipped.md Initializes shipped analyzer release tracking file.
src/softaware.Cqs.DependencyInjection.SourceGenerator.Tests/TestHelper.cs Adds helper for running generator in unit tests.
src/softaware.Cqs.DependencyInjection.SourceGenerator.Tests/softaware.Cqs.DependencyInjection.SourceGenerator.Tests.csproj New generator test project (net10.0) and references.
src/softaware.Cqs.DependencyInjection.SourceGenerator.Tests/GlobalSuppressions.cs Suppresses naming analyzer rule for test namespace.
src/softaware.Cqs.DependencyInjection.SourceGenerator.Tests/CqsSourceGeneratorTests.cs Adds unit tests covering discovery, decorators, diagnostics, and snapshots.
src/softaware.Cqs.DependencyInjection.SourceGenerated/SoftawareCqsExtensions.cs Adds consumer API entrypoint that invokes generated registrations.
src/softaware.Cqs.DependencyInjection.SourceGenerated/SoftawareCqsDecoratorBuilder.cs Adds runtime decorator tracking for conditional decorators.
src/softaware.Cqs.DependencyInjection.SourceGenerated/SoftawareCqsBuilder.cs Adds AddDecorators runtime behavior to populate decorator registry.
src/softaware.Cqs.DependencyInjection.SourceGenerated/softaware.Cqs.DependencyInjection.SourceGenerated.csproj New packable package that bundles generator as analyzer output.
src/softaware.Cqs.DependencyInjection.SourceGenerated/README.md New package README with migration guide, limitations, and debugging tips.
src/softaware.Cqs.DependencyInjection.SourceGenerated/CqsDecoratorRegistry.cs Adds decorator registry used by generated factories for conditionals.
src/softaware.Cqs.DependencyInjection.SourceGenerated/build/softaware.CQS.DependencyInjection.SourceGenerated.targets Exposes MSBuild property for generator debugging.
src/softaware.Cqs.Benchmarks/SourceGeneratedSetup.cs Adds source-generated DI setup used by benchmarks.
src/softaware.Cqs.Benchmarks/softaware.Cqs.Benchmarks.csproj New BenchmarkDotNet project (net10.0) with aliased references.
src/softaware.Cqs.Benchmarks/RuntimeSetup.cs Adds runtime/Scrutor DI setup used by benchmarks.
src/softaware.Cqs.Benchmarks/Program.cs Benchmark runner plus --validate validation mode.
src/softaware.Cqs.Benchmarks/Handlers/Handlers.cs Adds benchmark handler implementations.
src/softaware.Cqs.Benchmarks/GlobalUsings.cs Adds global using for DI.
src/softaware.Cqs.Benchmarks/GlobalSuppressions.cs Suppresses naming analyzer rule for benchmark namespace.
src/softaware.Cqs.Benchmarks/Decorators/Decorators.cs Adds benchmark decorator implementations with various constraints.
src/softaware.Cqs.Benchmarks/CqsBenchmarks.cs Adds runtime vs generated performance benchmarks.
src/softaware.Cqs.Benchmarks/Contracts/Queries/Queries.cs Adds benchmark query contracts.
src/softaware.Cqs.Benchmarks/Contracts/Interfaces.cs Adds benchmark marker interfaces used by decorator constraints.
src/softaware.Cqs.Benchmarks/Contracts/Commands/Commands.cs Adds benchmark command contracts.
src/softaware.Cqs.Analyzers/softaware.Cqs.Analyzers.csproj Bumps analyzer package version and updates Roslyn dependencies.
src/softaware.Cqs.Analyzers/IRequestShouldNotBeImplementedDirectlyAnalyzer.cs Modernizes DiagnosticDescriptor construction.
src/softaware.Cqs.Analyzers.Tests/softaware.Cqs.Analyzers.Tests.csproj Updates analyzer test TFM and Roslyn testing dependencies.
README.md Adds TOC and documents the new source-generated DI package.
CHANGELOG.md Adds entry for the new SourceGenerated package.
azure-pipelines.yml Updates CI to use .NET 10 SDK.

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

/// <returns>The decorator builder for chaining.</returns>
public SoftawareCqsDecoratorBuilder AddRequestHandlerDecorator(Type decoratorType)
{
this.EnabledDecorators.Add(decoratorType);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The registry is later queried with typeof(OpenGenericDecorator<,>) in the generated factories, but AddRequestHandlerDecorator currently stores the type as-is. If a consumer accidentally passes a constructed generic (e.g. typeof(MyDecorator<MyReq, MyRes>)), the registry lookup won’t match and conditional decorators won’t apply. Consider normalizing generic types to GetGenericTypeDefinition() before storing (or validating and throwing).

Suggested change
this.EnabledDecorators.Add(decoratorType);
ArgumentNullException.ThrowIfNull(decoratorType);
var normalizedDecoratorType = decoratorType.IsConstructedGenericType
? decoratorType.GetGenericTypeDefinition()
: decoratorType;
this.EnabledDecorators.Add(normalizedDecoratorType);

Copilot uses AI. Check for mistakes.
Comment on lines +340 to +344
var requestHandlerType = compilation.GetTypeByMetadataName("softaware.Cqs.IRequestHandler`2");
var requestType = compilation.GetTypeByMetadataName("softaware.Cqs.IRequest`1");
var commandType = compilation.GetTypeByMetadataName("softaware.Cqs.ICommand`1");
var queryType = compilation.GetTypeByMetadataName("softaware.Cqs.IQuery`1");
var requestProcessorType = compilation.GetTypeByMetadataName("softaware.Cqs.IRequestProcessor");
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

commandType and queryType are resolved but never used. Removing them would avoid warnings and keep the generator logic focused; alternatively, if they’re intended for future constraint checks, consider using them or adding a comment explaining why they’re needed.

Copilot uses AI. Check for mistakes.
@meinsiedler
Copy link
Copy Markdown
Member Author

@copilot apply changes based on the comments in this thread

…ize generic types, add CQ0011 diagnostic, remove unused variables

Agent-Logs-Url: https://github.com/softawaregmbh/library-cqs/sessions/095b89d0-64a2-4201-86b5-d6ee04d7622f

Co-authored-by: meinsiedler <13835497+meinsiedler@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented Apr 13, 2026

@copilot apply changes based on the comments in this thread

All review comments addressed in 6027d20:

  1. SoftawareCqsExtensions.cs:34 — Added ArgumentNullException for null softawareCqsTypesBuilderAction
  2. SoftawareCqsExtensions.cs:41 — Added explicit signature validation for RegisterAll method with clear InvalidOperationException
  3. SoftawareCqsDecoratorBuilder.cs:26 — Added null check + GetGenericTypeDefinition() normalization for constructed generics
  4. CqsSourceGenerator.cs:246-256 — Runtime normalization now matches compile-time OriginalDefinition usage, keeping decorator registry matching consistent
  5. CqsSourceGenerator.cs:340-344 — Removed unused commandType and queryType
  6. CqsSourceGenerator.cs:879-882 — Removed unused requestHandlerDef
  7. CqsSourceGenerator.cs:909-913 — Replaced silent fallback with CQ0011 diagnostic; added to AnalyzerReleases.Unshipped.md and both README files

All 121 tests pass (94 + 17 + 10).

@meinsiedler meinsiedler marked this pull request as ready for review April 13, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants