Skip to content

Latest commit

 

History

History
293 lines (245 loc) · 10.6 KB

File metadata and controls

293 lines (245 loc) · 10.6 KB

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
[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;
}

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:

// <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:

<!-- 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> { ... }

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)
  • 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)

[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;
    }
}