Replace the reflection-based ReflectingExecutor<T> pattern with a compile-time source generator that discovers [MessageHandler] attributed methods and generates ConfigureRoutes, ConfigureSentTypes, and ConfigureYieldTypes implementations.
- Attribute syntax: Inline properties on
[MessageHandler(Yield=[...], Send=[...])] - Class-level attributes: Generate
ConfigureSentTypes()/ConfigureYieldTypes()from[SendsMessage]/[YieldsMessage] - Migration: Clean break - requires direct
Executorinheritance (notReflectingExecutor<T>) - Handler accessibility: Any (private, protected, internal, public)
1.1 Create project structure:
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/
├── Microsoft.Agents.AI.Workflows.Generators.csproj
├── ExecutorRouteGenerator.cs # Main incremental generator
├── Models/
│ ├── ExecutorInfo.cs # Data model for executor analysis
│ └── HandlerInfo.cs # Data model for handler methods
├── Analysis/
│ ├── SyntaxDetector.cs # Syntax-based candidate detection
│ └── SemanticAnalyzer.cs # Semantic model analysis
├── Generation/
│ └── SourceBuilder.cs # Code generation logic
└── Diagnostics/
└── DiagnosticDescriptors.cs # Analyzer diagnostics
1.2 Project file configuration:
- Target
netstandard2.0 - Reference
Microsoft.CodeAnalysis.CSharp4.8.0+ - Set
IsRoslynComponent=true,EnforceExtendedAnalyzerRules=true - Package as analyzer in
analyzers/dotnet/cs
2.1 Create MessageHandlerAttribute:
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/MessageHandlerAttribute.cs
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class MessageHandlerAttribute : Attribute
{
public Type[]? Yield { get; set; } // Types yielded as workflow outputs
public Type[]? Send { get; set; } // Types sent to other executors
}2.2 Create SendsMessageAttribute:
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/SendsMessageAttribute.cs
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class SendsMessageAttribute : Attribute
{
public Type Type { get; }
public SendsMessageAttribute(Type type) => this.Type = type;
}2.3 Create YieldsMessageAttribute:
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/YieldsMessageAttribute.cs
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class YieldsMessageAttribute : Attribute
{
public Type Type { get; }
public YieldsMessageAttribute(Type type) => this.Type = type;
}3.1 Detection criteria (syntax level):
- Class has
partialmodifier - Class has at least one method with
[MessageHandler]attribute
3.2 Validation criteria (semantic level):
- Class derives from
Executor(directly or transitively) - Class does NOT already define
ConfigureRouteswith a body - Handler method has valid signature:
(TMessage, IWorkflowContext[, CancellationToken]) - Handler returns
void,ValueTask, orValueTask<T>
3.3 Handler signature mapping:
| Method Signature | Generated AddHandler Call |
|---|---|
void Handler(T, IWorkflowContext) |
AddHandler<T>(this.Handler) |
void Handler(T, IWorkflowContext, CT) |
AddHandler<T>(this.Handler) |
ValueTask Handler(T, IWorkflowContext) |
AddHandler<T>(this.Handler) |
ValueTask Handler(T, IWorkflowContext, CT) |
AddHandler<T>(this.Handler) |
TResult Handler(T, IWorkflowContext) |
AddHandler<T, TResult>(this.Handler) |
ValueTask<TResult> Handler(T, IWorkflowContext, CT) |
AddHandler<T, TResult>(this.Handler) |
3.4 Generated code structure:
// <auto-generated/>
#nullable enable
namespace MyNamespace;
partial class MyExecutor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
// Call base if inheriting from another executor with routes
// routeBuilder = base.ConfigureRoutes(routeBuilder);
return routeBuilder
.AddHandler<InputType1, OutputType1>(this.Handler1)
.AddHandler<InputType2>(this.Handler2);
}
protected override ISet<Type> ConfigureSentTypes()
{
var types = base.ConfigureSentTypes();
types.Add(typeof(SentType1));
return types;
}
protected override ISet<Type> ConfigureYieldTypes()
{
var types = base.ConfigureYieldTypes();
types.Add(typeof(YieldType1));
return types;
}
}3.5 Inheritance handling:
| Scenario | Generated ConfigureRoutes |
|---|---|
Directly extends Executor |
No base call (abstract) |
Extends executor with [MessageHandler] methods |
routeBuilder = base.ConfigureRoutes(routeBuilder); |
Extends executor with manual ConfigureRoutes |
routeBuilder = base.ConfigureRoutes(routeBuilder); |
| ID | Severity | Condition |
|---|---|---|
WFGEN001 |
Error | Handler missing IWorkflowContext parameter |
WFGEN002 |
Error | Handler has invalid return type |
WFGEN003 |
Error | Executor with [MessageHandler] must be partial |
WFGEN004 |
Warning | [MessageHandler] on non-Executor class |
WFGEN005 |
Error | Handler has fewer than 2 parameters |
WFGEN006 |
Info | ConfigureRoutes already defined, handlers ignored |
5.1 Wire generator to main project:
<!-- Microsoft.Agents.AI.Workflows.csproj -->
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows.Generators\..."
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>5.2 Mark ReflectingExecutor<T> obsolete:
[Obsolete("Use [MessageHandler] attribute on methods in a partial class deriving from Executor. " +
"See migration guide. This type will be removed in v1.0.", error: false)]
public class ReflectingExecutor<TExecutor> : Executor ...5.3 Mark IMessageHandler<T> interfaces obsolete:
[Obsolete("Use [MessageHandler] attribute instead.")]
public interface IMessageHandler<TMessage> { ... }6.1 Generator unit tests:
dotnet/tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/
├── ExecutorRouteGeneratorTests.cs
├── SyntaxDetectorTests.cs
├── SemanticAnalyzerTests.cs
└── TestHelpers/
└── GeneratorTestHelper.cs
Test cases:
- Simple single handler
- Multiple handlers on one class
- Handlers with different signatures (void, ValueTask, ValueTask)
- Nested classes
- Generic executors
- Inheritance chains (Executor -> CustomBase -> Concrete)
- Class-level
[SendsMessage]/[YieldsMessage]attributes - Manual
ConfigureRoutespresent (should skip generation) - Invalid signatures (should produce diagnostics)
6.2 Integration tests:
- Port existing
ReflectingExecutortest cases to use[MessageHandler] - Verify generated routes match reflection-discovered routes
| Path | Purpose |
|---|---|
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj |
Generator project |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/ExecutorRouteGenerator.cs |
Main generator |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ExecutorInfo.cs |
Data model |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/HandlerInfo.cs |
Data model |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SyntaxDetector.cs |
Syntax analysis |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs |
Semantic analysis |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Generation/SourceBuilder.cs |
Code gen |
dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Diagnostics/DiagnosticDescriptors.cs |
Diagnostics |
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/MessageHandlerAttribute.cs |
Handler attribute |
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/SendsMessageAttribute.cs |
Class-level send |
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/YieldsMessageAttribute.cs |
Class-level yield |
dotnet/tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/*.cs |
Generator tests |
| Path | Changes |
|---|---|
dotnet/src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj |
Add generator reference |
dotnet/src/Microsoft.Agents.AI.Workflows/Reflection/ReflectingExecutor.cs |
Add [Obsolete] |
dotnet/src/Microsoft.Agents.AI.Workflows/Reflection/IMessageHandler.cs |
Add [Obsolete] |
dotnet/Microsoft.Agents.sln |
Add new projects |
[SendsMessage(typeof(PollToken))]
public partial class MyChatExecutor : ChatProtocolExecutor
{
[MessageHandler]
private async ValueTask<ChatResponse> HandleQueryAsync(
ChatQuery query, IWorkflowContext ctx, CancellationToken ct)
{
// Return type automatically inferred as output
return new ChatResponse(...);
}
[MessageHandler(Yield = [typeof(StreamChunk)], Send = [typeof(InternalMessage)])]
private void HandleStream(StreamRequest req, IWorkflowContext ctx)
{
// Explicit Yield/Send for complex handlers
}
}Generated:
partial class MyChatExecutor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
routeBuilder = base.ConfigureRoutes(routeBuilder);
return routeBuilder
.AddHandler<ChatQuery, ChatResponse>(this.HandleQueryAsync)
.AddHandler<StreamRequest>(this.HandleStream);
}
protected override ISet<Type> ConfigureSentTypes()
{
var types = base.ConfigureSentTypes();
types.Add(typeof(PollToken));
types.Add(typeof(InternalMessage)); // From handler attribute
return types;
}
protected override ISet<Type> ConfigureYieldTypes()
{
var types = base.ConfigureYieldTypes();
types.Add(typeof(ChatResponse)); // From return type
types.Add(typeof(StreamChunk)); // From handler attribute
return types;
}
}