* Initial working version with tests.
* Updates to validate class data once instead of for each handler method. Also updated Diagnostics Ids to format of MAFGENWF{NUM}
* Formatting and trying to fix generation project pack.
* Another atempt at getting the genrators project to build.
* More attempts to fix generator build and pack.
* Fixing file encodings.
* Initail round of cleanup.
* Trying to fix packing.
* Still trying to fix pipeline pack.
* Remove obsolescence markers, sample updates, and docs from generator branch.
This commit separates the generator core functionality from the
deprecation of ReflectingExecutor. The removed changes will be
re-added in a dependent branch (wf-obsolete-reflector).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Mark ReflectingExecutor and IMessageHandler as obsolete.
This commit deprecates the reflection-based handler discovery approach
in favor of the new [MessageHandler] attribute with source generation.
Changes:
- Add [Obsolete] to ReflectingExecutor<T>, IMessageHandler<T>, IMessageHandler<T,R>
- Add #pragma to suppress warnings in internal reflection code
- Update Concurrent sample to use new [MessageHandler] pattern
- Add Directory.Build.props for samples to include generator
- Add documentation files explaining the migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Obsoleteing Reflector-based workflow code generation in favor of Source Generators and updating some samples to use new pattern.
This commit deprecates the reflection-based handler discovery approach
in favor of the new [MessageHandler] attribute with source generation.
Changes:
- Add [Obsolete] to ReflectingExecutor<T>, IMessageHandler<T>, IMessageHandler<T,R>
- Add #pragma to suppress warnings in internal reflection code
- Update Concurrent sample to use new [MessageHandler] pattern
- Add Directory.Build.props for samples to include generator
- Add documentation files explaining the migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Cleaning up temporary design and progress files.
---------
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
8.9 KiB
Source Generator for Workflow Executors: Rationale and Impact
Overview
The Microsoft Agents AI Workflows framework has introduced a Roslyn source generator (Microsoft.Agents.AI.Workflows.Generators) that replaces the previous reflection-based approach for discovering and registering message handlers. This document explains why this change was made, what benefits it provides, and how it impacts framework users.
Why Move from Reflection to Code Generation?
The Previous Approach: ReflectingExecutor<T>
Previously, executors that needed automatic handler discovery inherited from ReflectingExecutor<T> and implemented marker interfaces like IMessageHandler<TMessage>:
// Old approach - reflection-based
public class MyExecutor : ReflectingExecutor<MyExecutor>,
IMessageHandler<QueryMessage>,
IMessageHandler<CommandMessage, CommandResult>
{
public ValueTask HandleAsync(QueryMessage msg, IWorkflowContext ctx, CancellationToken ct)
{
// Handle query
}
public ValueTask<CommandResult> HandleAsync(CommandMessage msg, IWorkflowContext ctx, CancellationToken ct)
{
// Handle command and return result
}
}
This approach had several limitations:
- Runtime overhead: Handler discovery happened at runtime via reflection, adding latency to executor initialization
- No AOT compatibility: Reflection-based discovery doesn't work with Native AOT compilation
- Redundant declarations: The interface list duplicated information already present in method signatures
- Limited metadata: No clean way to declare yield/send types for protocol validation
- Hidden errors: Invalid handler signatures weren't caught until runtime
The New Approach: [MessageHandler] Attribute
The source generator enables a cleaner, attribute-based pattern:
// New approach - source generated
[SendsMessage(typeof(PollToken))]
public partial class MyExecutor : Executor
{
[MessageHandler]
private ValueTask HandleQueryAsync(QueryMessage msg, IWorkflowContext ctx, CancellationToken ct)
{
// Handle query
}
[MessageHandler(Yield = [typeof(StreamChunk)], Send = [typeof(InternalMessage)])]
private ValueTask<CommandResult> HandleCommandAsync(CommandMessage msg, IWorkflowContext ctx, CancellationToken ct)
{
// Handle command and return result
}
}
The generator produces a partial class with ConfigureRoutes(), ConfigureSentTypes(), and ConfigureYieldTypes() implementations at compile time.
What's Better About Code Generation?
1. Compile-Time Validation
Invalid handler signatures are caught during compilation, not at runtime:
[MessageHandler]
private void InvalidHandler(string msg) // Error WFGEN005: Missing IWorkflowContext parameter
{
}
Diagnostic errors include:
WFGEN001: Handler missingIWorkflowContextparameterWFGEN002: Invalid return type (must bevoid,ValueTask, orValueTask<T>)WFGEN003: Executor class must bepartialWFGEN004:[MessageHandler]on non-Executor classWFGEN005: Insufficient parametersWFGEN006:ConfigureRoutesalready manually defined
2. Zero Runtime Reflection
All handler registration happens at compile time. The generated code is simple, direct method calls:
// Generated code
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder
.AddHandler<QueryMessage>(this.HandleQueryAsync)
.AddHandler<CommandMessage, CommandResult>(this.HandleCommandAsync);
}
This eliminates:
- Reflection overhead during initialization
- Assembly scanning
- Dynamic delegate creation
3. Native AOT Compatibility
Because there's no runtime reflection, executors work seamlessly with .NET Native AOT compilation. This enables:
- Faster startup times
- Smaller deployment sizes
- Deployment to environments that don't support JIT compilation
4. Explicit Protocol Metadata
The Yield and Send properties on [MessageHandler] plus class-level [SendsMessage] and [YieldsMessage] attributes provide explicit protocol documentation:
[SendsMessage(typeof(PollToken))] // This executor sends PollToken messages
[YieldsMessage(typeof(FinalResult))] // This executor yields FinalResult to workflow output
public partial class MyExecutor : Executor
{
[MessageHandler(
Yield = [typeof(StreamChunk)], // This handler yields StreamChunk
Send = [typeof(InternalQuery)])] // This handler sends InternalQuery
private ValueTask HandleAsync(Request req, IWorkflowContext ctx) { ... }
}
This metadata enables:
- Static protocol validation
- Better IDE tooling and documentation
- Clearer code intent
5. Handler Accessibility Freedom
Handlers can be private, protected, internal, or public. The old interface-based approach required public methods. Now you can encapsulate handler implementations:
public partial class MyExecutor : Executor
{
[MessageHandler]
private ValueTask HandleInternalAsync(InternalMessage msg, IWorkflowContext ctx)
{
// Private handler - implementation detail
}
}
6. Cleaner Inheritance
The generator properly handles inheritance chains, calling base.ConfigureRoutes() when appropriate:
public partial class DerivedExecutor : BaseExecutor
{
[MessageHandler]
private ValueTask HandleDerivedAsync(DerivedMessage msg, IWorkflowContext ctx) { ... }
}
// Generated:
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
routeBuilder = base.ConfigureRoutes(routeBuilder); // Preserves base handlers
return routeBuilder
.AddHandler<DerivedMessage>(this.HandleDerivedAsync);
}
New Capabilities Enabled
1. Static Workflow Analysis
With explicit yield/send metadata, tools can analyze workflow graphs at compile time:
- Validate that all message types have handlers
- Detect unreachable executors
- Generate workflow documentation
2. Trimming-Safe Deployments
The generated code contains no reflection, making it fully compatible with IL trimming. This reduces deployment size significantly for serverless and edge scenarios.
3. Better IDE Experience
Because the generator runs in the IDE, you get:
- Immediate feedback on handler signature errors
- IntelliSense for generated methods
- Go-to-definition on generated code
4. Protocol Documentation Generation
The explicit type metadata can be used to generate:
- API documentation
- OpenAPI/Swagger specs for workflow endpoints
- Visual workflow diagrams
Impact on Framework Users
Migration Path
Existing code using ReflectingExecutor<T> continues to work but is marked [Obsolete]. To migrate:
- Change base class from
ReflectingExecutor<T>toExecutor - Add
partialmodifier to the class - Replace
IMessageHandler<T>interfaces with[MessageHandler]attributes - Optionally add
Yield/Sendmetadata for protocol validation
Before:
public class MyExecutor : ReflectingExecutor<MyExecutor>, IMessageHandler<Query, Result>
{
public ValueTask<Result> HandleAsync(Query q, IWorkflowContext ctx, CancellationToken ct) { ... }
}
After:
public partial class MyExecutor : Executor
{
[MessageHandler]
private ValueTask<Result> HandleQueryAsync(Query q, IWorkflowContext ctx, CancellationToken ct) { ... }
}
Breaking Changes
- Classes using
[MessageHandler]must bepartial - Handler methods must have at least 2 parameters:
(TMessage, IWorkflowContext) - Return type must be
void,ValueTask, orValueTask<T>
Performance Improvements
Users can expect:
- Faster executor initialization: No reflection overhead
- Reduced memory allocation: No dynamic delegate creation
- AOT deployment support: Full Native AOT compatibility
- Smaller trimmed deployments: No reflection metadata preserved
NuGet Package
The generator is distributed as a separate NuGet package (Microsoft.Agents.AI.Workflows.Generators) that's automatically referenced by the main Workflows package. It's packaged as an analyzer, so it:
- Runs automatically during build
- Requires no additional configuration
- Works in all IDEs that support Roslyn analyzers
Summary
The move from reflection to source generation represents a significant improvement in the Workflows framework:
| Aspect | Reflection (Old) | Source Generator (New) |
|---|---|---|
| Handler discovery | Runtime | Compile-time |
| Error detection | Runtime exceptions | Compiler errors |
| AOT support | No | Yes |
| Trimming support | Limited | Full |
| Protocol metadata | Implicit | Explicit |
| Handler visibility | Public only | Any |
| Initialization speed | Slower | Faster |
The source generator approach aligns with modern .NET best practices and positions the framework for future scenarios including edge computing, serverless, and mobile deployments where AOT compilation and minimal footprint are essential.