From 56bde4a3dd7e8c3ceed7e383fb6f56837c9fd1da Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 28 Mar 2026 13:23:14 -0700 Subject: [PATCH 01/11] Revert "Revert durable task scheduler Azure Functions changes" This reverts commit 2eebd53f9d55f09f80966a6ef5a53a3d81d932a1. --- Aspire.slnx | 4 + Directory.Packages.props | 3 + .../AzureFunctionsWithDts.AppHost.csproj | 22 ++ .../AzureFunctionsWithDts.AppHost/Program.cs | 14 + .../Properties/launchSettings.json | 41 +++ .../AzureFunctionsWithDts.Functions.csproj | 43 +++ .../MyOrchestrationTrigger.cs | 29 ++ .../Program.cs | 12 + .../Properties/launchSettings.json | 9 + .../AzureFunctionsWithDts.Functions/host.json | 22 ++ .../DurableTaskHubNameAnnotation.cs | 18 ++ .../DurableTask/DurableTaskHubResource.cs | 47 ++++ .../DurableTaskResourceExtensions.cs | 254 +++++++++++++++++ ...TaskSchedulerConnectionStringAnnotation.cs | 18 ++ ...TaskSchedulerEmulatorContainerImageTags.cs | 16 ++ .../DurableTaskSchedulerEmulatorResource.cs | 21 ++ .../DurableTaskSchedulerResource.cs | 61 +++++ src/Aspire.Hosting.Azure.Functions/README.md | 58 ++++ .../DurableTaskResourceExtensionsTests.cs | 256 ++++++++++++++++++ 19 files changed, 948 insertions(+) create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/AzureFunctionsWithDts.AppHost.csproj create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Properties/launchSettings.json create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/AzureFunctionsWithDts.Functions.csproj create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Program.cs create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Properties/launchSettings.json create mode 100644 playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/host.json create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubNameAnnotation.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerConnectionStringAnnotation.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorContainerImageTags.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorResource.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs create mode 100644 tests/Aspire.Hosting.Azure.Tests/DurableTaskResourceExtensionsTests.cs diff --git a/Aspire.slnx b/Aspire.slnx index 52300ace4ae..4ea6dbc4cdc 100644 --- a/Aspire.slnx +++ b/Aspire.slnx @@ -142,6 +142,10 @@ + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 7be83c1fba4..6f47a76767e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -174,6 +174,9 @@ + + + diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/AzureFunctionsWithDts.AppHost.csproj b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/AzureFunctionsWithDts.AppHost.csproj new file mode 100644 index 00000000000..141e339b6f1 --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/AzureFunctionsWithDts.AppHost.csproj @@ -0,0 +1,22 @@ + + + + Exe + $(DefaultTargetFramework) + enable + enable + true + DC3A64A6-3991-41E2-956F-BFACC8091EC1 + + + + + + + + + + + + + diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs new file mode 100644 index 00000000000..44e7d42bc99 --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs @@ -0,0 +1,14 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var storage = builder.AddAzureStorage("storage").RunAsEmulator(); + +var scheduler = builder.AddDurableTaskScheduler("scheduler").RunAsEmulator(); + +var taskHub = scheduler.AddTaskHub("taskhub"); + +builder.AddAzureFunctionsProject("funcapp") + .WithHostStorage(storage) + .WithEnvironment("DURABLE_TASK_SCHEDULER_CONNECTION_STRING", scheduler) + .WithEnvironment("TASKHUB_NAME", taskHub.Resource.TaskHubName); + +builder.Build().Run(); diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Properties/launchSettings.json b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000000..f5f441697c0 --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17244;http://localhost:15054", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21003", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22110" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15054", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19010", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20125" + } + }, + "generate-manifest": { + "commandName": "Project", + "launchBrowser": true, + "dotnetRunMessages": true, + "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", + "applicationUrl": "http://localhost:15888", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175" + } + } + } +} diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/AzureFunctionsWithDts.Functions.csproj b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/AzureFunctionsWithDts.Functions.csproj new file mode 100644 index 00000000000..ca8d05550d1 --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/AzureFunctionsWithDts.Functions.csproj @@ -0,0 +1,43 @@ + + + $(DefaultTargetFramework) + v4 + Exe + enable + enable + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + + + + + + + + + + + diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs new file mode 100644 index 00000000000..b0c9f43129d --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs @@ -0,0 +1,29 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.Extensions.Logging; + +public class MyOrchestrationTrigger +{ + [Function("Chaining")] + public static async Task Run( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestrationTrigger)); + logger.LogInformation("Saying hello."); + var outputs = new List(); + + // Replace name and input with values relevant for your Durable Functions Activity + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Tokyo")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Seattle")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "London")); + + // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] + return outputs; + } + + [Function(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext) + { + return $"Hello {name}!"; + } +} \ No newline at end of file diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Program.cs b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Program.cs new file mode 100644 index 00000000000..2ede5b1469f --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Program.cs @@ -0,0 +1,12 @@ +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Hosting; + +var builder = FunctionsApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.ConfigureFunctionsWebApplication(); + +var host = builder.Build(); + +host.Run(); diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Properties/launchSettings.json b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Properties/launchSettings.json new file mode 100644 index 00000000000..fa595ad7768 --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "AzureFunctionsWithDts_Functions": { + "commandName": "Project", + "commandLineArgs": "--port 7071", + "launchBrowser": false + } + } +} diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/host.json b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/host.json new file mode 100644 index 00000000000..455e0e8d677 --- /dev/null +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/host.json @@ -0,0 +1,22 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + }, + "extensions": { + "durableTask": { + "hubName": "%TASKHUB_NAME%", + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "telemetryMode": "openTelemetry" +} \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubNameAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubNameAnnotation.cs new file mode 100644 index 00000000000..a5f760aacdd --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubNameAnnotation.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure.DurableTask; + +/// +/// Annotation that supplies the name for an existing Durable Task hub resource. +/// +/// The name of the existing Durable Task hub. +internal sealed class DurableTaskHubNameAnnotation(object hubName) : IResourceAnnotation +{ + /// + /// Gets the name of the existing Durable Task hub. + /// + public object HubName { get; } = hubName; +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs new file mode 100644 index 00000000000..2e57c156407 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure.DurableTask; + +/// +/// Represents a Durable Task hub resource. A Task Hub groups durable orchestrations and activities. +/// This resource extends the scheduler connection string with the TaskHub name so that clients can +/// connect to the correct hub. +/// +/// The logical name of the Task Hub (used as the TaskHub value). +/// The durable task scheduler resource whose connection string is the base for this hub. +public sealed class DurableTaskHubResource(string name, DurableTaskSchedulerResource scheduler) + : Resource(name), IResourceWithConnectionString, IResourceWithParent +{ + /// + /// Gets the connection string expression composed of the scheduler connection string and the TaskHub name. + /// + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{Parent.ConnectionStringExpression};TaskHub={TaskHubName}"); + + /// + /// Gets the parent durable task scheduler resource that provides the base connection string. + /// + public DurableTaskSchedulerResource Parent => scheduler; + + /// + /// Gets the name of the Task Hub. If not provided, the logical name of this resource is returned. + /// + public ReferenceExpression TaskHubName => GetTaskHubName(); + + private ReferenceExpression GetTaskHubName() + { + if (this.TryGetLastAnnotation(out var taskHubNameAnnotation)) + { + return taskHubNameAnnotation.HubName switch + { + ParameterResource parameter => ReferenceExpression.Create($"{parameter}"), + string hubName => ReferenceExpression.Create($"{hubName}"), + _ => throw new InvalidOperationException($"Unexpected Task Hub name type: {taskHubNameAnnotation.HubName.GetType().Name}") + }; + } + + return ReferenceExpression.Create($"{Name}"); + } +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs new file mode 100644 index 00000000000..21b5558d8e9 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure.DurableTask; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting; + +/// +/// Extension methods for adding and configuring Durable Task resources within a distributed application. +/// +public static class DurableTaskResourceExtensions +{ + /// + /// Adds a Durable Task scheduler resource to the distributed application. + /// + /// The distributed application builder. + /// The logical name of the scheduler resource. + /// An for the scheduler resource. + /// + /// Add a Durable Task scheduler resource: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var scheduler = builder.AddDurableTaskScheduler("scheduler"); + /// + /// + public static IResourceBuilder AddDurableTaskScheduler(this IDistributedApplicationBuilder builder, string name) + { + var scheduler = new DurableTaskSchedulerResource(name); + + scheduler.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); + + return builder.AddResource(scheduler); + } + + /// + /// Configures the Durable Task scheduler to use an existing scheduler instance referenced by the provided connection string. + /// No new scheduler resource is provisioned. + /// + /// The scheduler resource builder. + /// The connection string referencing the existing Durable Task scheduler instance. + /// The same instance for fluent chaining. + /// + /// The existing resource annotation is only applied when the execution context is not in publish mode. + /// + /// + /// Use an existing scheduler instead of provisioning a new one: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var scheduler = builder.AddDurableTaskScheduler("scheduler") + /// .RunAsExisting("Endpoint=https://example;...;"); + /// + /// + public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, string connectionString) + { + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithAnnotation(new DurableTaskSchedulerConnectionStringAnnotation(connectionString)); + } + + return builder; + } + + /// + /// Configures the Durable Task scheduler to use an existing scheduler instance referenced by the provided connection string. + /// No new scheduler resource is provisioned. + /// + /// The scheduler resource builder. + /// The connection string parameter referencing the existing Durable Task scheduler instance. + /// The same instance for fluent chaining. + /// + /// The existing resource annotation is only applied when the execution context is not in publish mode. + /// + /// + /// Use an existing scheduler where the connection string is supplied via a parameter: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var schedulerConnectionString = builder.AddParameter("schedulerConnectionString"); + /// + /// var scheduler = builder.AddDurableTaskScheduler("scheduler") + /// .RunAsExisting(schedulerConnectionString); + /// + /// + public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString) + { + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithAnnotation(new DurableTaskSchedulerConnectionStringAnnotation(connectionString.Resource)); + } + + return builder; + } + + /// + /// Configures the Durable Task scheduler to run using the local emulator (only in non-publish modes). + /// + /// The resource builder for the scheduler. + /// Callback that exposes underlying container used for emulation to allow for customization. + /// The same instance for chaining. + /// + /// Run the scheduler locally using the emulator: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var scheduler = builder.AddDurableTaskScheduler("scheduler") + /// .RunAsEmulator(); + /// + /// + public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) + { + ArgumentNullException.ThrowIfNull(builder); + + if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + return builder; + } + + // Mark this resource as an emulator for consistent resource identification and tooling support + builder.WithAnnotation(new EmulatorResourceAnnotation()); + + builder.WithEndpoint(name: "grpc", targetPort: 8080) + .WithHttpEndpoint(name: "http", targetPort: 8081) + .WithHttpEndpoint(name: "dashboard", targetPort: 8082) + .WithUrlForEndpoint("dashboard", c => c.DisplayText = "Scheduler Dashboard") + .WithAnnotation(new ContainerImageAnnotation + { + Registry = DurableTaskSchedulerEmulatorContainerImageTags.Registry, + Image = DurableTaskSchedulerEmulatorContainerImageTags.Image, + Tag = DurableTaskSchedulerEmulatorContainerImageTags.Tag + }); + + var emulatorResource = new DurableTaskSchedulerEmulatorResource(builder.Resource); + + var surrogateBuilder = + builder + .ApplicationBuilder + .CreateResourceBuilder(emulatorResource) + .WithEnvironment( + context => + { + ReferenceExpressionBuilder namesBuilder = new(); + + var durableTaskHubNames = + builder + .ApplicationBuilder + .Resources + .OfType() + .Where(th => th.Parent == builder.Resource) + .Select(th => th.TaskHubName) + .ToList(); + + for (int i = 0; i < durableTaskHubNames.Count; i++) + { + if (i > 0) + { + namesBuilder.AppendLiteral(", "); + } + + namesBuilder.AppendFormatted(durableTaskHubNames[i]); + } + + context.EnvironmentVariables["DTS_TASK_HUB_NAMES"] = namesBuilder.Build(); + }); + + configureContainer?.Invoke(surrogateBuilder); + + return builder; + } + + /// + /// Adds a Durable Task hub resource associated with the specified scheduler. + /// + /// The scheduler resource builder. + /// The logical name of the task hub resource. + /// An for the task hub resource. + /// + /// Add a task hub under a scheduler: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var scheduler = builder.AddDurableTaskScheduler("scheduler").RunAsEmulator(); + /// + /// var hub = scheduler.AddTaskHub("hub") + /// .WithTaskHubName("MyTaskHub"); + /// + /// + public static IResourceBuilder AddTaskHub(this IResourceBuilder builder, string name) + { + var hub = new DurableTaskHubResource(name, builder.Resource); + + hub.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); + + var hubBuilder = builder.ApplicationBuilder.AddResource(hub); + + hubBuilder.OnResourceReady( + async (r, e, ct) => + { + var notifications = e.Services.GetRequiredService(); + + var url = builder.Resource.IsEmulator + ? await ReferenceExpression.Create($"{r.Parent.EmulatorDashboardEndpoint}/subscriptions/default/schedulers/default/taskhubs/{r.TaskHubName}").GetValueAsync(ct).ConfigureAwait(false) + : null; + + await notifications.PublishUpdateAsync(r, snapshot => snapshot with + { + State = KnownResourceStates.Running, + Urls = url is not null + ? [new("dashboard", url, false) { DisplayProperties = new() { DisplayName = "Task Hub Dashboard" } }] + : [] + }).ConfigureAwait(false); + }); + + return hubBuilder; + } + + /// + /// Sets the name of the Durable Task hub. + /// + /// The task hub resource builder. + /// The name of the Task Hub. + /// The same instance for fluent chaining. + /// + /// Set the task hub name: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var scheduler = builder.AddDurableTaskScheduler("scheduler").RunAsEmulator(); + /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName("MyTaskHub"); + /// + /// + public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, string taskHubName) + { + return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName)); + } + + /// + /// Sets the name of the Durable Task hub using a parameter resource. + /// + /// The task hub resource builder. + /// A parameter resource that resolves to the Task Hub name. + /// The same instance for fluent chaining. + /// + /// Set the task hub name from a parameter: + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var taskHubName = builder.AddParameter("taskHubName"); + /// + /// var scheduler = builder.AddDurableTaskScheduler("scheduler").RunAsEmulator(); + /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName(taskHubName); + /// + /// + public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, IResourceBuilder taskHubName) + { + return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName.Resource)); + } +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerConnectionStringAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerConnectionStringAnnotation.cs new file mode 100644 index 00000000000..c14ac070c45 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerConnectionStringAnnotation.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure.DurableTask; + +/// +/// Annotation that supplies the connection string for an existing Durable Task scheduler resource. +/// +/// The connection string of the existing Durable Task scheduler. +internal sealed class DurableTaskSchedulerConnectionStringAnnotation(object connectionString) : IResourceAnnotation +{ + /// + /// Gets the connection string of the existing Durable Task scheduler. + /// + public object ConnectionString { get; } = connectionString; +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorContainerImageTags.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorContainerImageTags.cs new file mode 100644 index 00000000000..015f8bd2784 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorContainerImageTags.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Azure.DurableTask; + +internal static class DurableTaskSchedulerEmulatorContainerImageTags +{ + /// mcr.microsoft.com + public const string Registry = "mcr.microsoft.com"; + + /// dts/dts-emulator + public const string Image = "dts/dts-emulator"; + + /// latest + public const string Tag = "latest"; +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorResource.cs new file mode 100644 index 00000000000..da207a102d9 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorResource.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure.DurableTask; + +/// +/// Represents the containerized emulator resource for a . +/// This is used to host the Durable Task scheduler logic when running locally (e.g. with an Azure Functions emulator). +/// +/// The underlying durable task scheduler resource that provides naming and annotations. +/// +/// The emulator resource delegates its annotation collection to the underlying scheduler so that configuration +/// and metadata remain consistent across both representations. +/// +public sealed class DurableTaskSchedulerEmulatorResource(DurableTaskSchedulerResource scheduler) : ContainerResource(scheduler.Name) +{ + /// + public override ResourceAnnotationCollection Annotations => scheduler.Annotations; +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs new file mode 100644 index 00000000000..49b62424fdf --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure.DurableTask; + +/// +/// Represents a Durable Task scheduler resource used in Aspire hosting that provides endpoints +/// and a connection string for Durable Task orchestration scheduling. +/// +/// The unique resource name. +public sealed class DurableTaskSchedulerResource(string name) : Resource(name), IResourceWithEndpoints, IResourceWithConnectionString +{ + /// + /// Gets the expression that resolves to the connection string for the Durable Task scheduler. + /// + public ReferenceExpression ConnectionStringExpression => CreateConnectionString(); + + internal ReferenceExpression EmulatorDashboardEndpoint => CreateDashboardEndpoint(); + + /// + /// Gets a value indicating whether the Durable Task scheduler is running using the local + /// emulator (container) instead of a cloud-hosted service. + /// + public bool IsEmulator => this.IsContainer(); + + private ReferenceExpression CreateConnectionString() + { + if (IsEmulator) + { + var grpcEndpoint = new EndpointReference(this, "grpc"); + + return ReferenceExpression.Create($"Endpoint=http://{grpcEndpoint.Property(EndpointProperty.Host)}:{grpcEndpoint.Property(EndpointProperty.Port)};Authentication=None"); + } + + if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) + { + return connectionStringAnnotation.ConnectionString switch + { + ParameterResource parameterResource => ReferenceExpression.Create($"{parameterResource}"), + string value => ReferenceExpression.Create($"{value}"), + _ => throw new InvalidOperationException($"Unexpected connection string type: {connectionStringAnnotation.ConnectionString.GetType().Name}"), + }; + } + + throw new InvalidOperationException($"Unable to resolve the Durable Task Scheduler connection string. Configure the scheduler using {nameof(DurableTaskResourceExtensions.RunAsEmulator)}() or {nameof(DurableTaskResourceExtensions.RunAsExisting)}(connectionString) before accessing {nameof(ConnectionStringExpression)}."); + } + + private ReferenceExpression CreateDashboardEndpoint() + { + if (IsEmulator) + { + var dashboardEndpoint = new EndpointReference(this, "dashboard"); + + return ReferenceExpression.Create($"http://{dashboardEndpoint.Property(EndpointProperty.Host)}:{dashboardEndpoint.Property(EndpointProperty.Port)}"); + } + + throw new NotImplementedException(); + } +} diff --git a/src/Aspire.Hosting.Azure.Functions/README.md b/src/Aspire.Hosting.Azure.Functions/README.md index 357ff30a739..bcfe22d4820 100644 --- a/src/Aspire.Hosting.Azure.Functions/README.md +++ b/src/Aspire.Hosting.Azure.Functions/README.md @@ -47,6 +47,64 @@ var app = builder.Build(); app.Run(); ``` +## Durable Task Scheduler (Durable Functions) + +The Azure Functions hosting library also provides resource APIs for using the Durable Task Scheduler (DTS) with Durable Functions. + +In the _AppHost.cs_ file of `AppHost`, add a Scheduler resource, create one or more Task Hubs, and pass the connection string and hub name to your Functions project: + +```csharp +using Aspire.Hosting; +using Aspire.Hosting.Azure; +using Aspire.Hosting.Azure.Functions; + +var builder = DistributedApplication.CreateBuilder(args); + +var storage = builder.AddAzureStorage("storage").RunAsEmulator(); + +var scheduler = builder.AddDurableTaskScheduler("scheduler") + .RunAsEmulator(); + +var taskHub = scheduler.AddTaskHub("taskhub"); + +builder.AddAzureFunctionsProject("funcapp") + .WithHostStorage(storage) + .WithEnvironment("DURABLE_TASK_SCHEDULER_CONNECTION_STRING", scheduler) + .WithEnvironment("TASKHUB_NAME", taskHub.Resource.TaskHubName); + +builder.Build().Run(); +``` + +### Use the DTS emulator + +`RunAsEmulator()` starts a local container running the Durable Task Scheduler emulator. + +When a Scheduler runs as an emulator, Aspire automatically provides: + +- A "Scheduler Dashboard" URL for the scheduler resource. +- A "Task Hub Dashboard" URL for each Task Hub resource. +- A `DTS_TASK_HUB_NAMES` environment variable on the emulator container listing the Task Hub names associated with that scheduler. + +### Use an existing Scheduler + +If you already have a Scheduler instance, configure the resource using its connection string: + +```csharp +var schedulerConnectionString = builder.AddParameter( + "dts-connection-string", + "Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"); + +var scheduler = builder.AddDurableTaskScheduler("scheduler") + .RunAsExisting(schedulerConnectionString); + +var taskHubName = builder.AddParameter("taskhub-name", "mytaskhub"); +var taskHub = scheduler.AddTaskHub("taskhub").WithTaskHubName(taskHubName); +``` +## Additional documentation + +- https://learn.microsoft.com/azure/azure-functions +- https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler + ## Feedback & contributing https://github.com/microsoft/aspire diff --git a/tests/Aspire.Hosting.Azure.Tests/DurableTaskResourceExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/DurableTaskResourceExtensionsTests.cs new file mode 100644 index 00000000000..71e23e99523 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/DurableTaskResourceExtensionsTests.cs @@ -0,0 +1,256 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Tests.Utils; +using Aspire.Hosting.Utils; + +namespace Aspire.Hosting.Azure.Tests; + +public class DurableTaskResourceExtensionsTests +{ + [Fact] + public async Task AddDurableTaskScheduler_RunAsEmulator_ResolvedConnectionString() + { + string expectedConnectionString = "Endpoint=http://localhost:8080;Authentication=None"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder + .AddDurableTaskScheduler("dts") + .RunAsEmulator(e => + { + e.WithEndpoint("grpc", e => e.AllocatedEndpoint = new(e, "localhost", 8080)); + e.WithEndpoint("http", e => e.AllocatedEndpoint = new(e, "localhost", 8081)); + e.WithEndpoint("dashboard", e => e.AllocatedEndpoint = new(e, "localhost", 8082)); + }); + + var connectionString = await dts.Resource.ConnectionStringExpression.GetValueAsync(default); + + Assert.Equal(expectedConnectionString, connectionString); + } + + [Fact] + public async Task AddDurableTaskScheduler_RunAsExisting_ResolvedConnectionString() + { + string expectedConnectionString = "Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder + .AddDurableTaskScheduler("dts") + .RunAsExisting(expectedConnectionString); + + var connectionString = await dts.Resource.ConnectionStringExpression.GetValueAsync(default); + + Assert.Equal(expectedConnectionString, connectionString); + } + + [Fact] + public async Task AddDurableTaskScheduler_RunAsExisting_ResolvedConnectionStringParameter() + { + string expectedConnectionString = "Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var connectionStringParameter = builder.AddParameter("dts-connection-string", expectedConnectionString); + + var dts = builder + .AddDurableTaskScheduler("dts") + .RunAsExisting(connectionStringParameter); + + var connectionString = await dts.Resource.ConnectionStringExpression.GetValueAsync(default); + + Assert.Equal(expectedConnectionString, connectionString); + } + + [Theory] + [InlineData(null, "mytaskhub")] + [InlineData("myrealtaskhub", "myrealtaskhub")] + public async Task AddDurableTaskHub_RunAsExisting_ResolvedConnectionStringParameter(string? taskHubName, string expectedTaskHubName) + { + string dtsConnectionString = "Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"; + string expectedConnectionString = $"{dtsConnectionString};TaskHub={expectedTaskHubName}"; + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder + .AddDurableTaskScheduler("dts") + .RunAsExisting(dtsConnectionString); + + var taskHub = dts.AddTaskHub("mytaskhub"); + + if (taskHubName is not null) + { + taskHub = taskHub.WithTaskHubName(taskHubName); + } + + var connectionString = await taskHub.Resource.ConnectionStringExpression.GetValueAsync(default); + + Assert.Equal(expectedConnectionString, connectionString); + } + + [Fact] + public void AddDurableTaskScheduler_IsExcludedFromPublishingManifest() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder.AddDurableTaskScheduler("dts"); + + Assert.True(dts.Resource.TryGetAnnotationsOfType(out var manifestAnnotations)); + var annotation = Assert.Single(manifestAnnotations); + Assert.Equal(ManifestPublishingCallbackAnnotation.Ignore, annotation); + } + + [Fact] + public void AddDurableTaskHub_IsExcludedFromPublishingManifest() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder.AddDurableTaskScheduler("dts").RunAsExisting("Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"); + var taskHub = dts.AddTaskHub("hub"); + + Assert.True(taskHub.Resource.TryGetAnnotationsOfType(out var manifestAnnotations)); + var annotation = Assert.Single(manifestAnnotations); + Assert.Equal(ManifestPublishingCallbackAnnotation.Ignore, annotation); + } + + [Fact] + public void RunAsExisting_InPublishMode_DoesNotApplyConnectionStringAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var dts = builder.AddDurableTaskScheduler("dts") + .RunAsExisting("Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"); + + Assert.False(dts.ApplicationBuilder.ExecutionContext.IsRunMode); + Assert.True(dts.ApplicationBuilder.ExecutionContext.IsPublishMode); + + var ex = Assert.Throws(() => _ = dts.Resource.ConnectionStringExpression); + Assert.Contains("Unable to resolve the Durable Task Scheduler connection string", ex.Message); + } + + [Fact] + public void RunAsEmulator_InPublishMode_IsNoOp() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var dts = builder.AddDurableTaskScheduler("dts") + .RunAsEmulator(); + + Assert.False(dts.Resource.IsEmulator); + Assert.DoesNotContain(dts.Resource.Annotations, a => a is EmulatorResourceAnnotation); + + Assert.Throws(() => _ = dts.Resource.ConnectionStringExpression); + } + + [Fact] + public void RunAsEmulator_AddsEmulatorAnnotationContainerImageAndEndpoints() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder.AddDurableTaskScheduler("dts") + .RunAsEmulator(); + + Assert.True(dts.Resource.IsEmulator); + + var emulatorAnnotation = dts.Resource.Annotations.OfType().SingleOrDefault(); + Assert.NotNull(emulatorAnnotation); + + var containerImageAnnotation = dts.Resource.Annotations.OfType().SingleOrDefault(); + Assert.NotNull(containerImageAnnotation); + Assert.Equal("mcr.microsoft.com", containerImageAnnotation.Registry); + Assert.Equal("dts/dts-emulator", containerImageAnnotation.Image); + Assert.Equal("latest", containerImageAnnotation.Tag); + + var endpointAnnotations = dts.Resource.Annotations.OfType().ToList(); + + var grpc = endpointAnnotations.SingleOrDefault(e => e.Name == "grpc"); + Assert.NotNull(grpc); + Assert.Equal(8080, grpc.TargetPort); + + var http = endpointAnnotations.SingleOrDefault(e => e.Name == "http"); + Assert.NotNull(http); + Assert.Equal(8081, http.TargetPort); + Assert.Equal("http", http.UriScheme); + + var dashboard = endpointAnnotations.SingleOrDefault(e => e.Name == "dashboard"); + Assert.NotNull(dashboard); + Assert.Equal(8082, dashboard.TargetPort); + Assert.Equal("http", dashboard.UriScheme); + } + + [Fact] + public async Task RunAsEmulator_SetsSingleDtsTaskHubNamesEnvironmentVariable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder.AddDurableTaskScheduler("dts").RunAsEmulator(); + + _ = dts.AddTaskHub("hub1").WithTaskHubName("realhub1"); + + var env = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dts.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + + Assert.Equal("realhub1", env["DTS_TASK_HUB_NAMES"]); + } + + [Fact] + public async Task RunAsEmulator_SetsMultipleDtsTaskHubNamesEnvironmentVariable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder.AddDurableTaskScheduler("dts").RunAsEmulator(); + + _ = dts.AddTaskHub("hub1"); + _ = dts.AddTaskHub("hub2").WithTaskHubName("realhub2"); + + var env = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dts.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + + Assert.Equal("hub1, realhub2", env["DTS_TASK_HUB_NAMES"]); + } + + [Fact] + public async Task RunAsEmulator_DtsTaskHubNamesOnlyIncludesHubsForSameScheduler() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts1 = builder.AddDurableTaskScheduler("dts1").RunAsEmulator(); + var dts2 = builder.AddDurableTaskScheduler("dts2").RunAsEmulator(); + + _ = dts1.AddTaskHub("hub1"); + _ = dts2.AddTaskHub("hub2"); + + var env1 = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dts1.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + var env2 = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dts2.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + + Assert.Equal("hub1", env1["DTS_TASK_HUB_NAMES"]); + Assert.Equal("hub2", env2["DTS_TASK_HUB_NAMES"]); + } + + [Fact] + public async Task WithTaskHubName_Parameter_ResolvedConnectionString() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + const string dtsConnectionString = "Endpoint=https://existing-scheduler.durabletask.io;Authentication=DefaultAzure"; + var hubNameParameter = builder.AddParameter("hub-name", "parameterHub"); + + var dts = builder.AddDurableTaskScheduler("dts") + .RunAsExisting(dtsConnectionString); + + var hub = dts.AddTaskHub("ignored").WithTaskHubName(hubNameParameter); + + var connectionString = await hub.Resource.ConnectionStringExpression.GetValueAsync(default); + Assert.Equal($"{dtsConnectionString};TaskHub=parameterHub", connectionString); + } + + [Fact] + public void DurableTaskSchedulerResource_WithoutEmulatorOrExistingConnectionString_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dts = builder.AddDurableTaskScheduler("dts"); + + var ex = Assert.Throws(() => _ = dts.Resource.ConnectionStringExpression); + Assert.Contains("Unable to resolve the Durable Task Scheduler connection string", ex.Message); + } +} From efed40de9fa7c3a9bea68051141125773065b38f Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 28 Mar 2026 13:29:12 -0700 Subject: [PATCH 02/11] Fix Durable Task ATS export annotations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTask/DurableTaskResourceExtensions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs index 21b5558d8e9..29be0b72032 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs @@ -12,6 +12,8 @@ namespace Aspire.Hosting; /// public static class DurableTaskResourceExtensions { + private const string AtsExportIgnoreReason = "Durable Task Scheduler resources are not yet exposed through ATS."; + /// /// Adds a Durable Task scheduler resource to the distributed application. /// @@ -25,6 +27,7 @@ public static class DurableTaskResourceExtensions /// var scheduler = builder.AddDurableTaskScheduler("scheduler"); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder AddDurableTaskScheduler(this IDistributedApplicationBuilder builder, string name) { var scheduler = new DurableTaskSchedulerResource(name); @@ -52,6 +55,7 @@ public static IResourceBuilder AddDurableTaskSched /// .RunAsExisting("Endpoint=https://example;...;"); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, string connectionString) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -82,6 +86,7 @@ public static IResourceBuilder RunAsExisting(this /// .RunAsExisting(schedulerConnectionString); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -106,6 +111,7 @@ public static IResourceBuilder RunAsExisting(this /// .RunAsEmulator(); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) { ArgumentNullException.ThrowIfNull(builder); @@ -183,6 +189,7 @@ public static IResourceBuilder RunAsEmulator(this /// .WithTaskHubName("MyTaskHub"); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder AddTaskHub(this IResourceBuilder builder, string name) { var hub = new DurableTaskHubResource(name, builder.Resource); @@ -226,6 +233,7 @@ await notifications.PublishUpdateAsync(r, snapshot => snapshot with /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName("MyTaskHub"); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, string taskHubName) { return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName)); @@ -247,6 +255,7 @@ public static IResourceBuilder WithTaskHubName(this IRes /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName(taskHubName); /// /// + [AspireExportIgnore(Reason = AtsExportIgnoreReason)] public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, IResourceBuilder taskHubName) { return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName.Resource)); From 3f8a48d48a5a8f06763466c17a5e15e35c3f9cae Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 28 Mar 2026 13:34:39 -0700 Subject: [PATCH 03/11] Fix Durable Task ATS exports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTaskResourceExtensions.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs index 29be0b72032..749e315b53b 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs @@ -12,8 +12,6 @@ namespace Aspire.Hosting; /// public static class DurableTaskResourceExtensions { - private const string AtsExportIgnoreReason = "Durable Task Scheduler resources are not yet exposed through ATS."; - /// /// Adds a Durable Task scheduler resource to the distributed application. /// @@ -27,8 +25,8 @@ public static class DurableTaskResourceExtensions /// var scheduler = builder.AddDurableTaskScheduler("scheduler"); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] - public static IResourceBuilder AddDurableTaskScheduler(this IDistributedApplicationBuilder builder, string name) + [AspireExport("addDurableTaskScheduler", Description = "Adds a Durable Task scheduler resource to the distributed application.")] + public static IResourceBuilder AddDurableTaskScheduler(this IDistributedApplicationBuilder builder, [ResourceName] string name) { var scheduler = new DurableTaskSchedulerResource(name); @@ -55,7 +53,7 @@ public static IResourceBuilder AddDurableTaskSched /// .RunAsExisting("Endpoint=https://example;...;"); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] + [AspireExport("runAsExisting", Description = "Configures the Durable Task scheduler to use an existing scheduler instance from a connection string.")] public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, string connectionString) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -86,7 +84,7 @@ public static IResourceBuilder RunAsExisting(this /// .RunAsExisting(schedulerConnectionString); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] + [AspireExport("runAsExistingFromParameter", Description = "Configures the Durable Task scheduler to use an existing scheduler instance from a parameterized connection string.")] public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -111,7 +109,7 @@ public static IResourceBuilder RunAsExisting(this /// .RunAsEmulator(); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] + [AspireExport("runAsEmulator", Description = "Configures the Durable Task scheduler to run using the local emulator.", RunSyncOnBackgroundThread = true)] public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) { ArgumentNullException.ThrowIfNull(builder); @@ -189,8 +187,8 @@ public static IResourceBuilder RunAsEmulator(this /// .WithTaskHubName("MyTaskHub"); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] - public static IResourceBuilder AddTaskHub(this IResourceBuilder builder, string name) + [AspireExport("addTaskHub", Description = "Adds a Durable Task hub resource associated with the scheduler.")] + public static IResourceBuilder AddTaskHub(this IResourceBuilder builder, [ResourceName] string name) { var hub = new DurableTaskHubResource(name, builder.Resource); @@ -233,7 +231,7 @@ await notifications.PublishUpdateAsync(r, snapshot => snapshot with /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName("MyTaskHub"); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] + [AspireExport("withTaskHubName", Description = "Sets the Durable Task hub name.")] public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, string taskHubName) { return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName)); @@ -255,7 +253,7 @@ public static IResourceBuilder WithTaskHubName(this IRes /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName(taskHubName); /// /// - [AspireExportIgnore(Reason = AtsExportIgnoreReason)] + [AspireExport("withTaskHubNameFromParameter", Description = "Sets the Durable Task hub name from a parameter resource.")] public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, IResourceBuilder taskHubName) { return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName.Resource)); From c18d6c0bdef6c0b73c102f7e3e24bbba413d28cb Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 28 Mar 2026 13:43:00 -0700 Subject: [PATCH 04/11] Refine Durable Task ATS overload exports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTaskResourceExtensions.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs index 749e315b53b..e1f1c266530 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs @@ -53,7 +53,7 @@ public static IResourceBuilder AddDurableTaskSched /// .RunAsExisting("Endpoint=https://example;...;"); /// /// - [AspireExport("runAsExisting", Description = "Configures the Durable Task scheduler to use an existing scheduler instance from a connection string.")] + [AspireExportIgnore(Reason = "Polyglot export is via RunAsExistingCore which accepts both string and parameter resource inputs.")] public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, string connectionString) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -84,7 +84,7 @@ public static IResourceBuilder RunAsExisting(this /// .RunAsExisting(schedulerConnectionString); /// /// - [AspireExport("runAsExistingFromParameter", Description = "Configures the Durable Task scheduler to use an existing scheduler instance from a parameterized connection string.")] + [AspireExportIgnore(Reason = "Polyglot export is via RunAsExistingCore which accepts both string and parameter resource inputs.")] public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -95,6 +95,17 @@ public static IResourceBuilder RunAsExisting(this return builder; } + [AspireExport("runAsExisting", Description = "Configures the Durable Task scheduler to use an existing scheduler instance from a connection string or parameter resource.")] + internal static IResourceBuilder RunAsExistingCore( + this IResourceBuilder builder, + [AspireUnion(typeof(string), typeof(IResourceBuilder))] object connectionString) + => connectionString switch + { + string value => builder.RunAsExisting(value), + IResourceBuilder parameter => builder.RunAsExisting(parameter), + _ => throw new ArgumentException($"Unexpected connection string type: {connectionString.GetType().Name}", nameof(connectionString)) + }; + /// /// Configures the Durable Task scheduler to run using the local emulator (only in non-publish modes). /// @@ -231,7 +242,7 @@ await notifications.PublishUpdateAsync(r, snapshot => snapshot with /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName("MyTaskHub"); /// /// - [AspireExport("withTaskHubName", Description = "Sets the Durable Task hub name.")] + [AspireExportIgnore(Reason = "Polyglot export is via WithTaskHubNameCore which accepts both string and parameter resource inputs.")] public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, string taskHubName) { return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName)); @@ -253,9 +264,20 @@ public static IResourceBuilder WithTaskHubName(this IRes /// var hub = scheduler.AddTaskHub("hub").WithTaskHubName(taskHubName); /// /// - [AspireExport("withTaskHubNameFromParameter", Description = "Sets the Durable Task hub name from a parameter resource.")] + [AspireExportIgnore(Reason = "Polyglot export is via WithTaskHubNameCore which accepts both string and parameter resource inputs.")] public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, IResourceBuilder taskHubName) { return builder.WithAnnotation(new DurableTaskHubNameAnnotation(taskHubName.Resource)); } + + [AspireExport("withTaskHubName", Description = "Sets the Durable Task hub name from a string or parameter resource.")] + internal static IResourceBuilder WithTaskHubNameCore( + this IResourceBuilder builder, + [AspireUnion(typeof(string), typeof(IResourceBuilder))] object taskHubName) + => taskHubName switch + { + string value => builder.WithTaskHubName(value), + IResourceBuilder parameter => builder.WithTaskHubName(parameter), + _ => throw new ArgumentException($"Unexpected task hub name type: {taskHubName.GetType().Name}", nameof(taskHubName)) + }; } From ca66006b8b7e57a9f4f7497135bec835228be1a4 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 28 Mar 2026 14:36:50 -0700 Subject: [PATCH 05/11] Export Durable Task hub resource Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTask/DurableTaskHubResource.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs index 2e57c156407..145d4cd6d9b 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -12,6 +12,7 @@ namespace Aspire.Hosting.Azure.DurableTask; /// /// The logical name of the Task Hub (used as the TaskHub value). /// The durable task scheduler resource whose connection string is the base for this hub. +[AspireExport] public sealed class DurableTaskHubResource(string name, DurableTaskSchedulerResource scheduler) : Resource(name), IResourceWithConnectionString, IResourceWithParent { From ee7e4e571def584877e08c3e21ba5b960c633718 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 28 Mar 2026 14:39:05 -0700 Subject: [PATCH 06/11] Expose Durable Task hub properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTask/DurableTaskHubResource.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs index 145d4cd6d9b..ff6ea63b156 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -12,18 +12,22 @@ namespace Aspire.Hosting.Azure.DurableTask; /// /// The logical name of the Task Hub (used as the TaskHub value). /// The durable task scheduler resource whose connection string is the base for this hub. -[AspireExport] +[AspireExport(ExposeProperties = true)] public sealed class DurableTaskHubResource(string name, DurableTaskSchedulerResource scheduler) : Resource(name), IResourceWithConnectionString, IResourceWithParent { /// /// Gets the connection string expression composed of the scheduler connection string and the TaskHub name. /// + /// This property is not available in polyglot app hosts. + [AspireExportIgnore] public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{Parent.ConnectionStringExpression};TaskHub={TaskHubName}"); /// /// Gets the parent durable task scheduler resource that provides the base connection string. /// + /// This property is not available in polyglot app hosts. + [AspireExportIgnore] public DurableTaskSchedulerResource Parent => scheduler; /// From 29bdd705237950c927e0af2d98d035a4377924a8 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sun, 29 Mar 2026 23:10:19 -0700 Subject: [PATCH 07/11] Address PR review feedback - Set Transport = "http2" on gRPC endpoint per repo convention - Replace NotImplementedException with InvalidOperationException - Use endpoint scheme expression instead of hardcoded "http://" - Add missing trailing newline in MyOrchestrationTrigger.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MyOrchestrationTrigger.cs | 2 +- .../DurableTask/DurableTaskResourceExtensions.cs | 1 + .../DurableTask/DurableTaskSchedulerResource.cs | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs index b0c9f43129d..18a9be7d909 100644 --- a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.Functions/MyOrchestrationTrigger.cs @@ -26,4 +26,4 @@ public static string SayHello([ActivityTrigger] string name, FunctionContext exe { return $"Hello {name}!"; } -} \ No newline at end of file +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs index e1f1c266530..31cfe51db67 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs @@ -134,6 +134,7 @@ public static IResourceBuilder RunAsEmulator(this builder.WithAnnotation(new EmulatorResourceAnnotation()); builder.WithEndpoint(name: "grpc", targetPort: 8080) + .WithEndpoint("grpc", endpoint => endpoint.Transport = "http2") .WithHttpEndpoint(name: "http", targetPort: 8081) .WithHttpEndpoint(name: "dashboard", targetPort: 8082) .WithUrlForEndpoint("dashboard", c => c.DisplayText = "Scheduler Dashboard") diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs index 49b62424fdf..2dd468690ac 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs @@ -31,7 +31,7 @@ private ReferenceExpression CreateConnectionString() { var grpcEndpoint = new EndpointReference(this, "grpc"); - return ReferenceExpression.Create($"Endpoint=http://{grpcEndpoint.Property(EndpointProperty.Host)}:{grpcEndpoint.Property(EndpointProperty.Port)};Authentication=None"); + return ReferenceExpression.Create($"Endpoint={grpcEndpoint.Property(EndpointProperty.Scheme)}://{grpcEndpoint.Property(EndpointProperty.Host)}:{grpcEndpoint.Property(EndpointProperty.Port)};Authentication=None"); } if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) @@ -53,9 +53,9 @@ private ReferenceExpression CreateDashboardEndpoint() { var dashboardEndpoint = new EndpointReference(this, "dashboard"); - return ReferenceExpression.Create($"http://{dashboardEndpoint.Property(EndpointProperty.Host)}:{dashboardEndpoint.Property(EndpointProperty.Port)}"); + return ReferenceExpression.Create($"{dashboardEndpoint.Property(EndpointProperty.Scheme)}://{dashboardEndpoint.Property(EndpointProperty.Host)}:{dashboardEndpoint.Property(EndpointProperty.Port)}"); } - throw new NotImplementedException(); + throw new InvalidOperationException("Dashboard endpoint is only available when running as an emulator."); } } From d2817538082280943a4e3d4036310377f636349e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:57:52 +0000 Subject: [PATCH 08/11] Fix grpc endpoint URI scheme: use WithHttpEndpoint for http2 transport Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/72c23597-975c-4688-8bc8-cdb15823aca8 Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../DurableTask/DurableTaskResourceExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs index 31cfe51db67..b16df76e6a0 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskResourceExtensions.cs @@ -133,7 +133,7 @@ public static IResourceBuilder RunAsEmulator(this // Mark this resource as an emulator for consistent resource identification and tooling support builder.WithAnnotation(new EmulatorResourceAnnotation()); - builder.WithEndpoint(name: "grpc", targetPort: 8080) + builder.WithHttpEndpoint(name: "grpc", targetPort: 8080) .WithEndpoint("grpc", endpoint => endpoint.Transport = "http2") .WithHttpEndpoint(name: "http", targetPort: 8081) .WithHttpEndpoint(name: "dashboard", targetPort: 8082) From ad321246fa26799e8924239bbae20dfa80960c46 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 30 Mar 2026 07:23:59 -0700 Subject: [PATCH 09/11] Remove AspireExportIgnore from DurableTaskHubResource properties Include ConnectionStringExpression and Parent in ATS exports per review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTask/DurableTaskHubResource.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs index ff6ea63b156..ca0670764bb 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -19,15 +19,11 @@ public sealed class DurableTaskHubResource(string name, DurableTaskSchedulerReso /// /// Gets the connection string expression composed of the scheduler connection string and the TaskHub name. /// - /// This property is not available in polyglot app hosts. - [AspireExportIgnore] public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{Parent.ConnectionStringExpression};TaskHub={TaskHubName}"); /// /// Gets the parent durable task scheduler resource that provides the base connection string. /// - /// This property is not available in polyglot app hosts. - [AspireExportIgnore] public DurableTaskSchedulerResource Parent => scheduler; /// From 9ccce92cdfdbe707772a82031826d33ba6010f52 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 30 Mar 2026 09:28:25 -0700 Subject: [PATCH 10/11] Use EndpointProperty.Url for dashboard endpoint expression Simplifies CreateDashboardEndpoint by using EndpointProperty.Url instead of manually composing scheme://host:port. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DurableTask/DurableTaskSchedulerResource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs index 2dd468690ac..ab099d63795 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs @@ -53,7 +53,7 @@ private ReferenceExpression CreateDashboardEndpoint() { var dashboardEndpoint = new EndpointReference(this, "dashboard"); - return ReferenceExpression.Create($"{dashboardEndpoint.Property(EndpointProperty.Scheme)}://{dashboardEndpoint.Property(EndpointProperty.Host)}:{dashboardEndpoint.Property(EndpointProperty.Port)}"); + return ReferenceExpression.Create($"{dashboardEndpoint.Property(EndpointProperty.Url)}"); } throw new InvalidOperationException("Dashboard endpoint is only available when running as an emulator."); From 67d7147b30306a78f329da7772b17fb668940d41 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 30 Mar 2026 09:56:46 -0700 Subject: [PATCH 11/11] Implement IResourceWithAzureFunctionsConfig for DurableTaskHubResource DurableTaskHubResource now implements IResourceWithAzureFunctionsConfig, enabling .WithReference(taskHub) on Azure Functions projects. This replaces manual .WithEnvironment() calls for the scheduler connection string and task hub name. Updated playground and README to use the new pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureFunctionsWithDts.AppHost/Program.cs | 3 +-- .../DurableTask/DurableTaskHubResource.cs | 10 +++++++++- src/Aspire.Hosting.Azure.Functions/README.md | 3 +-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs index 44e7d42bc99..ff0e39fb7be 100644 --- a/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs +++ b/playground/AzureFunctionsWithDts/AzureFunctionsWithDts.AppHost/Program.cs @@ -8,7 +8,6 @@ builder.AddAzureFunctionsProject("funcapp") .WithHostStorage(storage) - .WithEnvironment("DURABLE_TASK_SCHEDULER_CONNECTION_STRING", scheduler) - .WithEnvironment("TASKHUB_NAME", taskHub.Resource.TaskHubName); + .WithReference(taskHub); builder.Build().Run(); diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs index ca0670764bb..92440c6707e 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -14,7 +14,7 @@ namespace Aspire.Hosting.Azure.DurableTask; /// The durable task scheduler resource whose connection string is the base for this hub. [AspireExport(ExposeProperties = true)] public sealed class DurableTaskHubResource(string name, DurableTaskSchedulerResource scheduler) - : Resource(name), IResourceWithConnectionString, IResourceWithParent + : Resource(name), IResourceWithConnectionString, IResourceWithParent, IResourceWithAzureFunctionsConfig { /// /// Gets the connection string expression composed of the scheduler connection string and the TaskHub name. @@ -31,6 +31,14 @@ public sealed class DurableTaskHubResource(string name, DurableTaskSchedulerReso /// public ReferenceExpression TaskHubName => GetTaskHubName(); + /// + void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName) + { + // Injected to support Azure Functions listener initialization via the DTS storage provider. + target["DURABLE_TASK_SCHEDULER_CONNECTION_STRING"] = Parent.ConnectionStringExpression; + target["TASKHUB_NAME"] = TaskHubName; + } + private ReferenceExpression GetTaskHubName() { if (this.TryGetLastAnnotation(out var taskHubNameAnnotation)) diff --git a/src/Aspire.Hosting.Azure.Functions/README.md b/src/Aspire.Hosting.Azure.Functions/README.md index bcfe22d4820..86ef7ff7cec 100644 --- a/src/Aspire.Hosting.Azure.Functions/README.md +++ b/src/Aspire.Hosting.Azure.Functions/README.md @@ -69,8 +69,7 @@ var taskHub = scheduler.AddTaskHub("taskhub"); builder.AddAzureFunctionsProject("funcapp") .WithHostStorage(storage) - .WithEnvironment("DURABLE_TASK_SCHEDULER_CONNECTION_STRING", scheduler) - .WithEnvironment("TASKHUB_NAME", taskHub.Resource.TaskHubName); + .WithReference(taskHub); builder.Build().Run(); ```