diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 6c564d81b8..06002ead71 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -59,6 +59,7 @@
+
diff --git a/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj
new file mode 100644
index 0000000000..b9efe37eff
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj
@@ -0,0 +1,42 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ WorkflowAndAgentsFunctionApp
+ WorkflowAndAgentsFunctionApp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/Executors.cs b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/Executors.cs
new file mode 100644
index 0000000000..f26d194154
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/Executors.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace WorkflowAndAgentsFunctionApp;
+
+///
+/// Parses and validates the incoming question before sending to AI agents.
+///
+internal sealed class ParseQuestionExecutor() : Executor("ParseQuestion")
+{
+ public override ValueTask HandleAsync(
+ string message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"[ParseQuestion] Preparing question: \"{message}\"");
+
+ string formattedQuestion = message.Trim();
+ if (!formattedQuestion.EndsWith('?'))
+ {
+ formattedQuestion += "?";
+ }
+
+ return ValueTask.FromResult(formattedQuestion);
+ }
+}
+
+///
+/// Aggregates responses from multiple AI agents into a unified response.
+///
+internal sealed class ResponseAggregatorExecutor() : Executor("ResponseAggregator")
+{
+ public override ValueTask HandleAsync(
+ string[] message,
+ IWorkflowContext context,
+ CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"[Aggregator] Received {message.Length} AI agent responses, combining...");
+
+ string aggregatedResult = "AI EXPERT PANEL RESPONSES\n" +
+ "═════════════════════════\n\n";
+
+ for (int i = 0; i < message.Length; i++)
+ {
+ string expertLabel = i == 0 ? "PHYSICIST" : "CHEMIST";
+ aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
+ }
+
+ aggregatedResult += $"Summary: Received perspectives from {message.Length} AI experts.";
+
+ return ValueTask.FromResult(aggregatedResult);
+ }
+}
diff --git a/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/Program.cs b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/Program.cs
new file mode 100644
index 0000000000..664613a1be
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/Program.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates the THREE ways to configure durable agents and workflows
+// in an Azure Functions app:
+//
+// 1. ConfigureDurableAgents() - For standalone agents only
+// 2. ConfigureDurableWorkflows() - For workflows only
+// 3. ConfigureDurableOptions() - For both agents AND workflows
+//
+// KEY: All methods can be called MULTIPLE times - configurations are ADDITIVE.
+//
+// Workflow structure:
+//
+// PhysicsExpertReview: ParseQuestion ──► Physicist (AI Agent)
+//
+// ExpertTeamReview: ParseQuestion ──┬──► Physicist (AI Agent) ──┬──► Aggregator
+// └──► Chemist (AI Agent) ──┘
+//
+// ChemistryExpertReview: ParseQuestion ──► Chemist (AI Agent)
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using OpenAI.Chat;
+using WorkflowAndAgentsFunctionApp;
+
+// Configuration
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+
+// Create Azure OpenAI client
+AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
+
+// Create AI agents
+AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Explain concepts clearly in 2-3 sentences.", "Physicist");
+AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Explain concepts clearly in 2-3 sentences.", "Chemist");
+AIAgent biologist = chatClient.AsAIAgent("You are a biology expert. Explain concepts clearly in 2-3 sentences.", "Biologist");
+
+// Create custom executors
+ParseQuestionExecutor questionParser = new();
+ResponseAggregatorExecutor responseAggregator = new();
+
+// Workflow 1: Single-agent workflow (Physics)
+Workflow physicsWorkflow = new WorkflowBuilder(questionParser)
+ .WithName("PhysicsExpertReview")
+ .AddEdge(questionParser, physicist)
+ .Build();
+
+// Workflow 2: Multi-agent workflow with fan-out/fan-in (Expert Team)
+Workflow expertTeamWorkflow = new WorkflowBuilder(questionParser)
+ .WithName("ExpertTeamReview")
+ .AddFanOutEdge(questionParser, [physicist, chemist])
+ .AddFanInEdge([physicist, chemist], responseAggregator)
+ .Build();
+
+// Workflow 3: Single-agent workflow (Chemistry)
+Workflow chemistryWorkflow = new WorkflowBuilder(questionParser)
+ .WithName("ChemistryExpertReview")
+ .AddEdge(questionParser, chemist)
+ .Build();
+
+// Configure the function app using all 3 methods to demonstrate additive configuration.
+// Each method can be called one or more times - configurations accumulate.
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+
+ // METHOD 1: ConfigureDurableAgents - for standalone agents only.
+ // Registers the biologist agent as a standalone agent (not part of any workflow).
+ .ConfigureDurableAgents(options => options.AddAIAgent(biologist))
+
+ // METHOD 2: ConfigureDurableWorkflows - for workflows only.
+ // Registers the physics workflow. Agents referenced in the workflow (Physicist) are auto-discovered.
+ .ConfigureDurableWorkflows(options => options.AddWorkflow(physicsWorkflow))
+
+ // METHOD 3: ConfigureDurableOptions - for both agents AND workflows.
+ // Registers the chemist agent explicitly and the expert team workflow together.
+ .ConfigureDurableOptions(options =>
+ {
+ options.Agents.AddAIAgent(chemist);
+ options.Workflows.AddWorkflow(expertTeamWorkflow);
+ })
+
+ // Second call to ConfigureDurableOptions (additive - adds to existing config).
+ .ConfigureDurableOptions(options => options.Workflows.AddWorkflow(chemistryWorkflow))
+
+ .Build();
+app.Run();
diff --git a/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/README.md b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/README.md
new file mode 100644
index 0000000000..433216636c
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/README.md
@@ -0,0 +1,73 @@
+# Workflow and Agents - Azure Functions Sample
+
+This sample demonstrates how to combine **custom executors with AI agents** (backed by Azure OpenAI) in workflows hosted as Azure Functions. It shows single-agent workflows, multi-agent fan-out/fan-in workflows, and how multiple workflows can be registered together.
+
+## Key Concepts Demonstrated
+
+- Using **AI agents** (Azure OpenAI) as workflow executors alongside custom executors
+- **Fan-out/fan-in** pattern with multiple AI agents running in parallel
+- Registering **multiple workflows** in a single Azure Functions app via `ConfigureDurableOptions`
+
+## Overview
+
+Three workflows are registered, each demonstrating different patterns:
+
+```
+PhysicsExpertReview: ParseQuestion ──► Physicist (AI Agent)
+
+ExpertTeamReview: ParseQuestion ──┬──► Physicist (AI Agent) ──┬──► Aggregator
+ └──► Chemist (AI Agent) ──┘
+
+ChemistryExpertReview: ParseQuestion ──► Chemist (AI Agent)
+```
+
+| Executor | Type | Description |
+|----------|------|-------------|
+| ParseQuestion | Custom Executor | Validates and formats the incoming question |
+| Physicist | AI Agent | Physics expert backed by Azure OpenAI |
+| Chemist | AI Agent | Chemistry expert backed by Azure OpenAI |
+| ResponseAggregator | Custom Executor | Combines responses from multiple AI agents |
+
+## Environment Setup
+
+This sample requires:
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download)
+- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
+- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-azure-managed-storage) running locally (default: `http://localhost:8080`)
+- [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local Azure Storage emulation
+- An [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/) deployment
+
+### Configuration
+
+Set the following environment variables in `local.settings.json`:
+
+| Variable | Description |
+|----------|-------------|
+| `AZURE_OPENAI_ENDPOINT` | Your Azure OpenAI endpoint URL |
+| `AZURE_OPENAI_DEPLOYMENT` | The model deployment name (e.g., `gpt-4o`) |
+| `AZURE_OPENAI_KEY` | *(Optional)* API key. If not set, uses `AzureCliCredential` |
+
+## Running the Sample
+
+```bash
+cd dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents
+func start
+```
+
+### Testing
+
+**Single-agent workflow (Physics):**
+```bash
+curl -X POST http://localhost:7071/api/workflows/PhysicsExpertReview/run -H "Content-Type: text/plain" -d "What is the relationship between energy and mass?"
+```
+
+**Multi-agent workflow (Expert Team):**
+```bash
+curl -X POST http://localhost:7071/api/workflows/ExpertTeamReview/run -H "Content-Type: text/plain" -d "How does radiation affect living cells?"
+```
+
+**Single-agent workflow (Chemistry):**
+```bash
+curl -X POST http://localhost:7071/api/workflows/ChemistryExpertReview/run -H "Content-Type: text/plain" -d "What happens during combustion?"
+```
diff --git a/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/demo.http b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/demo.http
new file mode 100644
index 0000000000..177d3d243f
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/demo.http
@@ -0,0 +1,20 @@
+# Default endpoint address for local testing
+@authority=http://localhost:7071
+
+### Single-agent workflow: Physics expert
+POST {{authority}}/api/workflows/PhysicsExpertReview/run
+Content-Type: text/plain
+
+What is the relationship between energy and mass?
+
+### Multi-agent workflow: Expert team (Physicist + Chemist in parallel)
+POST {{authority}}/api/workflows/ExpertTeamReview/run
+Content-Type: text/plain
+
+How does radiation affect living cells?
+
+### Single-agent workflow: Chemistry expert
+POST {{authority}}/api/workflows/ChemistryExpertReview/run
+Content-Type: text/plain
+
+What happens during combustion?
diff --git a/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/host.json b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
index c7d8cc5a85..59f9043a36 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
@@ -235,19 +235,18 @@ public static class ServiceCollectionExtensions
private static void RegisterTasksFromOptions(DurableTaskRegistry registry, DurableOptions durableOptions)
{
- // Build registrations for all workflows including sub-workflows
+ // Build registrations for all workflows including discovered sub-workflows
List registrations = [];
HashSet registeredActivities = [];
HashSet registeredOrchestrations = [];
- foreach (Workflow workflow in durableOptions.Workflows.Workflows.Values.ToList())
+ foreach (Workflow workflow in durableOptions.Workflows.Workflows.Values)
{
- BuildWorkflowRegistrationRecursive(
- workflow,
- durableOptions.Workflows,
- registrations,
- registeredActivities,
- registeredOrchestrations);
+ string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!);
+ if (registeredOrchestrations.Add(orchestrationName))
+ {
+ registrations.Add(BuildWorkflowRegistration(workflow, registeredActivities));
+ }
}
IReadOnlyDictionary> agentFactories =
@@ -281,39 +280,6 @@ public static class ServiceCollectionExtensions
}
}
- private static void BuildWorkflowRegistrationRecursive(
- Workflow workflow,
- DurableWorkflowOptions workflowOptions,
- List registrations,
- HashSet registeredActivities,
- HashSet registeredOrchestrations)
- {
- string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!);
-
- if (!registeredOrchestrations.Add(orchestrationName))
- {
- return;
- }
-
- registrations.Add(BuildWorkflowRegistration(workflow, registeredActivities));
-
- // Process subworkflows recursively to register them as separate orchestrations
- foreach (SubworkflowBinding subworkflowBinding in workflow.ReflectExecutors()
- .Select(e => e.Value)
- .OfType())
- {
- Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
- workflowOptions.AddWorkflow(subWorkflow);
-
- BuildWorkflowRegistrationRecursive(
- subWorkflow,
- workflowOptions,
- registrations,
- registeredActivities,
- registeredOrchestrations);
- }
- }
-
private static WorkflowRegistrationInfo BuildWorkflowRegistration(
Workflow workflow,
HashSet registeredActivities)
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs
index eb9ee92758..866cf55b4c 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs
@@ -74,6 +74,7 @@ public sealed class DurableWorkflowOptions
///
/// Registers all executors from a workflow, including AI agents if agent options are available.
+ /// Any sub-workflow bindings are automatically discovered and added recursively.
///
private void RegisterWorkflowExecutors(Workflow workflow)
{
@@ -85,6 +86,16 @@ public sealed class DurableWorkflowOptions
this.Executors.Register(executorName, executorId, workflow);
TryRegisterAgent(binding, agentOptions);
+
+ // Automatically discover and register sub-workflows
+ if (binding is SubworkflowBinding subworkflowBinding)
+ {
+ Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
+ if (subWorkflow.Name is not null && !this._workflows.ContainsKey(subWorkflow.Name))
+ {
+ this.AddWorkflow(subWorkflow);
+ }
+ }
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
index f626db2a90..ebe2eda471 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
@@ -21,6 +21,11 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat
private static readonly string s_builtInFunctionsScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location);
#pragma warning restore IL3000
+ ///
+ /// Tracks all registered function names to prevent duplicates across multiple metadata transformers.
+ ///
+ internal static readonly HashSet RegisteredFunctionNames = new(StringComparer.OrdinalIgnoreCase);
+
public DurableAgentFunctionMetadataTransformer(
IReadOnlyDictionary> agents,
ILogger logger,
@@ -42,6 +47,13 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat
foreach (KeyValuePair> kvp in this._agents)
{
string agentName = kvp.Key;
+ string entityFunctionName = AgentSessionId.ToEntityName(agentName);
+
+ // Skip if this entity function has already been registered by another transformer
+ if (!RegisteredFunctionNames.Add(entityFunctionName))
+ {
+ continue;
+ }
this._logger.LogRegisteringTriggerForAgent(agentName, "entity");
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
index eb549952db..7c6157823e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
@@ -3,7 +3,6 @@
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
-using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
@@ -21,6 +20,16 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
///
public static class FunctionsApplicationBuilderExtensions
{
+ ///
+ /// Tracks workflow orchestration names that have already been registered to prevent duplicates.
+ ///
+ private static readonly HashSet s_registeredOrchestrations = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Tracks whether middleware and shared services have been registered.
+ ///
+ private static bool s_middlewareRegistered;
+
///
/// Configures durable agents and workflows in a unified way.
///
@@ -31,6 +40,7 @@ public static class FunctionsApplicationBuilderExtensions
/// This method provides a unified configuration point for both durable agents and workflows.
/// It automatically generates HTTP API endpoints for agents and workflows, and configures
/// the necessary middleware and services for durable execution.
+ /// Multiple calls to this method are supported and configurations are composed additively.
///
public static FunctionsApplicationBuilder ConfigureDurableOptions(
this FunctionsApplicationBuilder builder,
@@ -39,56 +49,60 @@ public static class FunctionsApplicationBuilderExtensions
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configure);
- DurableOptions options = new();
- configure(options);
+ // Delegate to the shared DurableOptions registration in Microsoft.Agents.AI.DurableTask.
+ // This ensures a single shared DurableOptions instance across all Configure* calls.
+ builder.Services.ConfigureDurableOptions(options => configure(options));
- builder.Services.AddSingleton(options);
+ // Read the shared options to check if workflows were added
+ DurableOptions sharedOptions = GetOrCreateSharedOptions(builder.Services);
- if (options.Workflows.Workflows.Count > 0)
+ if (sharedOptions.Workflows.Workflows.Count > 0)
{
- ConfigureWorkflowOrchestrations(builder, options.Workflows);
- // Do things to enable workflow as orchestrator functions.
- // Register the Workflow metadata transformer.
- builder.ConfigureDurableWorkflows(durableWorkflwoOptions =>
- {
- // what
- });
+ ConfigureWorkflowOrchestrations(builder, sharedOptions.Workflows);
- builder.Services.AddSingleton();
-
- builder.UseWhen(static context =>
- string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
- string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
- string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal)
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
-
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal)
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
- );
- builder.Services.AddSingleton();
-
- //builder.UseWhen(static context =>
- // string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
- // || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal)
- // || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
- // || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal)
- // );
- //builder.Services.AddSingleton();
+ builder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton());
}
+ EnsureMiddlewareRegistered(builder);
+
return builder;
}
- private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflows)
+ ///
+ /// Gets or creates a shared instance from the service collection.
+ ///
+ private static DurableOptions GetOrCreateSharedOptions(IServiceCollection services)
{
- // Discover sub-workflows recursively and add them to the workflows dictionary
- // so they are registered as separate orchestrations alongside the main workflows.
- DiscoverSubWorkflows(workflows);
+ ServiceDescriptor? existingDescriptor = services.FirstOrDefault(
+ d => d.ServiceType == typeof(DurableOptions) && d.ImplementationInstance is not null);
+
+ if (existingDescriptor?.ImplementationInstance is DurableOptions existing)
+ {
+ return existing;
+ }
+
+ DurableOptions options = new();
+ services.AddSingleton(options);
+ return options;
+ }
+
+ private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflowOptions)
+ {
+ // Collect only workflows that haven't been registered yet
+ List newWorkflowNames = workflowOptions.Workflows
+ .Select(kp => kp.Key)
+ .Where(name => s_registeredOrchestrations.Add(name))
+ .ToList();
+
+ if (newWorkflowNames.Count == 0)
+ {
+ return;
+ }
builder.ConfigureDurableWorker().AddTasks(tasks =>
{
- foreach (string workflowName in workflows.Workflows.Select(kp => kp.Key))
+ foreach (string workflowName in newWorkflowNames)
{
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
@@ -109,48 +123,19 @@ public static class FunctionsApplicationBuilderExtensions
});
}
- private static void DiscoverSubWorkflows(DurableWorkflowOptions workflows)
- {
- HashSet visited = new(workflows.Workflows.Keys);
- Queue queue = new(workflows.Workflows.Values);
-
- while (queue.Count > 0)
- {
- Workflow workflow = queue.Dequeue();
-
- foreach (ExecutorBinding binding in workflow.ReflectExecutors().Values)
- {
- if (binding is SubworkflowBinding subworkflowBinding)
- {
- Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
- if (subWorkflow.Name is not null && visited.Add(subWorkflow.Name))
- {
- workflows.AddWorkflow(subWorkflow);
- queue.Enqueue(subWorkflow);
- }
- }
- }
- }
- }
- internal static FunctionsApplicationBuilder RegisterWorkflowServices(this FunctionsApplicationBuilder builder)
- {
- // Register FunctionsWorkflowRunner as a singleton
- // builder.Services.TryAddSingleton();
-
- // Also register it as DurableWorkflowRunner so orchestrations can resolve it by base type
- //builder.Services.TryAddSingleton(sp => sp.GetRequiredService());
-
- builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
-
- return builder;
- }
-
///
- /// Configures durable workflow services for the application and allows customization of durable workflow options.
+ /// Configures durable workflow services for the Azure Functions application.
///
- /// This method registers the services required for durable workflows using
- /// Microsoft.DurableTask.Workflows. Call this method during application startup to enable durable workflows in your
- /// Azure Functions app.
+ ///
+ ///
+ /// This method registers the services required for durable workflows in an Azure Functions app,
+ /// including orchestration triggers, HTTP triggers, and activity/entity triggers for executors.
+ ///
+ ///
+ /// Multiple calls to this method are supported and configurations are composed additively.
+ /// Agents referenced in workflows are automatically discovered and registered.
+ ///
+ ///
/// The application builder used to configure services and middleware for the Azure Functions app.
/// A delegate that is used to configure the durable workflow options. Cannot be null.
/// The same instance that this method was called on, to support method
@@ -159,13 +144,7 @@ public static class FunctionsApplicationBuilderExtensions
{
ArgumentNullException.ThrowIfNull(configure);
- //RegisterWorkflowServices(builder);
- //builder.Services.AddSingleton();
-
- // The main durable workflows services registration is done in Microsoft.DurableTask.Workflows.
- builder.Services.ConfigureDurableWorkflows(configure);
-
- return builder;
+ return builder.ConfigureDurableOptions(options => configure(options.Workflows));
}
///
@@ -188,19 +167,31 @@ public static class FunctionsApplicationBuilderExtensions
builder.Services.AddSingleton();
- // Handling of built-in function execution for Agent HTTP, MCP tool, or Entity invocations.
- builder.UseWhen(static context =>
- string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
- string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
- string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal)
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
-
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal)
- || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
- );
- builder.Services.AddSingleton();
+ EnsureMiddlewareRegistered(builder);
return builder;
}
+
+ ///
+ /// Registers the built-in function execution middleware and executor exactly once.
+ ///
+ private static void EnsureMiddlewareRegistered(FunctionsApplicationBuilder builder)
+ {
+ if (s_middlewareRegistered)
+ {
+ return;
+ }
+
+ s_middlewareRegistered = true;
+
+ builder.UseWhen(static context =>
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
+ );
+ builder.Services.TryAddSingleton();
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowFunctionMetadataTransformer.cs
index 49a23d0584..682ec6e292 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowFunctionMetadataTransformer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Workflows/DurableWorkflowFunctionMetadataTransformer.cs
@@ -10,7 +10,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMetadataTransformer
{
- private static readonly HashSet _registeredFunctionNames = new();
private readonly ILogger _logger;
private readonly DurableWorkflowOptions _options;
@@ -29,6 +28,14 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
foreach (var workflow in this._options.Workflows)
{
+ string httpFunctionName = $"{BuiltInFunctions.HttpPrefix}{workflow.Key}";
+
+ // Skip if this workflow's HTTP trigger has already been registered by another transformer
+ if (!DurableAgentFunctionMetadataTransformer.RegisteredFunctionNames.Add(httpFunctionName))
+ {
+ continue;
+ }
+
this._logger.LogAddingWorkflowFunction(workflow.Key);
// Currently due to how durable executor is registered, we are not able to bind TaskOrechestrationContext parameter properly
@@ -80,8 +87,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
string executorName = WorkflowNamingHelper.GetExecutorName(executorId);
string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
- // Skip if this function has already been registered by another workflow
- if (!_registeredFunctionNames.Add(functionName))
+ // Skip if this function has already been registered by another workflow or transformer
+ if (!DurableAgentFunctionMetadataTransformer.RegisteredFunctionNames.Add(functionName))
{
this._logger.LogSkippingDuplicateFunction(functionName, workflow.Key);
continue;