// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
///
/// Extension methods for the class.
///
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.
///
/// The Functions application builder.
/// A delegate to configure the durable options.
/// The Functions application builder for method chaining.
///
/// 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,
Action configure)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configure);
// 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));
// Read the shared options to check if workflows were added
DurableOptions sharedOptions = GetOrCreateSharedOptions(builder.Services);
if (sharedOptions.Workflows.Workflows.Count > 0)
{
ConfigureWorkflowOrchestrations(builder, sharedOptions.Workflows);
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton());
}
EnsureMiddlewareRegistered(builder);
return builder;
}
///
/// Gets or creates a shared instance from the service collection.
///
private static DurableOptions GetOrCreateSharedOptions(IServiceCollection services)
{
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 newWorkflowNames)
{
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
tasks.AddOrchestratorFunc, string>(
orchestrationFunctionName,
async (orchestrationContext, orchInput) =>
{
FunctionContext functionContext = orchestrationContext.GetFunctionContext()
?? throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService();
ILogger logger = orchestrationContext.CreateReplaySafeLogger(orchestrationFunctionName);
DurableWorkflowInput