diff --git a/Aspire.slnx b/Aspire.slnx
index 243909d5080..3d5e9ae5f0e 100644
--- a/Aspire.slnx
+++ b/Aspire.slnx
@@ -153,6 +153,12 @@
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 935bda3f634..6eb496bfdc8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -88,6 +88,8 @@
+
+
diff --git a/playground/DurableTask/DurableTask.Scheduler.AppHost/DurableTask.Scheduler.AppHost.csproj b/playground/DurableTask/DurableTask.Scheduler.AppHost/DurableTask.Scheduler.AppHost.csproj
new file mode 100644
index 00000000000..cc1a737c1e6
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.AppHost/DurableTask.Scheduler.AppHost.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ $(DefaultTargetFramework)
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/DurableTask/DurableTask.Scheduler.AppHost/Program.cs b/playground/DurableTask/DurableTask.Scheduler.AppHost/Program.cs
new file mode 100644
index 00000000000..494e4355b41
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.AppHost/Program.cs
@@ -0,0 +1,23 @@
+using Aspire.Hosting.Azure;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var scheduler =
+ builder.AddDurableTaskScheduler("scheduler")
+ .RunAsEmulator(
+ options =>
+ {
+ options.WithDynamicTaskHubs();
+ });
+
+var taskHub = scheduler.AddTaskHub("taskhub");
+
+var webApi =
+ builder.AddProject("webapi")
+ .WithReference(taskHub);
+
+builder.AddProject("worker")
+ .WithReference(webApi)
+ .WithReference(taskHub);
+
+builder.Build().Run();
diff --git a/playground/DurableTask/DurableTask.Scheduler.AppHost/Properties/launchSettings.json b/playground/DurableTask/DurableTask.Scheduler.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..ec446d195a0
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17222;http://localhost:15079",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21093",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22284"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15079",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19250",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20227"
+ }
+ }
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.AppHost/appsettings.json b/playground/DurableTask/DurableTask.Scheduler.AppHost/appsettings.json
new file mode 100644
index 00000000000..31c092aa450
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/DurableTask.Scheduler.ExternalAppHost.csproj b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/DurableTask.Scheduler.ExternalAppHost.csproj
new file mode 100644
index 00000000000..cc1a737c1e6
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/DurableTask.Scheduler.ExternalAppHost.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ $(DefaultTargetFramework)
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Program.cs b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Program.cs
new file mode 100644
index 00000000000..679b8316e8b
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Program.cs
@@ -0,0 +1,21 @@
+using Aspire.Hosting.Azure;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var scheduler =
+ builder.AddDurableTaskScheduler("scheduler")
+ .RunAsExisting(builder.AddParameter("scheduler-connection-string"));
+
+var taskHub =
+ scheduler.AddTaskHub("taskhub")
+ .WithTaskHubName(builder.AddParameter("taskhub-name"));
+
+var webApi =
+ builder.AddProject("webapi")
+ .WithReference(taskHub);
+
+builder.AddProject("worker")
+ .WithReference(webApi)
+ .WithReference(taskHub);
+
+builder.Build().Run();
diff --git a/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Properties/launchSettings.json b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..ec446d195a0
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17222;http://localhost:15079",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21093",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22284"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15079",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19250",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20227"
+ }
+ }
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/appsettings.json b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/appsettings.json
new file mode 100644
index 00000000000..4a65f26cfac
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ },
+ "Parameters": {
+ "scheduler-connection-string": "",
+ "taskhub-name": ""
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.csproj b/playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.csproj
new file mode 100644
index 00000000000..1007026550f
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(DefaultTargetFramework)
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.http b/playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.http
new file mode 100644
index 00000000000..669830c2c64
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.http
@@ -0,0 +1,11 @@
+@HostAddress = http://localhost:5142
+
+POST {{HostAddress}}/create
+Content-Type: application/json
+
+{
+ "text": "hello world"
+}
+
+###
+
diff --git a/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs b/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs
new file mode 100644
index 00000000000..92ff9cf8328
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs
@@ -0,0 +1,47 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Client.AzureManaged;
+using System.Text.Json.Serialization;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.AddDurableTaskClient(
+ clientBuilder =>
+ {
+ clientBuilder.UseDurableTaskScheduler(
+ builder.Configuration.GetConnectionString("taskhub") ?? throw new InvalidOperationException("Scheduler connection string not configured."),
+ options =>
+ {
+ options.AllowInsecureCredentials = true;
+ });
+ });
+
+var app = builder.Build();
+
+app.MapPost("/create", async (EchoValue value, DurableTaskClient durableTaskClient) =>
+ {
+ string instanceId = await durableTaskClient.ScheduleNewOrchestrationInstanceAsync(
+ "Echo",
+ value);
+
+ await durableTaskClient.WaitForInstanceCompletionAsync(instanceId);
+
+ return Results.Ok();
+ })
+ .WithName("CreateOrchestration");
+
+app.MapPost("/echo", ([FromBody] EchoValue value) =>
+ {
+ return new EchoValue { Text = $"Echoed: {value.Text}" };
+ })
+ .WithName("EchoText");
+
+app.Run();
+
+public record EchoValue
+{
+ [JsonPropertyName("text")]
+ public required string Text { get; init; }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.WebApi/Properties/launchSettings.json b/playground/DurableTask/DurableTask.Scheduler.WebApi/Properties/launchSettings.json
new file mode 100644
index 00000000000..5f5caff3a97
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.WebApi/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5142",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.WebApi/appsettings.json b/playground/DurableTask/DurableTask.Scheduler.WebApi/appsettings.json
new file mode 100644
index 00000000000..10f68b8c8b4
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.WebApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/DurableTask.Scheduler.Worker.csproj b/playground/DurableTask/DurableTask.Scheduler.Worker/DurableTask.Scheduler.Worker.csproj
new file mode 100644
index 00000000000..7b066f9480c
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/DurableTask.Scheduler.Worker.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(DefaultTargetFramework)
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs
new file mode 100644
index 00000000000..aad6f3b3cd5
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs
@@ -0,0 +1,35 @@
+using DurableTask.Scheduler.Worker.Tasks.Echo;
+using Microsoft.DurableTask.Worker;
+using Microsoft.DurableTask.Worker.AzureManaged;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.AddServiceDiscovery();
+
+builder
+ .Services
+ .AddHttpClient("Echo",
+ client => client.BaseAddress = new Uri("https+http://webapi"))
+ .AddServiceDiscovery();
+
+builder.Services.AddDurableTaskWorker(
+ workerBuilder =>
+ {
+ workerBuilder.AddTasks(r =>
+ {
+ r.AddActivity("EchoActivity");
+ r.AddOrchestrator("Echo");
+ });
+ workerBuilder.UseDurableTaskScheduler(
+ builder.Configuration.GetConnectionString("taskhub") ?? throw new InvalidOperationException("Scheduler connection string not configured."),
+ options =>
+ {
+ options.AllowInsecureCredentials = true;
+ });
+ });
+
+var host = builder.Build();
+
+host.Run();
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Properties/launchSettings.json b/playground/DurableTask/DurableTask.Scheduler.Worker/Properties/launchSettings.json
new file mode 100644
index 00000000000..a2a6bf247e8
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "CommunityToolkit.Aspire.Hosting.DurableTask.Scheduler.Worker": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs
new file mode 100644
index 00000000000..65f7d7ed2f8
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs
@@ -0,0 +1,18 @@
+using System.Net.Http.Json;
+using Microsoft.DurableTask;
+
+namespace DurableTask.Scheduler.Worker.Tasks.Echo;
+
+sealed class EchoActivity(IHttpClientFactory clientFactory) : TaskActivity
+{
+ public override async Task RunAsync(TaskActivityContext context, string input)
+ {
+ HttpClient client = clientFactory.CreateClient("Echo");
+
+ var result = await client.PostAsync("/echo", JsonContent.Create(new EchoInput { Text = input }));
+
+ var output = await result.Content.ReadFromJsonAsync();
+
+ return output?.Text ?? throw new InvalidOperationException("Invalid response from echo service!");
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoInput.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoInput.cs
new file mode 100644
index 00000000000..867a1bb87f1
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoInput.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace DurableTask.Scheduler.Worker.Tasks.Echo;
+
+sealed record EchoInput
+{
+ [JsonPropertyName("text")]
+ public required string Text { get; init; }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs
new file mode 100644
index 00000000000..7b5b1c11fe2
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs
@@ -0,0 +1,17 @@
+using Microsoft.DurableTask;
+
+namespace DurableTask.Scheduler.Worker.Tasks.Echo;
+
+sealed class EchoOrchestrator : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, EchoInput input)
+ {
+ string output = await context.CallActivityAsync("EchoActivity", input.Text);
+
+ output = await context.CallActivityAsync("EchoActivity", output);
+
+ output = await context.CallActivityAsync("EchoActivity", output);
+
+ return output;
+ }
+}
diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/appsettings.json b/playground/DurableTask/DurableTask.Scheduler.Worker/appsettings.json
new file mode 100644
index 00000000000..b2dcdb67421
--- /dev/null
+++ b/playground/DurableTask/DurableTask.Scheduler.Worker/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs
new file mode 100644
index 00000000000..a603081a7cc
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs
@@ -0,0 +1,41 @@
+// 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;
+
+internal static class DurableTaskConstants
+{
+ public static class Scheduler
+ {
+ public static class Dashboard
+ {
+ public static readonly Uri Endpoint = new Uri("https://dashboard.durabletask.io");
+ }
+
+ public static class Emulator
+ {
+ public static class Container
+ {
+ /// mcr.microsoft.com/dts
+ public const string Registry = "mcr.microsoft.com/dts";
+
+ /// dts-emulator
+ public const string Image = "dts-emulator";
+
+ /// latest
+ public static string Tag => "latest";
+ }
+
+ public static class Endpoints
+ {
+ public const string Worker = "worker";
+ public const string Dashboard = "dashboard";
+ }
+ }
+
+ public static class TaskHub
+ {
+ public const string DefaultName = "default";
+ }
+ }
+}
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..7a89fdc4cd4
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs
@@ -0,0 +1,74 @@
+// 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;
+
+///
+/// Represents an indivdual task hub of a Durable Task Scheduler.
+///
+/// The name of the task hub resource.
+/// The scheduler to which the task hub belongs.
+public class DurableTaskHubResource(string name, DurableTaskSchedulerResource parent)
+ : Resource(name), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithParent, IDurableTaskResourceWithDashboard
+{
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"{this.Parent.ConnectionStringExpression};TaskHub={this.ResolveTaskHubName()}");
+
+ ///
+ public DurableTaskSchedulerResource Parent => parent;
+
+ ///
+ /// Gets or sets the name of the task hub (if different from the resource name).
+ ///
+ public string? TaskHubName { get; set; }
+
+ ReferenceExpression IDurableTaskResourceWithDashboard.DashboardEndpointExpression =>
+ this.GetDashboardEndpoint();
+
+ internal ReferenceExpression TaskHubNameExpression =>
+ ReferenceExpression.Create($"{this.ResolveTaskHubName()}");
+
+ ReferenceExpression GetDashboardEndpoint()
+ {
+ var defaultValue = ReferenceExpression.Create($"default");
+
+ ReferenceExpressionBuilder builder = new();
+
+ builder.Append($"{this.ResolveDashboardEndpoint()}subscriptions/{this.ResolveSubscriptionId() ?? defaultValue}/schedulers/{this.Parent.SchedulerNameExpression}/taskhubs/{this.ResolveTaskHubName()}");
+
+ if (!this.Parent.IsEmulator)
+ {
+ // NOTE: The endpoint is expected to have the trailing slash.
+ builder.Append($"?endpoint={QueryParameterReference.Create(this.Parent.DashboardSchedulerEndpointExpression)}");
+ }
+
+ return builder.Build();
+ }
+
+ string ResolveTaskHubName() => this.TaskHubName ?? this.Name;
+
+ ReferenceExpression ResolveDashboardEndpoint()
+ {
+ if (this.TryGetLastAnnotation(out var annotation)
+ && annotation.DashboardEndpoint is not null)
+ {
+ return annotation.DashboardEndpoint;
+ }
+
+ return (this.Parent as IDurableTaskResourceWithDashboard).DashboardEndpointExpression;
+ }
+
+ ReferenceExpression? ResolveSubscriptionId()
+ {
+ if (this.TryGetLastAnnotation(out var annotation)
+ && annotation.SubscriptionId is not null)
+ {
+ return annotation.SubscriptionId;
+ }
+
+ return this.Parent.SubscriptionIdExpression;
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerAuthentication.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerAuthentication.cs
new file mode 100644
index 00000000000..a399221636f
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerAuthentication.cs
@@ -0,0 +1,28 @@
+// 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;
+
+///
+/// Represents the authentication methods supported by the Durable Task Scheduler.
+///
+public static class DurableTaskSchedulerAuthentication
+{
+ ///
+ /// No authentication is used with the scheduler.
+ ///
+ ///
+ /// This is suitable only for local, emulator-based development.
+ ///
+ public const string None = "None";
+
+ ///
+ /// Use the developer's Azure account to authenticate with the scheduler.
+ ///
+ public const string Default = "DefaultAzure";
+
+ ///
+ /// Use managed identity to authenticate with the scheduler.
+ ///
+ public const string ManagedIdentity = "ManagedIdentity";
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs
new file mode 100644
index 00000000000..d4b18be2aa6
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs
@@ -0,0 +1,14 @@
+// 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;
+
+internal sealed class DurableTaskSchedulerDashboardAnnotation(ReferenceExpression? subscriptionId, ReferenceExpression? dashboardEndpoint)
+ : IResourceAnnotation
+{
+ public ReferenceExpression? DashboardEndpoint => dashboardEndpoint;
+
+ public ReferenceExpression? SubscriptionId => subscriptionId;
+}
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..710bbdd0ef0
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorResource.cs
@@ -0,0 +1,29 @@
+// 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;
+
+///
+/// Wraps an in a type that exposes container extension methods.
+///
+/// The inner resource used to store annotations.
+public sealed class DurableTaskSchedulerEmulatorResource(DurableTaskSchedulerResource innerResource)
+ : ContainerResource(innerResource.Name), IResource
+{
+ ///
+ public override ResourceAnnotationCollection Annotations => innerResource.Annotations;
+
+ ///
+ public override string Name => innerResource.Name;
+
+ ///
+ /// Gets or sets whether the emulator should use dynamic task hubs.
+ ///
+ ///
+ /// Using dynamic task hubs eliminates the requirement that they be pre-defined,
+ /// which can be useful when the same emulator instance is used across sessions.
+ ///
+ public bool UseDynamicTaskHubs { get; set; }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs
new file mode 100644
index 00000000000..c9461c25d85
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs
@@ -0,0 +1,441 @@
+// 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 System.Diagnostics;
+
+namespace Aspire.Hosting.Azure;
+
+///
+/// Provides extension methods for adding and configuring Durable Task Scheduler resources to the application model.
+///
+public static class DurableTaskSchedulerExtensions
+{
+ ///
+ /// Adds a Durable Task Scheduler resource to the application model.
+ ///
+ /// The builder for the distributed application.
+ /// The name of the resource.
+ /// (Optional) Callback that exposes the resource allowing for customization.
+ /// A reference to the .
+ public static IResourceBuilder AddDurableTaskScheduler(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action>? configure = null)
+ {
+ DurableTaskSchedulerResource resource = new(name);
+
+ var resourceBuilder = builder.AddResource(resource);
+
+ configure?.Invoke(resourceBuilder);
+
+ return resourceBuilder;
+ }
+
+ ///
+ /// Configures a Durable Task Scheduler resource to be emulated.
+ ///
+ /// The Durable Task Scheduler resource builder.
+ /// Callback that exposes the underlying container used for emulation allowing for customization.
+ /// A reference to the .
+ ///
+ /// This version of the package defaults to the tag of the container image in the registry.
+ ///
+ ///
+ /// The following example creates a Durable Task Scheduler resource that runs locally in an emulator and referencing that resource in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var scheduler = builder.AddDurableTaskScheduler("scheduler")
+ /// .RunAsEmulator();
+ ///
+ /// var taskHub = scheduler.AddDurableTaskHub("taskhub");
+ ///
+ /// builder.AddProject<Projects.MyApp>("myapp")
+ /// .WithReference(taskHub);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null)
+ {
+ if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ return builder;
+ }
+
+ builder
+ .WithDashboard();
+
+ //
+ // Add dashboards for existing task hubs (not already marked to have a dashboard annotation).
+ //
+
+ var existingTaskHubs =
+ builder
+ .ApplicationBuilder
+ .Resources
+ .OfType()
+ .Where(taskHub => taskHub.Parent == builder.Resource)
+ .Where(taskHub => !taskHub.HasAnnotationOfType());
+
+ foreach (var taskHub in existingTaskHubs)
+ {
+ builder.ApplicationBuilder.CreateResourceBuilder(taskHub).WithDashboard();
+ }
+
+ var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(new DurableTaskSchedulerEmulatorResource(builder.Resource))
+ .WithHttpEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker, targetPort: 8080)
+ .WithHttpEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard, targetPort: 8082)
+ .WithEnvironment(
+ async (EnvironmentCallbackContext context) =>
+ {
+ var nameTasks =
+ builder
+ .ApplicationBuilder
+ .Resources
+ .OfType()
+ .Where(r => r.Parent == builder.Resource)
+ .Select(r => r.TaskHubNameExpression)
+ .Select(async r => await r.GetValueAsync(context.CancellationToken).ConfigureAwait(false))
+ .ToList();
+
+ await Task.WhenAll(nameTasks).ConfigureAwait(false);
+
+ var taskHubNames = nameTasks
+ .Select(r => r.Result)
+ .Where(r => r is not null)
+ .Distinct()
+ .ToList();
+
+ if (taskHubNames.Any())
+ {
+ context.EnvironmentVariables.Add("DTS_TASK_HUB_NAMES", String.Join(",", taskHubNames));
+ }
+ })
+ .WithAnnotation(
+ new ContainerImageAnnotation
+ {
+ Registry = DurableTaskConstants.Scheduler.Emulator.Container.Registry,
+ Image = DurableTaskConstants.Scheduler.Emulator.Container.Image,
+ Tag = DurableTaskConstants.Scheduler.Emulator.Container.Tag
+ });
+
+ configureContainer?.Invoke(surrogateBuilder);
+
+ if (surrogateBuilder.Resource.UseDynamicTaskHubs)
+ {
+ surrogateBuilder.WithEnvironment(
+ (EnvironmentCallbackContext context) =>
+ {
+ context.EnvironmentVariables.Add("DTS_USE_DYNAMIC_TASK_HUBS", "true");
+ });
+ }
+
+ builder.Resource.Authentication ??= DurableTaskSchedulerAuthentication.None;
+
+ return builder;
+ }
+
+ ///
+ /// Marks the resource as an existing Durable Task Scheduler instance when the application is running.
+ ///
+ /// The Durable Task Scheduler resource builder.
+ /// The connection string to the existing Durable Task Scheduler instance.
+ /// A reference to the .
+ ///
+ /// The following example creates a preexisting Durable Task Scheduler resource configured via external parameters and referencing that resource in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var scheduler = builder.AddDurableTaskScheduler("scheduler")
+ /// .RunAsExisting(builder.AddParameter("scheduler-connection-string"));
+ ///
+ /// var taskHub =
+ /// scheduler.AddDurableTaskHub("taskhub")
+ /// .WithTaskHubName(builder.AddParameter("taskhub-name"));
+ ///
+ /// builder.AddProject<Projects.MyApp>("myapp")
+ /// .WithReference(taskHub);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString)
+ {
+ return builder.RunAsExisting(ReferenceExpression.Create($"{connectionString}"));
+ }
+
+ ///
+ /// Marks the resource as an existing Durable Task Scheduler instance when the application is running.
+ ///
+ /// The Durable Task Scheduler resource builder.
+ /// The connection string to the existing Durable Task Scheduler instance.
+ /// A reference to the .
+ ///
+ /// The following example creates a preexisting Durable Task Scheduler resource configured via strings and referencing that resource in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// string connectionString = "...";
+ ///
+ /// var scheduler = builder.AddDurableTaskScheduler("scheduler")
+ /// .RunAsExisting(connectionString);
+ ///
+ /// string taskHubName = "...";
+ ///
+ /// var taskHub =
+ /// scheduler.AddDurableTaskHub("taskhub")
+ /// .WithTaskHubName(taskHubName);
+ ///
+ /// builder.AddProject<Projects.MyApp>("myapp")
+ /// .WithReference(taskHub);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, string connectionString)
+ {
+ return builder.RunAsExisting(ReferenceExpression.Create($"{connectionString}"));
+ }
+
+ ///
+ /// Marks the resource as an existing Durable Task Scheduler instance when the application is running.
+ ///
+ /// The Durable Task Scheduler resource builder.
+ /// The connection string to the existing Durable Task Scheduler instance.
+ /// A reference to the .
+ ///
+ /// The following example creates a preexisting Durable Task Scheduler resource configured via strings and referencing that resource in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var connectionString = ReferenceExpression.Create($"...");
+ ///
+ /// var scheduler = builder.AddDurableTaskScheduler("scheduler")
+ /// .RunAsExisting(connectionString);
+ ///
+ /// string taskHubName = "...";
+ ///
+ /// var taskHub =
+ /// scheduler.AddDurableTaskHub("taskhub")
+ /// .WithTaskHubName(taskHubName);
+ ///
+ /// builder.AddProject<Projects.MyApp>("myapp")
+ /// .WithReference(taskHub);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, ReferenceExpression connectionString)
+ {
+ if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ builder.WithAnnotation(new ExistingDurableTaskSchedulerAnnotation(connectionString));
+
+ var connectionStringParameters = ParseConnectionString(connectionString.ValueExpression);
+
+ if (connectionStringParameters.TryGetValue("Endpoint", out string? endpoint))
+ {
+ builder.Resource.SchedulerEndpoint ??= new Uri(endpoint);
+ }
+
+ if (connectionStringParameters.TryGetValue("Authentication", out string? authentication))
+ {
+ builder.Resource.Authentication ??= authentication;
+ }
+ }
+
+ return builder;
+ }
+
+ static IReadOnlyDictionary ParseConnectionString(string connectionString)
+ {
+ Dictionary dictionary = new(StringComparer.OrdinalIgnoreCase);
+
+ var parameters = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ foreach (var parameter in parameters)
+ {
+ var keyValue = parameter.Split('=', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (keyValue.Length != 2)
+ {
+ throw new ArgumentException($"Invalid connection string format: {parameter}");
+ }
+
+ dictionary[keyValue[0]] = keyValue[1];
+ }
+
+ return dictionary;
+ }
+
+ ///
+ /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard in a web browser.
+ ///
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDashboard(this IResourceBuilder builder, string dashboardEndpoint)
+ {
+ return builder.WithDashboard(ReferenceExpression.Create($"{dashboardEndpoint}"));
+ }
+
+ ///
+ /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard in a web browser.
+ ///
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDashboard(this IResourceBuilder builder, IResourceBuilder? dashboardEndpoint = null)
+ {
+ return builder.WithDashboard(dashboardEndpoint is not null ? ReferenceExpression.Create($"{dashboardEndpoint}") : null);
+ }
+
+ static IResourceBuilder WithDashboard(this IResourceBuilder builder, ReferenceExpression? dashboardEndpoint)
+ {
+ if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ builder.WithAnnotation(new DurableTaskSchedulerDashboardAnnotation(
+ subscriptionId: null,
+ dashboardEndpoint: dashboardEndpoint));
+
+ builder.WithOpenDashboardCommand(isTaskHub: false);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Adds a Durable Task Scheduler task hub resource to the application model.
+ ///
+ /// The Durable Task Scheduler resource builder.
+ /// The name of the resource.
+ /// (Optional) Callback that exposes the resource allowing for customization.
+ /// A reference to the .
+ public static IResourceBuilder AddTaskHub(this IResourceBuilder builder, [ResourceName] string name, Action>? configure = null)
+ {
+ DurableTaskHubResource taskHubResource = new(name, builder.Resource);
+
+ var taskHubResourceBuilder = builder.ApplicationBuilder.AddResource(taskHubResource);
+
+ configure?.Invoke(taskHubResourceBuilder);
+
+ if (builder.Resource.IsEmulator)
+ {
+ taskHubResourceBuilder.WithDashboard();
+ }
+
+ return taskHubResourceBuilder;
+ }
+
+ ///
+ /// Sets the name of the task hub if different from the resource name.
+ ///
+ /// The Durable Task Scheduler task hub resource builder.
+ /// The name of the task hub.
+ /// A reference to the .
+ public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, string name)
+ {
+ builder.Resource.TaskHubName = name;
+
+ return builder;
+ }
+
+ ///
+ /// Sets the name of the task hub if different from the resource name.
+ ///
+ /// The Durable Task Scheduler task hub resource builder.
+ /// The name of the task hub.
+ /// A reference to the .
+ public static IResourceBuilder WithTaskHubName(this IResourceBuilder builder, IResourceBuilder name)
+ {
+ builder.Resource.TaskHubName = name.Resource.Value;
+
+ return builder;
+ }
+
+ ///
+ /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard for the task hub in a web browser.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDashboard(this IResourceBuilder builder, string? dashboardEndpoint, string? subscriptionId)
+ {
+ return builder.WithDashboard(
+ dashboardEndpoint: dashboardEndpoint is not null ? ReferenceExpression.Create($"{dashboardEndpoint}") : null,
+ subscriptionId: subscriptionId is not null ? ReferenceExpression.Create($"{subscriptionId}") : null);
+ }
+
+ ///
+ /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard for the task hub in a web browser.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDashboard(this IResourceBuilder builder, IResourceBuilder? dashboardEndpoint = null, IResourceBuilder? subscriptionId = null)
+ {
+ return builder.WithDashboard(
+ dashboardEndpoint: dashboardEndpoint is not null ? ReferenceExpression.Create($"{dashboardEndpoint}") : null,
+ subscriptionId: subscriptionId is not null ? ReferenceExpression.Create($"{subscriptionId}") : null);
+ }
+
+ static IResourceBuilder WithDashboard(this IResourceBuilder builder, ReferenceExpression? dashboardEndpoint, ReferenceExpression? subscriptionId)
+ {
+ if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ builder.WithAnnotation(new DurableTaskSchedulerDashboardAnnotation(
+ subscriptionId: subscriptionId,
+ dashboardEndpoint: dashboardEndpoint));
+
+ builder.WithOpenDashboardCommand(isTaskHub: true);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Enables the use of dynamic task hubs for the emulator.
+ ///
+ /// The Durable Task Scheduler emulator resource builder.
+ /// A reference to the .
+ ///
+ /// Using dynamic task hubs eliminates the requirement that they be pre-defined,
+ /// which can be useful when the same emulator instance is used across sessions.
+ ///
+ public static IResourceBuilder WithDynamicTaskHubs(this IResourceBuilder builder)
+ {
+ if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ builder.Resource.UseDynamicTaskHubs = true;
+ }
+
+ return builder;
+ }
+
+ static IResourceBuilder WithOpenDashboardCommand(this IResourceBuilder builder, bool isTaskHub) where T : IDurableTaskResourceWithDashboard
+ {
+ if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ builder.WithCommand(
+ isTaskHub ? "durabletask-hub-open-dashboard" : "durabletask-scheduler-open-dashboard",
+ "Open Dashboard",
+ async context =>
+ {
+ var dashboardEndpoint = await builder.Resource.DashboardEndpointExpression.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
+
+ Process.Start(new ProcessStartInfo { FileName = dashboardEndpoint, UseShellExecute = true });
+
+ return CommandResults.Success();
+ },
+ new()
+ {
+ Description = "Open the Durable Task Scheduler Dashboard",
+ IconName = "GlobeArrowForward",
+ IsHighlighted = isTaskHub,
+ });
+ }
+
+ return builder;
+ }
+}
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..1e9b8c8ac3d
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs
@@ -0,0 +1,150 @@
+// 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;
+
+///
+/// Represents a Durable Task Scheduler resource.
+///
+/// The name of the resource.
+public sealed class DurableTaskSchedulerResource(string name)
+ : Resource(name), IResourceWithConnectionString, IResourceWithEndpoints, IDurableTaskResourceWithDashboard
+{
+ EndpointReference EmulatorDashboardEndpoint => new(this, DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard);
+ EndpointReference EmulatorSchedulerEndpoint => new(this, DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker);
+
+ ///
+ /// Gets or sets the authentication type used to access the scheduler.
+ ///
+ ///
+ /// The value should be from .
+ /// The default value is .
+ ///
+ public string? Authentication { get; set; }
+
+ ///
+ /// Gets or sets the client ID used to access the scheduler, when using managed identity for authentication.
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ this.CreateConnectionString();
+
+ ///
+ /// Gets a value indicating whether the scheduler is running as a local emulator.
+ ///
+ public bool IsEmulator => this.IsContainer();
+
+ ///
+ /// Gets or sets the endpoint used by applications to access the scheduler.
+ ///
+ public Uri? SchedulerEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the name of the scheduler (if different from the resource name).
+ ///
+ public string? SchedulerName { get; set; }
+
+ ReferenceExpression IDurableTaskResourceWithDashboard.DashboardEndpointExpression =>
+ this.ResolveDashboardEndpoint();
+
+ internal ReferenceExpression DashboardSchedulerEndpointExpression =>
+ this.ResolveDashboardSchedulerEndpoint();
+
+ internal ReferenceExpression SchedulerEndpointExpression =>
+ this.ResolveSchedulerEndpoint();
+
+ internal ReferenceExpression? SubscriptionIdExpression =>
+ this.ResolveSubscriptionId();
+
+ internal ReferenceExpression SchedulerNameExpression =>
+ this.ResolveSchedulerName();
+
+ ReferenceExpression? ResolveSubscriptionId()
+ {
+ if (this.TryGetLastAnnotation(out DurableTaskSchedulerDashboardAnnotation? annotation) && annotation.SubscriptionId is not null)
+ {
+ return annotation.SubscriptionId;
+ }
+
+ return null;
+ }
+
+ ReferenceExpression ResolveSchedulerName()
+ {
+ if (this.SchedulerName is not null)
+ {
+ return ReferenceExpression.Create($"{this.SchedulerName}");
+ }
+
+ if (this.IsEmulator)
+ {
+ return ReferenceExpression.Create($"default");
+ }
+
+ return ReferenceExpression.Create($"{this.Name}");
+ }
+
+ ReferenceExpression CreateConnectionString()
+ {
+ if (this.TryGetLastAnnotation(out ExistingDurableTaskSchedulerAnnotation? annotation))
+ {
+ return annotation.ConnectionString;
+ }
+
+ string connectionString = $"Authentication={this.Authentication ?? DurableTaskSchedulerAuthentication.None}";
+
+ if (this.ClientId is not null)
+ {
+ connectionString += $";ClientID={this.ClientId}";
+ }
+
+ return ReferenceExpression.Create($"Endpoint={this.SchedulerEndpointExpression};{connectionString}");
+ }
+
+ ReferenceExpression ResolveDashboardEndpoint()
+ {
+ if (this.TryGetLastAnnotation(out DurableTaskSchedulerDashboardAnnotation? annotation) && annotation.DashboardEndpoint is not null)
+ {
+ // NOTE: Container endpoints do not include the trailing slash.
+ return ReferenceExpression.Create($"{annotation.DashboardEndpoint}/");
+ }
+
+ if (this.IsEmulator)
+ {
+ // NOTE: Container endpoints do not include the trailing slash.
+ return ReferenceExpression.Create($"{this.EmulatorDashboardEndpoint}/");
+ }
+
+ return ReferenceExpression.Create($"{DurableTaskConstants.Scheduler.Dashboard.Endpoint.ToString()}");
+ }
+
+ ReferenceExpression ResolveDashboardSchedulerEndpoint()
+ {
+ if (this.IsEmulator)
+ {
+ return ReferenceExpression.Create($"{this.EmulatorDashboardEndpoint}/api/");
+ }
+
+ return this.ResolveSchedulerEndpoint();
+ }
+
+ ReferenceExpression ResolveSchedulerEndpoint()
+ {
+ if (this.SchedulerEndpoint is not null)
+ {
+ return ReferenceExpression.Create($"{this.SchedulerEndpoint.ToString()}");
+ }
+
+ if (this.IsEmulator)
+ {
+ // NOTE: Container endpoints do not include the trailing slash.
+ return ReferenceExpression.Create($"{this.EmulatorSchedulerEndpoint}/");
+ }
+
+ throw new InvalidOperationException("Scheduler endpoint is not set.");
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs
new file mode 100644
index 00000000000..21f8a13cfe7
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs
@@ -0,0 +1,11 @@
+// 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;
+
+internal sealed class ExistingDurableTaskSchedulerAnnotation(ReferenceExpression connectionString) : IResourceAnnotation
+{
+ public ReferenceExpression ConnectionString => connectionString;
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/IDurableTaskResourceWithDashboard.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/IDurableTaskResourceWithDashboard.cs
new file mode 100644
index 00000000000..5e586601c5f
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/IDurableTaskResourceWithDashboard.cs
@@ -0,0 +1,11 @@
+// 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;
+
+internal interface IDurableTaskResourceWithDashboard : IResource
+{
+ ReferenceExpression DashboardEndpointExpression { get; }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs
new file mode 100644
index 00000000000..be4f6efb31c
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs
@@ -0,0 +1,33 @@
+// 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 System.Net;
+
+namespace Aspire.Hosting.Azure;
+
+///
+/// TODO: Drop this when https://github.com/dotnet/aspire/issues/3117 is resolved.
+///
+internal sealed class QueryParameterReference : IValueProvider, IValueWithReferences, IManifestExpressionProvider
+{
+ public static QueryParameterReference Create(ReferenceExpression reference) => new(reference);
+
+ private readonly ReferenceExpression _reference;
+
+ private QueryParameterReference(ReferenceExpression reference)
+ {
+ this._reference = reference;
+ }
+
+ IEnumerable