Files
agent-framework/wf-source-gen-plan.md
Ben Thomas 907654a489 [BREAKING] Obsoleting ReflectingExecutor in favor of source gen (#3380)
* 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>
2026-02-04 20:07:43 +00:00

294 lines
11 KiB
Markdown

# Roslyn Source Generator for Workflow Executor Routes
## Overview
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.
## Design Decisions (Confirmed)
- **Attribute syntax**: Inline properties on `[MessageHandler(Yield=[...], Send=[...])]`
- **Class-level attributes**: Generate `ConfigureSentTypes()`/`ConfigureYieldTypes()` from `[SendsMessage]`/`[YieldsMessage]`
- **Migration**: Clean break - requires direct `Executor` inheritance (not `ReflectingExecutor<T>`)
- **Handler accessibility**: Any (private, protected, internal, public)
---
## Implementation Steps
### Phase 1: Create Source Generator Project
**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.CSharp` 4.8.0+
- Set `IsRoslynComponent=true`, `EnforceExtendedAnalyzerRules=true`
- Package as analyzer in `analyzers/dotnet/cs`
### Phase 2: Define Attributes
**2.1 Create `MessageHandlerAttribute`:**
```
dotnet/src/Microsoft.Agents.AI.Workflows/Attributes/MessageHandlerAttribute.cs
```
```csharp
[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
```
```csharp
[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
```
```csharp
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class YieldsMessageAttribute : Attribute
{
public Type Type { get; }
public YieldsMessageAttribute(Type type) => this.Type = type;
}
```
### Phase 3: Implement Source Generator
**3.1 Detection criteria (syntax level):**
- Class has `partial` modifier
- 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 `ConfigureRoutes` with a body
- Handler method has valid signature: `(TMessage, IWorkflowContext[, CancellationToken])`
- Handler returns `void`, `ValueTask`, or `ValueTask<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:**
```csharp
// <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);` |
### Phase 4: Analyzer Diagnostics
| 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 |
### Phase 5: Integration & Migration
**5.1 Wire generator to main project:**
```xml
<!-- Microsoft.Agents.AI.Workflows.csproj -->
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows.Generators\..."
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```
**5.2 Mark `ReflectingExecutor<T>` obsolete:**
```csharp
[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:**
```csharp
[Obsolete("Use [MessageHandler] attribute instead.")]
public interface IMessageHandler<TMessage> { ... }
```
### Phase 6: Testing
**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<T>)
- Nested classes
- Generic executors
- Inheritance chains (Executor -> CustomBase -> Concrete)
- Class-level `[SendsMessage]`/`[YieldsMessage]` attributes
- Manual `ConfigureRoutes` present (should skip generation)
- Invalid signatures (should produce diagnostics)
**6.2 Integration tests:**
- Port existing `ReflectingExecutor` test cases to use `[MessageHandler]`
- Verify generated routes match reflection-discovered routes
---
## Files to Create
| 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 |
## Files to Modify
| 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 |
---
## Example Usage (End State)
```csharp
[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:
```csharp
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;
}
}
```