feat: compile-time CQS handler registration via Roslyn source generator#15
feat: compile-time CQS handler registration via Roslyn source generator#15meinsiedler wants to merge 14 commits intomasterfrom
Conversation
…n to be detected by release tracking
There was a problem hiding this comment.
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) andsoftaware.Cqs.DependencyInjection.SourceGenerator(bundled analyzer) to generate explicit handler/decorator registrations and a static-dispatchIRequestProcessor. - 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.
src/softaware.Cqs.DependencyInjection.SourceGenerated/SoftawareCqsExtensions.cs
Show resolved
Hide resolved
src/softaware.Cqs.DependencyInjection.SourceGenerated/SoftawareCqsExtensions.cs
Outdated
Show resolved
Hide resolved
| /// <returns>The decorator builder for chaining.</returns> | ||
| public SoftawareCqsDecoratorBuilder AddRequestHandlerDecorator(Type decoratorType) | ||
| { | ||
| this.EnabledDecorators.Add(decoratorType); |
There was a problem hiding this comment.
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).
| this.EnabledDecorators.Add(decoratorType); | |
| ArgumentNullException.ThrowIfNull(decoratorType); | |
| var normalizedDecoratorType = decoratorType.IsConstructedGenericType | |
| ? decoratorType.GetGenericTypeDefinition() | |
| : decoratorType; | |
| this.EnabledDecorators.Add(normalizedDecoratorType); |
src/softaware.Cqs.DependencyInjection.SourceGenerator/CqsSourceGenerator.cs
Show resolved
Hide resolved
| 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"); |
There was a problem hiding this comment.
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.
src/softaware.Cqs.DependencyInjection.SourceGenerator/CqsSourceGenerator.cs
Outdated
Show resolved
Hide resolved
src/softaware.Cqs.DependencyInjection.SourceGenerator/CqsSourceGenerator.cs
Outdated
Show resolved
Hide resolved
|
@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>
All review comments addressed in 6027d20:
All 121 tests pass (94 + 17 + 10). |
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
Source generator behaviour
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
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
Testing
Benchmarks