A compile-time alternative to softaware.Cqs.DependencyInjection that uses a Roslyn source generator instead of Scrutor-based runtime reflection to register CQS handlers and decorators.
services
.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyMarkerType)))
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(LoggingDecorator<,>))
.AddRequestHandlerDecorator(typeof(ValidationDecorator<,>)));Important: All arguments to
IncludeTypesFromandAddRequestHandlerDecoratormust betypeof()expressions.
Variables, method calls, or other expressions will produce a compile error (CQ0008).
<!-- Remove -->
<PackageReference Include="softaware.CQS.DependencyInjection" Version="..." />
<!-- Add -->
<PackageReference Include="softaware.CQS.DependencyInjection.SourceGenerated" Version="..." />The runtime package accepts an Assembly directly. The source generator can only work with typeof() expressions — a marker type from each assembly is enough.
// Before
services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyHandler).Assembly));
// After
services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyHandler)));If you were passing multiple assemblies, pass a marker type from each:
// Before
services.AddSoftawareCqs(b => b
.IncludeTypesFrom(typeof(SomeHandler).Assembly)
.IncludeTypesFrom(typeof(OtherHandler).Assembly));
// After
services.AddSoftawareCqs(b => b
.IncludeTypesFrom(typeof(SomeHandler))
.IncludeTypesFrom(typeof(OtherHandler)));The source generator cannot trace through extension methods. Replace all convenience methods with explicit AddRequestHandlerDecorator(typeof(...)) calls:
| Convenience method (remove) | Replace with |
|---|---|
.AddTransactionCommandHandlerDecorator() |
.AddRequestHandlerDecorator(typeof(TransactionAwareCommandHandlerDecorator<,>)) |
.AddTransactionQueryHandlerDecorator() |
.AddRequestHandlerDecorator(typeof(TransactionAwareQueryHandlerDecorator<,>)) |
.AddDataAnnotationsValidationDecorators() |
.AddRequestHandlerDecorator(typeof(ValidationRequestHandlerDecorator<,>)) |
.AddFluentValidationDecorators() |
.AddRequestHandlerDecorator(typeof(FluentValidationRequestHandlerDecorator<,>)) |
.AddUsageAwareDecorators() |
.AddRequestHandlerDecorator(typeof(UsageAwareRequestHandlerDecorator<,>)) |
.AddApplicationInsightsDependencyTelemetryDecorator() |
.AddRequestHandlerDecorator(typeof(DependencyTelemetryRequestHandlerDecorator<,>)) |
A build warning (CQ0004) is emitted for every detected convenience method.
// Before
.AddDecorators(b => b
.AddTransactionCommandHandlerDecorator()
.AddUsageAwareDecorators());
// After
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(TransactionAwareCommandHandlerDecorator<,>))
.AddRequestHandlerDecorator(typeof(UsageAwareRequestHandlerDecorator<,>)));Build the project. If the generator is working correctly you will see:
info CQ0007: softaware.Cqs source generator: Registered 12 handler(s) with 5 decorator(s). IRequestProcessor → GeneratedRequestProcessor
Request types with type parameters cannot be registered by the source generator, because it generates one explicit registration per concrete request type.
// ❌ NOT supported — causes CQ0009 (error)
public class GetNextLogicalId<TEntity> : IQuery<int> { }
public class GetNextLogicalIdHandler<TEntity> : IRequestHandler<GetNextLogicalId<TEntity>, int> { }Workaround: Keep this handler registered via the runtime package, or refactor to a non-generic design (e.g. pass a Type parameter or use a separate handler per entity type).
The generator cannot statically trace extension method calls.
// ❌ NOT supported — causes CQ0004 (warning, handler skipped)
.AddDecorators(b => b.AddTransactionCommandHandlerDecorator())Fix: Use explicit typeof() calls as shown in Step 3 above.
Any method call other than AddRequestHandlerDecorator inside the AddDecorators lambda is silently ignored by the source generator — even if that method internally calls AddRequestHandlerDecorator. The generator cannot trace through arbitrary method calls.
// ❌ NOT supported — causes CQ0012 (warning, decorator silently skipped)
.AddDecorators(b =>
{
b.AddRequestHandlerDecorator(typeof(LoggingDecorator<,>)); // ✅ recognized
MyHelper.RegisterAllDecorators(b); // ❌ ignored
})Fix: Register each decorator directly with AddRequestHandlerDecorator(typeof(...)):
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(LoggingDecorator<,>))
.AddRequestHandlerDecorator(typeof(ValidationDecorator<,>)))The generator reads types from the syntax tree — expressions that produce an Assembly or Type at runtime cannot be evaluated at compile time.
// ❌ NOT supported — causes CQ0008 (error)
var marker = typeof(MyHandler);
services.AddSoftawareCqs(b => b.IncludeTypesFrom(marker));
// ❌ NOT supported — causes CQ0008 (error)
services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyHandler).Assembly));
// ✅ OK
services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyHandler)));Decorators with only one type parameter (partially closed generics) are not supported. The runtime Scrutor package throws a NotSupportedException for these too, and the same restriction applies here.
// ❌ NOT supported by either package
class Decorator<TResult> : IRequestHandler<SomeRequest, TResult> { }
// ✅ Refactor to fully generic with type constraint
class Decorator<TRequest, TResult> : IRequestHandler<TRequest, TResult>
where TRequest : SomeRequest { }At compile time, the generator:
- Reads
AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(...)))from the syntax tree - Discovers all
IRequestHandler<TRequest, TResult>implementations in the referenced assemblies - Evaluates generic type constraints to determine which decorators apply to which handlers
- Generates explicit
IServiceCollectionregistrations with decorator chains - Generates a
GeneratedRequestProcessorfor static dispatch (registered asIRequestProcessor)
At runtime, the AddSoftawareCqs() call locates the generated CqsServiceRegistration class via reflection and invokes RegisterAll(IServiceCollection).
The AddDecorators() lambda is executed at runtime to capture conditional registrations — see Conditional Decorator Registration below.
| 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 typeof() expression. |
CQ0009 |
Error | Handler uses an open generic request type (e.g. MyRequest<TEntity>). 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. |
Unlike the runtime (Scrutor-based) package, the source generator reads all AddRequestHandlerDecorator calls from the syntax tree at compile time — including those inside if/switch blocks.
To handle this correctly, when the generator detects a decorator inside a conditional block, it generates an if (registry.IsEnabled(...)) guard in the factory lambda. At runtime, the AddDecorators lambda is actually executed, and the SoftawareCqsDecoratorBuilder records which decorators were called. This information is stored in a CqsDecoratorRegistry singleton.
services
.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(MyMarker)))
.AddDecorators(b =>
{
if (useLogging) // ✅ This works — evaluated at runtime
{
b.AddRequestHandlerDecorator(typeof(LoggingDecorator<,>));
}
b.AddRequestHandlerDecorator(typeof(ValidationDecorator<,>)); // Always applied
});The generated code for a handler would look like:
services.AddTransient<IRequestHandler<MyCommand, NoResult>>(sp =>
{
var __decoratorRegistry = sp.GetRequiredService<CqsDecoratorRegistry>();
IRequestHandler<MyCommand, NoResult> current = ActivatorUtilities.CreateInstance<MyCommandHandler>(sp);
if (__decoratorRegistry.IsEnabled(typeof(LoggingDecorator<,>)))
{
current = ActivatorUtilities.CreateInstance<LoggingDecorator<MyCommand, NoResult>>(sp, current);
}
current = ActivatorUtilities.CreateInstance<ValidationDecorator<MyCommand, NoResult>>(sp, current);
return current;
});Note: When no decorators are conditional (the common case), the registry is not used and there is zero runtime overhead.
Build the project and look for warning CQ0005 and CQ0006, or info CQ0007 in the build output:
info CQ0007: softaware.Cqs source generator: Registered 5 handler(s) with 3 decorator(s). IRequestProcessor → GeneratedRequestProcessor
If you see no CQ warnings at all, the generator is not running. Check:
- The NuGet package is properly installed (
dotnet list package) - Clear the NuGet cache:
dotnet nuget locals all --clear - Rebuild from scratch:
dotnet clean && dotnet build
Note:
CQ0007is an Info-level diagnostic that only appears in Visual Studio's Error List (enable the "Messages" filter) or when building withdotnet build -v detailed. The warning-level diagnostics (CQ0005,CQ0006) always appear in normal build output.
You should find the two files in Visual Studio Search (Ctrl + T shortcut):
CqsServiceRegistration.g.cs— handler and decorator registrationsGeneratedRequestProcessor.g.cs— static request dispatch
If not, add this to your .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>Then build and check the generated files at:
obj/Debug/{TFM}/generated/softaware.Cqs.DependencyInjection.SourceGenerator/softaware.Cqs.DependencyInjection.SourceGenerator.CqsSourceGenerator/
You should see:
CqsServiceRegistration.g.cs— handler and decorator registrationsGeneratedRequestProcessor.g.cs— static request dispatch
Add the MSBuild property CqsDebugSourceGenerator to your project for the duration of the debugging session:
<PropertyGroup>
<CqsDebugSourceGenerator>true</CqsDebugSourceGenerator>
</PropertyGroup>Then build the project. This triggers Debugger.Launch() and lets you step through the generator in Visual Studio.
Note: Remove this property once you are done debugging — it causes a debugger prompt on every build.
| Symptom | Cause | Fix |
|---|---|---|
| No warnings, no generated files | Generator not running | Clear NuGet cache, rebuild |
CQ0005 — "No AddSoftawareCqs call found" |
Missing or incorrect AddSoftawareCqs call |
Ensure you call services.AddSoftawareCqs(b => b.IncludeTypesFrom(typeof(...))) |
CQ0006 — "Core CQS types not resolved" |
Missing softaware.CQS package reference |
Add <PackageReference Include="softaware.CQS" /> |
CQ0008 — "must be a typeof() expression" |
Using a variable or .Assembly instead of typeof() |
Replace IncludeTypesFrom(myVariable) with IncludeTypesFrom(typeof(MyType)) |
InvalidOperationException at runtime |
Generated class not found | Ensure the NuGet package is installed and project was rebuilt |
CQ0009 — "open generic request type" |
Handler like MyHandler<TEntity> : IRequestHandler<MyRequest<TEntity>, int> |
Not supported. Use the runtime (Scrutor-based) package or refactor to closed generic types |
CQ0011 — "unsupported decorator generic shape" |
Decorator has extra generic parameters beyond TRequest/TResult |
Ensure all generic parameters are used by the IRequestHandler<TRequest, TResult> implementation |
CQ0012 — "unsupported method call in AddDecorators" |
A helper or extension method was called inside AddDecorators |
Register decorators directly via AddRequestHandlerDecorator(typeof(...)) |
Decorators are applied in registration order:
- First registered = closest to the handler (innermost)
- Last registered = outermost (executed first)
.AddDecorators(b => b
.AddRequestHandlerDecorator(typeof(InnerDecorator<,>)) // wraps handler directly
.AddRequestHandlerDecorator(typeof(OuterDecorator<,>))); // wraps InnerDecoratorExecution order: OuterDecorator → InnerDecorator → Handler