Files
agent-framework/dotnet/wf-source-gen-changes.md
T
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

11 KiB

Workflow Executor Route Source Generator - Implementation Summary

This document summarizes all changes made to implement a Roslyn source generator that replaces the reflection-based ReflectingExecutor<T> pattern with compile-time code generation using [MessageHandler] attributes.

Overview

The source generator automatically discovers methods marked with [MessageHandler] and generates ConfigureRoutes, ConfigureSentTypes, and ConfigureYieldTypes method implementations at compile time. This improves AOT compatibility and eliminates the need for the CRTP (Curiously Recurring Template Pattern) used by ReflectingExecutor<T>.

New Files Created

Attributes (3 files)

File Purpose
src/Microsoft.Agents.AI.Workflows/Attributes/MessageHandlerAttribute.cs Marks methods as message handlers with optional Yield and Send type arrays
src/Microsoft.Agents.AI.Workflows/Attributes/SendsMessageAttribute.cs Class-level attribute declaring message types an executor may send
src/Microsoft.Agents.AI.Workflows/Attributes/YieldsMessageAttribute.cs Class-level attribute declaring output types an executor may yield

Source Generator Project (8 files)

File Purpose
src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj Project file targeting netstandard2.0 with Roslyn component settings
src/Microsoft.Agents.AI.Workflows.Generators/ExecutorRouteGenerator.cs Main incremental generator implementing IIncrementalGenerator
src/Microsoft.Agents.AI.Workflows.Generators/Models/HandlerInfo.cs Data model for handler method information
src/Microsoft.Agents.AI.Workflows.Generators/Models/ExecutorInfo.cs Data model for executor class information
src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SyntaxDetector.cs Fast syntax-level candidate detection
src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs Semantic validation and type extraction
src/Microsoft.Agents.AI.Workflows.Generators/Generation/SourceBuilder.cs Code generation logic
src/Microsoft.Agents.AI.Workflows.Generators/Diagnostics/DiagnosticDescriptors.cs Analyzer diagnostic definitions

Files Modified

Project Files

File Changes
src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj Added generator project reference and InternalsVisibleTo for generator tests
Directory.Packages.props Added Microsoft.CodeAnalysis.Analyzers version 3.11.0
agent-framework-dotnet.slnx Added generator project to solution

Obsolete Annotations

File Changes
src/Microsoft.Agents.AI.Workflows/Reflection/ReflectingExecutor.cs Added [Obsolete] attribute with migration guidance
src/Microsoft.Agents.AI.Workflows/Reflection/IMessageHandler.cs Added [Obsolete] to both IMessageHandler<T> and IMessageHandler<T,TResult> interfaces

Pragma Suppressions for Internal Obsolete Usage

File Changes
src/Microsoft.Agents.AI.Workflows/Executor.cs Added #pragma warning disable CS0618
src/Microsoft.Agents.AI.Workflows/StatefulExecutor.cs Added #pragma warning disable CS0618
src/Microsoft.Agents.AI.Workflows/Reflection/RouteBuilderExtensions.cs Added #pragma warning disable CS0618
src/Microsoft.Agents.AI.Workflows/Reflection/MessageHandlerInfo.cs Added #pragma warning disable CS0618

Test File Pragma Suppressions

File Changes
tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/01_Simple_Workflow_Sequential.cs Added #pragma warning disable CS0618 for legacy pattern testing
tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/02_Simple_Workflow_Condition.cs Added #pragma warning disable CS0618 for legacy pattern testing
tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/03_Simple_Workflow_Loop.cs Added #pragma warning disable CS0618 for legacy pattern testing
tests/Microsoft.Agents.AI.Workflows.UnitTests/ReflectionSmokeTest.cs Added #pragma warning disable CS0618 for legacy pattern testing

Attribute Definitions

MessageHandlerAttribute

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

SendsMessageAttribute

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class SendsMessageAttribute : Attribute
{
    public Type Type { get; }
    public SendsMessageAttribute(Type type) => this.Type = Throw.IfNull(type);
}

YieldsMessageAttribute

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class YieldsMessageAttribute : Attribute
{
    public Type Type { get; }
    public YieldsMessageAttribute(Type type) => this.Type = Throw.IfNull(type);
}

Diagnostic Rules

ID Severity Description
WFGEN001 Error Handler method must have at least 2 parameters (message and IWorkflowContext)
WFGEN002 Error Handler method's second parameter must be IWorkflowContext
WFGEN003 Error Handler method must return void, ValueTask, or ValueTask
WFGEN004 Error Executor class with [MessageHandler] methods must be declared as partial
WFGEN005 Warning [MessageHandler] attribute on method in non-Executor class (ignored)
WFGEN006 Info ConfigureRoutes already defined manually, [MessageHandler] methods ignored
WFGEN007 Error Handler method's third parameter (if present) must be CancellationToken

