// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Agents.AI.Workflows.Generators.Models;
namespace Microsoft.Agents.AI.Workflows.Generators.Generation;
///
/// Generates source code for executor route configuration.
///
///
/// This builder produces a partial class file that overrides ConfigureRoutes to register
/// handlers discovered via [MessageHandler] attributes. It may also generate ConfigureSentTypes
/// and ConfigureYieldTypes overrides when [SendsMessage] or [YieldsOutput] attributes are present.
///
internal static class SourceBuilder
{
internal const string IndentUnit = " ";
///
/// Generates the complete source file for an executor's generated partial class.
///
/// The analyzed executor information containing class metadata and handler details.
/// The generated C# source code as a string.
public static string Generate(ExecutorInfo info)
{
var sb = new StringBuilder();
// File header
sb.AppendLine("// ");
sb.AppendLine("#nullable enable");
sb.AppendLine();
// Using directives
sb.AppendLine("using System;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using Microsoft.Agents.AI.Workflows;");
sb.AppendLine();
// Namespace
if (!string.IsNullOrWhiteSpace(info.Namespace))
{
sb.AppendLine($"namespace {info.Namespace};");
sb.AppendLine();
}
// For nested classes, we must emit partial declarations for each containing type.
// Example: if MyExecutor is nested in Outer.Inner, we emit:
// partial class Outer { partial class Inner { partial class MyExecutor { ... } } }
string indent = "";
if (info.IsNested)
{
foreach (string containingType in info.ContainingTypeChain.Split('.'))
{
sb.AppendLine($"{indent}partial class {containingType}");
sb.AppendLine($"{indent}{{");
indent += IndentUnit;
}
}
// Class declaration
sb.AppendLine($"{indent}partial class {info.ClassName}{info.GenericParameters}");
sb.AppendLine($"{indent}{{");
string memberIndent = indent + IndentUnit;
// ConfigureProtocol
sb.AppendLine($"{memberIndent}protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)");
sb.AppendLine($"{memberIndent}{{");
string bodyIndent = memberIndent + IndentUnit;
if (info.BaseHasConfigureProtocol)
{
sb.Append($"{bodyIndent}return base.ConfigureProtocol(protocolBuilder)");
bodyIndent += " ";
}
else
{
sb.Append($"{bodyIndent}return protocolBuilder");
}
// Only generate protocol overrides if [SendsMessage] or [YieldsOutput] attributes are present.
// Without these attributes, we rely on the base class defaults.
if (info.ShouldGenerateSentMessageRegistrations)
{
GenerateConfigureSentTypes(sb, info, bodyIndent);
}
if (info.ShouldGenerateYieldedOutputRegistrations)
{
GenerateConfigureYieldTypes(sb, info, bodyIndent);
}
// Only generate ConfigureRoutes if there are handlers
if (info.Handlers.Count > 0)
{
GenerateConfigureRoutes(sb, info, bodyIndent);
}
else
{
sb.AppendLine(";");
}
// Close ConfigureProtocol
sb.AppendLine($"{memberIndent}}}");
// Close class
sb.AppendLine($"{indent}}}");
// Close nested classes
if (info.IsNested)
{
string[] containingTypes = info.ContainingTypeChain.Split('.');
for (int i = containingTypes.Length - 1; i >= 0; i--)
{
indent = new string(' ', i * 4);
sb.AppendLine($"{indent}}}");
}
}
return sb.ToString();
}
///
/// Generates the ConfigureRoutes override that registers all [MessageHandler] methods.
///
private static void GenerateConfigureRoutes(StringBuilder sb, ExecutorInfo info, string indent)
{
sb.AppendLine(".ConfigureRoutes(ConfigureRoutes);");
sb.AppendLine($"{indent}void ConfigureRoutes(RouteBuilder routeBuilder)");
sb.AppendLine($"{indent}{{");
string bodyIndent = indent + IndentUnit;
// Generate handler registrations using fluent AddHandler calls.
// RouteBuilder.AddHandler registers a void handler; AddHandler registers one with a return value.
if (info.Handlers.Count == 1)
{
HandlerInfo handler = info.Handlers[0];
sb.AppendLine($"{bodyIndent}routeBuilder");
sb.Append($"{bodyIndent} .AddHandler");
AppendHandlerGenericArgs(sb, handler);
sb.AppendLine($"(this.{handler.MethodName});");
}
else
{
// Multiple handlers: chain fluent calls, semicolon only on the last one.
sb.AppendLine($"{bodyIndent}routeBuilder");
for (int i = 0; i < info.Handlers.Count; i++)
{
HandlerInfo handler = info.Handlers[i];
sb.Append($"{bodyIndent} .AddHandler");
AppendHandlerGenericArgs(sb, handler);
sb.Append($"(this.{handler.MethodName})");
sb.AppendLine();
}
// Remove last newline without using that System.Environment which is banned from use in analyzers
var newLineLength = new StringBuilder().AppendLine().Length;
sb.Remove(sb.Length - newLineLength, newLineLength);
sb.AppendLine(";");
}
sb.AppendLine($"{indent}}}");
}
///
/// Appends generic type arguments for AddHandler based on whether the handler returns a value.
///
private static void AppendHandlerGenericArgs(StringBuilder sb, HandlerInfo handler)
{
// Handlers returning ValueTask use single type arg; ValueTask uses two.
if (handler.HasOutput && handler.OutputTypeName != null)
{
sb.Append($"<{handler.InputTypeName}, {handler.OutputTypeName}>");
}
else
{
sb.Append($"<{handler.InputTypeName}>");
}
}
///
/// Generates ConfigureSentTypes override declaring message types this executor sends via context.SendMessageAsync.
///
///
/// Types come from [SendsMessage] attributes on the class or individual handler methods.
/// This enables workflow protocol validation at build time.
///
private static void GenerateConfigureSentTypes(StringBuilder sb, ExecutorInfo info, string indent)
{
// Track types to avoid emitting duplicate Add calls (the set handles runtime dedup,
// but cleaner generated code is easier to read).
var addedTypes = new HashSet();
foreach (var type in info.ClassSendTypes.Where(type => addedTypes.Add(type)))
{
sb.AppendLine($".SendsMessage<{type}>()");
sb.Append(indent);
}
foreach (var handler in info.Handlers)
{
foreach (var type in handler.SendTypes.Where(type => addedTypes.Add(type)))
{
sb.AppendLine($".SendsMessage<{type}>()");
sb.Append(indent);
}
}
}
///
/// Generates ConfigureYieldTypes override declaring message types this executor yields via context.YieldOutputAsync.
///
///
/// Types come from [YieldsOutput] attributes and handler return types (ValueTask<T>).
/// This enables workflow protocol validation at build time.
///
private static void GenerateConfigureYieldTypes(StringBuilder sb, ExecutorInfo info, string indent)
{
// Track types to avoid emitting duplicate Add calls (the set handles runtime dedup,
// but cleaner generated code is easier to read).
var addedTypes = new HashSet();
foreach (var type in info.ClassYieldTypes.Where(type => addedTypes.Add(type)))
{
sb.AppendLine($".YieldsOutput<{type}>()");
sb.Append(indent);
}
foreach (var handler in info.Handlers)
{
foreach (var type in handler.YieldTypes.Where(type => addedTypes.Add(type)))
{
sb.AppendLine($".YieldsOutput<{type}>()");
sb.Append(indent);
}
}
}
}