Source-Generated Dependency Injection¶
AlexaVoxCraft automatically generates dependency injection registration code at compile time using C# interceptors, eliminating runtime reflection and improving startup performance.
🎯 Trivia Skill Examples: All code examples demonstrate building a trivia game skill with automatic handler registration.
Features¶
- ⚡ Compile-Time Registration: Zero runtime reflection or assembly scanning
- 🎯 Type-Safe: Compile-time validation of handler implementations
- 🚀 Faster Startup: No reflection overhead during Lambda cold starts
- 🔧 Customizable: Control registration with attributes
- 📦 Automatic: Works out of the box with no configuration
Requirements¶
⚠️ SDK Version Required: To use source-generated dependency injection with interceptors, you must use at least version 8.0.400 of the .NET SDK. This ships with Visual Studio 2022 version 17.11 or higher.
Check your SDK version:
How It Works¶
The source generator uses C# 12 interceptors to replace calls to AddSkillMediator() with compile-time generated registration code:
// Your code - no assembly scanning needed!
services.AddSkillMediator(context.Configuration);
// Generated at compile time - intercepts the call above
internal static IServiceCollection AddSkillMediator(
this IServiceCollection services,
IConfiguration configuration,
Action<SkillServiceConfiguration>? settingsAction = null)
{
// Auto-generated registrations
services.AddTransient<IRequestHandler<LaunchRequest>, LaunchRequestHandler>();
services.AddTransient<IRequestHandler<IntentRequest>, AnswerHandler>();
services.AddTransient<IRequestHandler<UserEventRequest>, AnswerHandler>();
services.TryAddTransient<IDefaultRequestHandler, DefaultHandler>();
services.AddTransient<IExceptionHandler, ErrorHandler>();
services.AddTransient<IRequestInterceptor, LocalizationRequestInterceptor>();
return services;
}
Benefits¶
- No Runtime Reflection: All handler discovery happens at compile time
- Faster Cold Starts: Eliminates assembly scanning during Lambda initialization
- Compile-Time Validation: Errors caught during build, not at runtime
- IDE Support: Full IntelliSense and navigation for generated code
Default Behavior¶
Source generation is enabled by default. Simply install the AlexaVoxCraft.MediatR package and call AddSkillMediator():
// In your AlexaSkillFunction
protected override void Init(IHostBuilder builder)
{
builder
.UseHandler<LambdaHandler, APLSkillRequest, SkillResponse>()
.ConfigureServices((context, services) =>
{
// This call is automatically intercepted at compile time
// No assembly scanning - all registration is generated!
services.AddSkillMediator(context.Configuration);
});
}
The generator discovers and registers: - IRequestHandler<T> implementations - IDefaultRequestHandler implementations - IPipelineBehavior implementations - IExceptionHandler implementations - IRequestInterceptor implementations - IResponseInterceptor implementations - IPersistenceAdapter implementations
Customizing Registration¶
The AlexaHandler Attribute¶
Control how handlers are registered using the [AlexaHandler] attribute:
using AlexaVoxCraft.MediatR.Annotations;
[AlexaHandler(Lifetime = ServiceLifetime.Scoped, Order = 10, Exclude = false)]
public class MyHandler : IRequestHandler<LaunchRequest>
{
public Task<bool> CanHandle(IHandlerInput input, CancellationToken cancellationToken)
{
return Task.FromResult(input.RequestEnvelope.Request is LaunchRequest);
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
return await input.ResponseBuilder
.Speak("Welcome!")
.GetResponse(cancellationToken);
}
}
Lifetime Property¶
Controls the service lifetime for dependency injection:
Default Lifetime: When the
Lifetimeproperty is not specified, handlers are registered asTransient(a new instance is created for each request). This is the recommended default for stateless request handlers.
// Transient (default when Lifetime not specified) - new instance per request
[AlexaHandler(Lifetime = ServiceLifetime.Transient)]
public class TransientHandler : IRequestHandler<LaunchRequest> { }
// Scoped - one instance per Lambda invocation
[AlexaHandler(Lifetime = ServiceLifetime.Scoped)]
public class ScopedHandler : IRequestHandler<IntentRequest> { }
// Singleton - one instance for the lifetime of the Lambda container
[AlexaHandler(Lifetime = ServiceLifetime.Singleton)]
public class SingletonHandler : IPipelineBehavior { }
Best Practices: - Use Transient for stateless handlers (default) - Use Scoped for handlers that share state within a single request - Use Singleton for expensive-to-create services or caches - Warning: Be careful with Singleton - ensure thread-safety
Order Property¶
Controls execution order for handlers and pipeline behaviors:
// Lower numbers execute first
[AlexaHandler(Order = 1)]
public class AuthenticationInterceptor : IRequestInterceptor
{
// Runs first - validates authentication
}
[AlexaHandler(Order = 5)]
public class LoggingInterceptor : IRequestInterceptor
{
// Runs second - logs authenticated requests
}
[AlexaHandler(Order = 10)]
public class LocalizationInterceptor : IRequestInterceptor
{
// Runs third - sets up localization
}
Ordering Pipeline Behaviors:
// Behavior 1: Validation (Order = 0 - default)
[AlexaHandler]
public class ValidationBehavior : IPipelineBehavior
{
public async Task<SkillResponse> Handle(
SkillRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<SkillResponse> next)
{
// Validate request
if (!IsValid(request))
throw new ValidationException();
return await next();
}
}
// Behavior 2: Logging (Order = 5)
[AlexaHandler(Order = 5)]
public class LoggingBehavior : IPipelineBehavior
{
public async Task<SkillResponse> Handle(
SkillRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<SkillResponse> next)
{
_logger.LogInformation("Processing request");
var response = await next();
_logger.LogInformation("Request processed");
return response;
}
}
// Behavior 3: Performance Monitoring (Order = 10)
[AlexaHandler(Order = 10)]
public class PerformanceBehavior : IPipelineBehavior
{
public async Task<SkillResponse> Handle(
SkillRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<SkillResponse> next)
{
var sw = Stopwatch.StartNew();
var response = await next();
_logger.LogInformation("Processed in {elapsed}ms", sw.ElapsedMilliseconds);
return response;
}
}
Execution Order: Validation → Logging → Performance Monitoring → Handler → Performance Monitoring → Logging → Validation
Exclude Property¶
Skip automatic registration for specific handlers:
// This handler will NOT be automatically registered
[AlexaHandler(Exclude = true)]
public class ManuallyRegisteredHandler : IRequestHandler<LaunchRequest>
{
// Implementation
}
// Register it manually if needed
services.AddScoped<IRequestHandler<LaunchRequest>, ManuallyRegisteredHandler>();
Use Cases for Exclude: - Testing - register mock handlers manually - Conditional registration - register based on environment - Custom registration logic - need special configuration
Multiple Interface Implementations¶
Handlers can implement multiple IRequestHandler<T> interfaces and all will be registered:
// Handles both IntentRequest and UserEventRequest
public class AnswerHandler :
IRequestHandler<IntentRequest>,
IRequestHandler<UserEventRequest>
{
public Task<bool> CanHandle(IHandlerInput input, CancellationToken cancellationToken)
{
return Task.FromResult(
input.RequestEnvelope.Request is UserEventRequest ||
input.RequestEnvelope.Request is IntentRequest intent &&
(intent.Intent.Name == "AnswerIntent" || intent.Intent.Name == "DontKnowIntent"));
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
// Handle both request types
var answer = GetAnswer(input.RequestEnvelope.Request);
// Process answer...
}
}
// Generated registration
services.AddTransient<IRequestHandler<IntentRequest>, AnswerHandler>();
services.AddTransient<IRequestHandler<UserEventRequest>, AnswerHandler>();
Similarly, a class can implement both IRequestInterceptor and IResponseInterceptor:
public class FullCycleInterceptor :
IRequestInterceptor,
IResponseInterceptor
{
public Task Process(IHandlerInput input, CancellationToken cancellationToken)
{
// Pre-process request
return Task.CompletedTask;
}
public Task Process(IHandlerInput input, SkillResponse? output, CancellationToken cancellationToken)
{
// Post-process response
return Task.CompletedTask;
}
}
// Both interfaces registered
services.AddTransient<IRequestInterceptor, FullCycleInterceptor>();
services.AddTransient<IResponseInterceptor, FullCycleInterceptor>();
Disabling Source Generation¶
If you need to disable the source generator and use runtime registration instead:
<!-- In your .csproj file -->
<PropertyGroup>
<EnableMediatRGeneratorInterceptor>false</EnableMediatRGeneratorInterceptor>
</PropertyGroup>
When disabled, AddSkillMediator() falls back to runtime reflection-based registration and you must specify the assembly to scan:
// With source generation DISABLED - requires assembly scanning configuration
services.AddSkillMediator(context.Configuration, cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>()); // ⚠️ Only needed when source generation is disabled
When to Disable: - Debugging generator issues - Using .NET SDK version < 8.0.400 - Need dynamic handler registration at runtime - Testing custom registration scenarios
Default Handler Registration¶
The generator uses TryAddTransient for IDefaultRequestHandler to allow manual override:
// Auto-registered as default handler
public class FallbackHandler : IDefaultRequestHandler
{
public Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
return input.ResponseBuilder
.Speak("Sorry, I didn't understand that.")
.GetResponse(cancellationToken);
}
}
// You can override it manually if needed
services.AddTransient<IDefaultRequestHandler, CustomDefaultHandler>();
Referenced Assembly Discovery¶
When source generation is enabled, you can explicitly include handlers from referenced assemblies by using settingsAction in AddSkillMediator(...).
The generator supports these forms:
// Positional settingsAction + generic form
services.AddSkillMediator(context.Configuration, cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<ExternalProject.Marker>();
});
// Named settingsAction + generic form
services.AddSkillMediator(context.Configuration, settingsAction: cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<ExternalProject.Marker>();
});
// Positional settingsAction + typeof form
services.AddSkillMediator(context.Configuration, cfg =>
{
cfg.RegisterServicesFromAssemblyContaining(typeof(ExternalProject.Marker));
});
Use this when handlers live in a separate class library and you want them included in compile-time generated registrations.
Diagnostics¶
The source generator provides compile-time diagnostics:
AVXC001: No Handlers Found¶
Cause: No classes implementingIRequestHandler<T> or IDefaultRequestHandler were found. Solution: Ensure your handlers implement the correct interfaces and are not marked with [AlexaHandler(Exclude = true)]. AVXC002: Multiple Default Handlers¶
Cause: More than one class implementsIDefaultRequestHandler. Solution: Only one default handler is allowed. Remove or exclude extra implementations. AVXC003: Multiple Persistence Adapters¶
Cause: More than one class implementsIPersistenceAdapter. Solution: Only one persistence adapter is allowed per skill. Remove or exclude extra implementations. Viewing Generated Code¶
The generated interceptor code is written to your project's obj/ folder:
obj/Debug/net10.0/generated/
AlexaVoxCraft.MediatR.Generators/
AlexaVoxCraft.MediatR.Generators.Generators.AlexaVoxCraftDiGenerator/
__AlexaVoxCraft_Interceptors.g.cs
You can inspect this file to see exactly what registrations are being generated.
Performance Impact¶
Startup Time Comparison (Lambda cold start):
| Method | Cold Start Time | Description |
|---|---|---|
| Runtime Reflection | ~250ms | Scans assemblies, creates type instances |
| Source Generation | ~50ms | Direct method calls, no reflection |
Benefits: - 80% faster cold start initialization - No runtime overhead for handler discovery - Smaller memory footprint - no reflection metadata caching - Predictable performance - same every time
Best Practices¶
1. Use Source Generation (Default)¶
// ✅ Good - source generation enabled (default)
services.AddSkillMediator(context.Configuration);
// ✅ Good - explicit referenced assembly inclusion with source generation enabled
services.AddSkillMediator(context.Configuration, cfg =>
cfg.RegisterServicesFromAssemblyContaining<ExternalProject.Marker>());
// ❌ Avoid - fallback runtime assembly scanning (only if source generation disabled)
services.AddSkillMediator(context.Configuration, cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>());
2. Use Attributes for Control¶
// Good - explicit control
[AlexaHandler(Lifetime = ServiceLifetime.Scoped, Order = 5)]
public class GameStateHandler : IRequestHandler<IntentRequest> { }
// Also good - uses sensible defaults
public class SimpleHandler : IRequestHandler<LaunchRequest> { }
3. Order Pipeline Components¶
[AlexaHandler(Order = 1)] // Authentication first
public class AuthInterceptor : IRequestInterceptor { }
[AlexaHandler(Order = 5)] // Then logging
public class LogInterceptor : IRequestInterceptor { }
[AlexaHandler(Order = 10)] // Finally localization
public class LocalizationInterceptor : IRequestInterceptor { }
4. Keep Handlers Simple¶
// Good - focused responsibility
public class LaunchHandler : IRequestHandler<LaunchRequest>
{
private readonly IGameService _gameService;
public LaunchHandler(IGameService gameService)
{
_gameService = gameService;
}
}
// Avoid - too many dependencies, consider splitting
public class OverloadedHandler : IRequestHandler<IntentRequest>
{
// 10+ constructor parameters - red flag
}
5. Document Handler Order¶
/// <summary>
/// Handles user answers to trivia questions.
/// Executes after authentication (Order = 1) and state loading (Order = 5).
/// </summary>
[AlexaHandler(Order = 10)]
public class AnswerHandler : IRequestHandler<IntentRequest> { }
Troubleshooting¶
Interceptor Not Working¶
Symptoms: Runtime registration still being used, no compile-time interception.
Solutions: 1. Verify .NET SDK version: dotnet --version (must be 8.0.400+) 2. Check <EnableMediatRGeneratorInterceptor> is not set to false 3. Ensure you're calling the extension method, not a custom method 4. Clean and rebuild: dotnet clean && dotnet build
Handlers Not Being Registered¶
Symptoms: Handler exists but not getting called.
Solutions: 1. Check handler implements correct interface (IRequestHandler<T>) 2. Verify handler is not marked [AlexaHandler(Exclude = true)] 3. If handler is in a referenced assembly, include it via settingsAction and RegisterServicesFromAssemblyContaining(...) 4. Confirm settingsAction registration can be resolved (positional or named lambda are both supported) 5. Check CanHandle() logic returns true for expected requests 6. Review generated code in obj/ folder to confirm registration and ordering
Mixed Assembly Ordering Questions¶
[AlexaHandler(Order = ...)] is applied across both source and referenced assemblies. Registration order follows Order values, not alphabetical class names.
Build Errors with Interceptors¶
Symptoms: CS errors about interceptors, method signatures.
Solutions: 1. Update to .NET SDK 8.0.400+ (interceptors require C# 12) 2. Ensure <LangVersion>12</LangVersion> or higher in csproj 3. Check Visual Studio is version 17.11 or higher 4. Try deleting bin/ and obj/ folders, then rebuild
Multiple Default Handler Error¶
Symptoms: AVXC002 diagnostic at compile time.
Solution:
// Option 1: Exclude one
[AlexaHandler(Exclude = true)]
public class OldDefaultHandler : IDefaultRequestHandler { }
// Option 2: Remove IDefaultRequestHandler from one
public class RegularHandler : IRequestHandler<IntentRequest> { }
Migration from Runtime Registration¶
If upgrading from a version that used runtime registration:
Before (v4.x with runtime registration):
services.AddSkillMediator(context.Configuration, cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
cfg.RegisterInterceptors(typeof(MyInterceptor).Assembly);
});
After (v5.x with source generation - recommended):
// Simplified - everything auto-registered at compile time!
services.AddSkillMediator(context.Configuration);
No code changes needed - existing handlers work with source generation automatically. Just remove the assembly scanning configuration and enjoy faster startup times!
Examples¶
See the complete trivia skill implementation for real-world usage of source-generated handlers with: - Multiple request types - Pipeline behaviors with ordering - Exception handlers - Request/response interceptors - Custom lifetimes and configurations