Handler Signature Support

The generator supports the following method signatures:

Return Type Parameters Generated Call
void (TMessage, IWorkflowContext) AddHandler<TMessage>(this.Method)
void (TMessage, IWorkflowContext, CancellationToken) AddHandler<TMessage>(this.Method)
ValueTask (TMessage, IWorkflowContext) AddHandler<TMessage>(this.Method)
ValueTask (TMessage, IWorkflowContext, CancellationToken) AddHandler<TMessage>(this.Method)
TResult (TMessage, IWorkflowContext) AddHandler<TMessage, TResult>(this.Method)
TResult (TMessage, IWorkflowContext, CancellationToken) AddHandler<TMessage, TResult>(this.Method)
ValueTask<TResult> (TMessage, IWorkflowContext) AddHandler<TMessage, TResult>(this.Method)
ValueTask<TResult> (TMessage, IWorkflowContext, CancellationToken) AddHandler<TMessage, TResult>(this.Method)

Generated Code Example

Input (User Code)

[SendsMessage(typeof(PollToken))]
public partial class MyChatExecutor : Executor
{
    [MessageHandler]
    private async ValueTask<ChatResponse> HandleQueryAsync(
        ChatQuery query, IWorkflowContext ctx, CancellationToken ct)
    {
        return new ChatResponse(...);
    }

    [MessageHandler(Yield = new[] { typeof(StreamChunk) }, Send = new[] { typeof(InternalMessage) })]
    private void HandleStream(StreamRequest req, IWorkflowContext ctx)
    {
        // Handler implementation
    }
}

Output (Generated Code)

// <auto-generated/>
#nullable enable

namespace MyNamespace;

partial class MyChatExecutor
{
    protected override RouteBuilder ConfigureRoutes(RouteBuilder 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));
        return types;
    }

    protected override ISet<Type> ConfigureYieldTypes()
    {
        var types = base.ConfigureYieldTypes();
        types.Add(typeof(ChatResponse));
        types.Add(typeof(StreamChunk));
        return types;
    }
}

Build Issues Resolved

1. NU1008 - Central Package Management

Package references in the generator project had inline versions, which conflicts with central package management. Fixed by removing Version attributes from PackageReference items.

2. RS2008 - Analyzer Release Tracking

Roslyn requires analyzer release tracking documentation. Fixed by adding <NoWarn>$(NoWarn);RS2008</NoWarn> to the generator project.

3. CA1068 - CancellationToken Parameter Order

Method parameters were in wrong order. Fixed by reordering CancellationToken to be last.

4. RCS1146 - Conditional Access

Used null check with && instead of ?. operator. Fixed by using conditional access.

5. CA1310 - StringComparison

StartsWith(string) calls without StringComparison. Fixed by adding StringComparison.Ordinal.

6. CS0103 - Missing Using Directive

Missing using System; in SemanticAnalyzer.cs. Fixed by adding the using directive.

7. CS0618 - Obsolete Warnings as Errors

Internal uses of obsolete types caused build failures (TreatWarningsAsErrors). Fixed by adding #pragma warning disable CS0618 to affected internal files and test files.

8. NU1109 - Package Version Conflict

Microsoft.CodeAnalysis.Analyzers 3.3.4 conflicts with Microsoft.CodeAnalysis.CSharp 4.14.0 which requires >= 3.11.0. Fixed by updating version to 3.11.0 in Directory.Packages.props.

9. RS1041 - Wrong Target Framework for Analyzer

The generator was being multi-targeted due to inherited TargetFrameworks from Directory.Build.props. Fixed by clearing TargetFrameworks and only setting TargetFramework to netstandard2.0.

Migration Guide

Before (Reflection-based)

public class MyExecutor : ReflectingExecutor<MyExecutor>, IMessageHandler<MyMessage, MyResult>
{
    public MyExecutor() : base("MyExecutor") { }

    public ValueTask<MyResult> HandleAsync(MyMessage message, IWorkflowContext context, CancellationToken ct)
    {
        // Handler implementation
    }
}

After (Source Generator)

public partial class MyExecutor : Executor
{
    public MyExecutor() : base("MyExecutor") { }

    [MessageHandler]
    private ValueTask<MyResult> HandleAsync(MyMessage message, IWorkflowContext context, CancellationToken ct)
    {
        // Handler implementation
    }
}

Key migration steps:

  1. Change base class from ReflectingExecutor<T> to Executor
  2. Add partial modifier to the class
  3. Remove IMessageHandler<T> interface implementations
  4. Add [MessageHandler] attribute to handler methods
  5. Handler methods can now be any accessibility (private, protected, internal, public)

Future Work

  • Create comprehensive unit tests for the source generator
  • Add integration tests verifying generated routes match reflection-discovered routes
  • Consider adding IDE quick-fix for migrating from ReflectingExecutor<T> pattern