From 6b698baccb35022df1d22f341214c1d85959d713 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 12 May 2025 13:38:09 -0700 Subject: [PATCH 01/13] Port integration and samples. --- .../DurableTask/DurableTaskConstants.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs 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..a15e36efb26 --- /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; + +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"; + } + } +} From 6cdadb2e3c63258d18478db65b6a0fba9a177e6a Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 12 May 2025 13:38:49 -0700 Subject: [PATCH 02/13] Include remainder. --- Aspire.sln | 63 +++ Directory.Packages.props | 2 + .../DurableTask.Scheduler.AppHost.csproj | 25 ++ .../DurableTask.Scheduler.AppHost/Program.cs | 23 ++ .../Properties/launchSettings.json | 29 ++ .../appsettings.json | 9 + ...rableTask.Scheduler.ExternalAppHost.csproj | 25 ++ .../Program.cs | 21 + .../Properties/launchSettings.json | 29 ++ .../appsettings.json | 13 + .../DurableTask.Scheduler.WebApi.csproj | 17 + .../DurableTask.Scheduler.WebApi.http | 11 + .../DurableTask.Scheduler.WebApi/Program.cs | 47 +++ .../Properties/launchSettings.json | 14 + .../appsettings.json | 9 + .../DurableTask.Scheduler.Worker.csproj | 17 + .../DurableTask.Scheduler.Worker/Program.cs | 35 ++ .../Properties/launchSettings.json | 12 + .../Tasks/Echo/EchoActivity.cs | 19 + .../Tasks/Echo/EchoInput.cs | 9 + .../Tasks/Echo/EchoOrchestrator.cs | 17 + .../appsettings.json | 8 + .../DurableTask/DurableTaskHubResource.cs | 76 ++++ .../DurableTaskSchedulerAuthentication.cs | 28 ++ ...DurableTaskSchedulerDashboardAnnotation.cs | 14 + .../DurableTaskSchedulerEmulatorResource.cs | 29 ++ .../DurableTaskSchedulerExtensions.cs | 361 ++++++++++++++++++ .../DurableTaskSchedulerResource.cs | 150 ++++++++ .../ExistingDurableTaskSchedulerAnnotation.cs | 11 + .../DurableTask/IResourceWithDashboard.cs | 13 + .../DurableTask/ParameterOrValue.cs | 36 ++ .../DurableTask/QueryParameterReference.cs | 30 ++ 32 files changed, 1202 insertions(+) create mode 100644 playground/DurableTask/DurableTask.Scheduler.AppHost/DurableTask.Scheduler.AppHost.csproj create mode 100644 playground/DurableTask/DurableTask.Scheduler.AppHost/Program.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.AppHost/Properties/launchSettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.AppHost/appsettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/DurableTask.Scheduler.ExternalAppHost.csproj create mode 100644 playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Program.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/Properties/launchSettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/appsettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.csproj create mode 100644 playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.http create mode 100644 playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.WebApi/Properties/launchSettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.WebApi/appsettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/DurableTask.Scheduler.Worker.csproj create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/Properties/launchSettings.json create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoInput.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs create mode 100644 playground/DurableTask/DurableTask.Scheduler.Worker/appsettings.json create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerAuthentication.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerEmulatorResource.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs create mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs diff --git a/Aspire.sln b/Aspire.sln index be3505999da..1ce40c17d99 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -671,6 +671,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAppService.ApiService" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAppService.AppHost", "playground\AzureAppService\AzureAppService.AppHost\AzureAppService.AppHost.csproj", "{2C879943-DF34-44FA-B2C3-29D97F24DD76}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DurableTask", "DurableTask", "{4AC6FF77-B104-42B1-92F2-6D8E11B5CE22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Scheduler.Worker", "playground\DurableTask\DurableTask.Scheduler.Worker\DurableTask.Scheduler.Worker.csproj", "{C13DFE4C-4A06-4A50-9B63-7296E214E883}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Scheduler.WebApi", "playground\DurableTask\DurableTask.Scheduler.WebApi\DurableTask.Scheduler.WebApi.csproj", "{40CF7365-191A-4E04-A0E1-6B6C10DF99F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Scheduler.ExternalAppHost", "playground\DurableTask\DurableTask.Scheduler.ExternalAppHost\DurableTask.Scheduler.ExternalAppHost.csproj", "{37369502-9828-455D-82C7-57BEA24BF46F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableTask.Scheduler.AppHost", "playground\DurableTask\DurableTask.Scheduler.AppHost\DurableTask.Scheduler.AppHost.csproj", "{AD9E9337-1132-47A0-AF78-7E2CC812AE75}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -3933,6 +3943,54 @@ Global {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x64.Build.0 = Release|Any CPU {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x86.ActiveCfg = Release|Any CPU {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x86.Build.0 = Release|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Debug|x64.ActiveCfg = Debug|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Debug|x64.Build.0 = Debug|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Debug|x86.ActiveCfg = Debug|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Debug|x86.Build.0 = Debug|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Release|Any CPU.Build.0 = Release|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Release|x64.ActiveCfg = Release|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Release|x64.Build.0 = Release|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Release|x86.ActiveCfg = Release|Any CPU + {C13DFE4C-4A06-4A50-9B63-7296E214E883}.Release|x86.Build.0 = Release|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Debug|x64.Build.0 = Debug|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Debug|x86.Build.0 = Debug|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Release|Any CPU.Build.0 = Release|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Release|x64.ActiveCfg = Release|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Release|x64.Build.0 = Release|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Release|x86.ActiveCfg = Release|Any CPU + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5}.Release|x86.Build.0 = Release|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Debug|x64.ActiveCfg = Debug|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Debug|x64.Build.0 = Debug|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Debug|x86.ActiveCfg = Debug|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Debug|x86.Build.0 = Debug|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Release|Any CPU.Build.0 = Release|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Release|x64.ActiveCfg = Release|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Release|x64.Build.0 = Release|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Release|x86.ActiveCfg = Release|Any CPU + {37369502-9828-455D-82C7-57BEA24BF46F}.Release|x86.Build.0 = Release|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Debug|x64.Build.0 = Debug|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Debug|x86.ActiveCfg = Debug|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Debug|x86.Build.0 = Debug|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Release|Any CPU.Build.0 = Release|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Release|x64.ActiveCfg = Release|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Release|x64.Build.0 = Release|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Release|x86.ActiveCfg = Release|Any CPU + {AD9E9337-1132-47A0-AF78-7E2CC812AE75}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -4255,6 +4313,11 @@ Global {2D9974C2-3AB2-FBFD-5156-080508BB7449} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {A617DC84-65DA-41B5-B378-6C2F569CEE48} = {2D9974C2-3AB2-FBFD-5156-080508BB7449} {2C879943-DF34-44FA-B2C3-29D97F24DD76} = {2D9974C2-3AB2-FBFD-5156-080508BB7449} + {4AC6FF77-B104-42B1-92F2-6D8E11B5CE22} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {C13DFE4C-4A06-4A50-9B63-7296E214E883} = {4AC6FF77-B104-42B1-92F2-6D8E11B5CE22} + {40CF7365-191A-4E04-A0E1-6B6C10DF99F5} = {4AC6FF77-B104-42B1-92F2-6D8E11B5CE22} + {37369502-9828-455D-82C7-57BEA24BF46F} = {4AC6FF77-B104-42B1-92F2-6D8E11B5CE22} + {AD9E9337-1132-47A0-AF78-7E2CC812AE75} = {4AC6FF77-B104-42B1-92F2-6D8E11B5CE22} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4} diff --git a/Directory.Packages.props b/Directory.Packages.props index 08ea0b6de79..035e92d055f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,6 +87,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..46aaca3eb5d --- /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 ([FromBody] EchoValue value, [FromServices] 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..74e3a9213b8 --- /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("EchoOrchestrator"); + }); + 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..a94ddc1a03e --- /dev/null +++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs @@ -0,0 +1,19 @@ +using System.Net.Http.Json; +using Microsoft.DurableTask; + +namespace DurableTask.Scheduler.Worker.Tasks.Echo; + +[DurableTask("EchoActivity")] +sealed class EchoActivity(IHttpClientFactory clientFactory) : TaskActivity +{ + public override async Task RunAsync(TaskActivityContext context, EchoInput input) + { + HttpClient client = clientFactory.CreateClient("Echo"); + + var result = await client.PostAsync("/echo", JsonContent.Create(new EchoInput { Text = input.Text })); + + 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..128a62dcbfe --- /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); + + 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/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs new file mode 100644 index 00000000000..4e00d7eab68 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -0,0 +1,76 @@ +// 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, IResourceWithDashboard +{ + /// + 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 IResourceWithDashboard.DashboardEndpointExpression => + this.GetDashboardEndpoint(); + + internal ReferenceExpression TaskHubNameExpression => + ReferenceExpression.Create($"{this.ResolveTaskHubName()}"); + + bool IResourceWithDashboard.IsTaskHub => true; + + 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 ReferenceExpression.Create($"{annotation.DashboardEndpoint}"); + } + + return (this.Parent as IResourceWithDashboard).DashboardEndpointExpression; + } + + ReferenceExpression? ResolveSubscriptionId() + { + if (this.TryGetLastAnnotation(out var annotation) + && annotation.SubscriptionId is not null) + { + return ReferenceExpression.Create($"{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..1a0cfec10af --- /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; + +sealed class DurableTaskSchedulerDashboardAnnotation(ParameterOrValue? subscriptionId, ParameterOrValue? dashboardEndpoint) + : IResourceAnnotation +{ + public ParameterOrValue? DashboardEndpoint => dashboardEndpoint; + + public ParameterOrValue? 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..36113ea1a6a --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -0,0 +1,361 @@ +// 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. + /// + public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) + { + if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + return builder; + } + + builder + .WithEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker, scheme: "http", targetPort: 8080) + .WithEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard, scheme: "http", targetPort: 8082) + .WithAnnotation( + new EnvironmentCallbackAnnotation( + 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 + }) + .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(); + } + + if (configureContainer is not null) + { + var surrogate = new DurableTaskSchedulerEmulatorResource(builder.Resource); + + var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate); + + configureContainer(surrogateBuilder); + + if (surrogate.UseDynamicTaskHubs) + { + builder.WithAnnotation( + new EnvironmentCallbackAnnotation( + (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 . + public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString) + { + return builder.RunAsExisting(connectionString.Resource.Value); + } + + /// + /// 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 . + public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, string connectionString) + { + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithAnnotation(new ExistingDurableTaskSchedulerAnnotation(ParameterOrValue.Create(connectionString))); + + var connectionStringParameters = ParseConnectionString(connectionString); + + 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; + } + + /// + /// + /// + /// + /// + /// + public static IResourceBuilder WithDashboard(this IResourceBuilder builder, string dashboardEndpoint) + { + return builder.WithDashboard(ParameterOrValue.Create(dashboardEndpoint)); + } + + /// + /// + /// + /// + /// + /// + public static IResourceBuilder WithDashboard(this IResourceBuilder builder, IResourceBuilder? dashboardEndpoint = null) + { + return builder.WithDashboard(dashboardEndpoint is not null ? ParameterOrValue.Create(dashboardEndpoint) : null); + } + + static IResourceBuilder WithDashboard(this IResourceBuilder builder, ParameterOrValue? dashboardEndpoint) + { + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithAnnotation(new DurableTaskSchedulerDashboardAnnotation( + subscriptionId: null, + dashboardEndpoint: dashboardEndpoint)); + + builder.WithOpenDashboardCommand(); + } + + 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; + } + + /// + /// + /// + /// + /// + /// + /// + public static IResourceBuilder WithDashboard(this IResourceBuilder builder, string? dashboardEndpoint, string? subscriptionId) + { + return builder.WithDashboard( + dashboardEndpoint: dashboardEndpoint is not null ? ParameterOrValue.Create(dashboardEndpoint) : null, + subscriptionId: subscriptionId is not null ? ParameterOrValue.Create(subscriptionId) : null); + } + + /// + /// + /// + /// + /// + /// + /// + public static IResourceBuilder WithDashboard(this IResourceBuilder builder, IResourceBuilder? dashboardEndpoint = null, IResourceBuilder? subscriptionId = null) + { + return builder.WithDashboard( + dashboardEndpoint: dashboardEndpoint is not null ? ParameterOrValue.Create(dashboardEndpoint) : null, + subscriptionId: subscriptionId is not null ? ParameterOrValue.Create(subscriptionId) : null); + } + + static IResourceBuilder WithDashboard(this IResourceBuilder builder, ParameterOrValue? dashboardEndpoint, ParameterOrValue? subscriptionId) + { + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithAnnotation(new DurableTaskSchedulerDashboardAnnotation( + subscriptionId: subscriptionId, + dashboardEndpoint: dashboardEndpoint)); + + builder.WithOpenDashboardCommand(); + } + + 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) where T : IResourceWithDashboard + { + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + builder.WithCommand( + builder.Resource.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 = builder.Resource.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..81df6489db4 --- /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, IResourceWithDashboard +{ + 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 IResourceWithDashboard.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 ReferenceExpression.Create($"{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 ReferenceExpression.Create($"{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..5cb10744dff --- /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; + +sealed class ExistingDurableTaskSchedulerAnnotation(ParameterOrValue connectionString) : IResourceAnnotation +{ + public ParameterOrValue ConnectionString => connectionString; +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs new file mode 100644 index 00000000000..d403ba48798 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs @@ -0,0 +1,13 @@ +// 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; + +interface IResourceWithDashboard : IResource +{ + ReferenceExpression DashboardEndpointExpression { get; } + + bool IsTaskHub => false; +} diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs new file mode 100644 index 00000000000..64fd924f901 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs @@ -0,0 +1,36 @@ +// 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; + +sealed class ParameterOrValue(object? parameter) : IValueProvider, IManifestExpressionProvider +{ + public static ParameterOrValue Create(IValueProvider? parameter) + { + return new(parameter); + } + + public static ParameterOrValue Create(object? parameter) + { + return new(parameter); + } + + public ValueTask GetValueAsync(CancellationToken cancellationToken = new CancellationToken()) + { + if (parameter is IValueProvider valueProvider) + { + return valueProvider.GetValueAsync(cancellationToken); + } + else + { + return new ValueTask(parameter?.ToString()); + } + } + + public string ValueExpression => + parameter is IManifestExpressionProvider manifestExpressionProvider + ? manifestExpressionProvider.ValueExpression + : parameter?.ToString() ?? String.Empty; +} 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..6bd7234c0e6 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs @@ -0,0 +1,30 @@ +// 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; + +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 IValueWithReferences.References => [this._reference]; + + public async ValueTask GetValueAsync(CancellationToken cancellationToken = default) + { + var value = await _reference.GetValueAsync(cancellationToken).ConfigureAwait(false); + + return WebUtility.UrlEncode(value); + } + + string IManifestExpressionProvider.ValueExpression => WebUtility.UrlEncode(_reference.ValueExpression); +} From 1b7b410f1cf0c7d999c023a190e633dce9de6237 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 13 May 2025 14:39:40 -0700 Subject: [PATCH 03/13] Pull in unit/integration test. --- .../AzureFunctionsDurableTaskTests.cs | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs new file mode 100644 index 00000000000..55e212d75e7 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs @@ -0,0 +1,275 @@ +// 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; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Aspire.Hosting.Testing; +using Aspire.Hosting.Utils; +using Aspire.TestUtilities; +using Microsoft.Extensions.DependencyInjection; + +namespace CommunityToolkit.Aspire.Hosting.DurableTask.Tests.Scheduler; + +public class AddDurableTaskSchedulerTests +{ + [Fact] + public async Task AddDurableTaskScheduler() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddDurableTaskScheduler("scheduler"); + + using var app = builder.Build(); + + var model = app.Services.GetRequiredService(); + + var scheduler = model.Resources.OfType().Single(); + + Assert.Null(scheduler.Authentication); + Assert.Null(scheduler.ClientId); + Assert.False(scheduler.IsEmulator); + Assert.Null(scheduler.SchedulerEndpoint); + Assert.Null(scheduler.SchedulerName); + + await Assert.ThrowsAsync(async () => await scheduler.ConnectionStringExpression.GetValueAsync(CancellationToken.None)); + await Assert.ThrowsAsync(async () => await scheduler.SchedulerEndpointExpression.GetValueAsync(CancellationToken.None)); + + Assert.Null(scheduler.SubscriptionIdExpression); + + Assert.Equal(DurableTaskConstants.Scheduler.Dashboard.Endpoint.ToString(), await (scheduler as IResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); + await Assert.ThrowsAsync(async () => await scheduler.DashboardSchedulerEndpointExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("scheduler", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); + } + + [Fact] + public async Task AddDurableTaskSchedulerWithConfiguration() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddDurableTaskScheduler( + "scheduler", + options => + { + options.Resource.Authentication = "TestAuthentication"; + options.Resource.ClientId = "TestClientId"; + options.Resource.SchedulerEndpoint = new Uri("https://scheduler.test.io"); + options.Resource.SchedulerName = "TestSchedulerName"; + }); + + using var app = builder.Build(); + + var model = app.Services.GetRequiredService(); + + var scheduler = model.Resources.OfType().Single(); + + Assert.Equal("TestAuthentication", scheduler.Authentication); + Assert.Equal("TestClientId", scheduler.ClientId); + Assert.False(scheduler.IsEmulator); + Assert.Equal(new Uri("https://scheduler.test.io"), scheduler.SchedulerEndpoint); + Assert.Equal("TestSchedulerName", scheduler.SchedulerName); + + Assert.Equal("Endpoint=https://scheduler.test.io/;Authentication=TestAuthentication;ClientID=TestClientId", await scheduler.ConnectionStringExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("https://scheduler.test.io/", await scheduler.SchedulerEndpointExpression.GetValueAsync(CancellationToken.None)); + + Assert.Null(scheduler.SubscriptionIdExpression); + + Assert.Equal("https://dashboard.durabletask.io/".ToString(), await (scheduler as IResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("https://scheduler.test.io/", scheduler.DashboardSchedulerEndpointExpression.ValueExpression); + Assert.Equal("TestSchedulerName", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); + } + + [Fact] + public async Task AddDurableTaskSchedulerAsExisting() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder + .AddDurableTaskScheduler("scheduler") + .RunAsExisting("Endpoint=https://scheduler.test.io/;Authentication=TestAuth"); + + using var app = builder.Build(); + + var model = app.Services.GetRequiredService(); + + var scheduler = model.Resources.OfType().Single(); + + Assert.Equal("TestAuth", scheduler.Authentication); + Assert.Null(scheduler.ClientId); + Assert.False(scheduler.IsEmulator); + Assert.Equal(new Uri("https://scheduler.test.io/"), scheduler.SchedulerEndpoint); + Assert.Null(scheduler.SchedulerName); + + Assert.Equal("Endpoint=https://scheduler.test.io/;Authentication=TestAuth", await scheduler.ConnectionStringExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("https://scheduler.test.io/", await scheduler.SchedulerEndpointExpression.GetValueAsync(CancellationToken.None)); + + Assert.Null(scheduler.SubscriptionIdExpression); + + Assert.Equal("https://dashboard.durabletask.io/".ToString(), await (scheduler as IResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("https://scheduler.test.io/", scheduler.DashboardSchedulerEndpointExpression.ValueExpression); + Assert.Equal("scheduler", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); + } + + [Fact] + public async Task AddDurableTaskSchedulerAsEmulator() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder + .AddDurableTaskScheduler("scheduler") + .RunAsEmulator(); + + using var app = builder.Build(); + + var model = app.Services.GetRequiredService(); + + var scheduler = model.Resources.OfType().Single(); + + Assert.Equal("None", scheduler.Authentication); + Assert.Null(scheduler.ClientId); + Assert.True(scheduler.IsEmulator); + Assert.Null(scheduler.SchedulerEndpoint); + Assert.Null(scheduler.SchedulerName); + + Assert.Equal("Endpoint={scheduler.bindings.worker.url}/;Authentication=None", scheduler.ConnectionStringExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.worker.url}/", scheduler.SchedulerEndpointExpression.ValueExpression); + + Assert.Null(scheduler.SubscriptionIdExpression); + + Assert.Equal("{scheduler.bindings.dashboard.url}/", (scheduler as IResourceWithDashboard).DashboardEndpointExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.dashboard.url}/api/", scheduler.DashboardSchedulerEndpointExpression.ValueExpression); + Assert.Equal("default", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); + + Assert.True(scheduler.TryGetLastAnnotation(out var imageAnnotation)); + + Assert.Equal("mcr.microsoft.com/dts", imageAnnotation.Registry); + Assert.Equal("dts-emulator", imageAnnotation.Image); + Assert.Equal("latest", imageAnnotation.Tag); + + Assert.True(scheduler.TryGetEnvironmentVariables(out var environmentVariables)); + + EnvironmentCallbackContext context = new(builder.ExecutionContext); + + foreach (var environmentVariable in environmentVariables) + { + await environmentVariable.Callback(context); + } + + // NOTE: If no task hub names are specified, no variable should be set as the default task hub name. + Assert.False(context.EnvironmentVariables.TryGetValue("DTS_TASK_HUB_NAMES", out var taskHubNames)); + } + + [Fact] + public async Task AddDurableTaskSchedulerAsEmulatorWithDynamicTaskhubs() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var schedulerBuilder = builder + .AddDurableTaskScheduler("scheduler") + .RunAsEmulator( + options => + { + options.WithDynamicTaskHubs(); + }); + + using var app = builder.Build(); + + var model = app.Services.GetRequiredService(); + + var scheduler = model.Resources.OfType().Single(); + + Assert.True(scheduler.TryGetEnvironmentVariables(out var environmentVariables)); + + EnvironmentCallbackContext context = new(builder.ExecutionContext); + + foreach (var environmentVariable in environmentVariables) + { + await environmentVariable.Callback(context); + } + + Assert.True(context.EnvironmentVariables.TryGetValue("DTS_USE_DYNAMIC_TASK_HUBS", out var useDynamicTaskHubs)); + Assert.Equal("true", useDynamicTaskHubs); + } + + [Fact] + public async Task AddDurableTaskSchedulerAsEmulatorWithTaskhub() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var schedulerBuilder = builder + .AddDurableTaskScheduler("scheduler") + .RunAsEmulator(); + + schedulerBuilder.AddTaskHub("taskhub1"); + schedulerBuilder.AddTaskHub("taskhub2").WithTaskHubName("taskhub2a"); + + using var app = builder.Build(); + + var model = app.Services.GetRequiredService(); + + var scheduler = model.Resources.OfType().Single(); + + Assert.True(scheduler.TryGetEnvironmentVariables(out var environmentVariables)); + + EnvironmentCallbackContext context = new(builder.ExecutionContext); + + foreach (var environmentVariable in environmentVariables) + { + await environmentVariable.Callback(context); + } + + Assert.True(context.EnvironmentVariables.TryGetValue("DTS_TASK_HUB_NAMES", out var taskHubNameString)); + + var taskHubNames = + taskHubNameString + .ToString() + !.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .OrderBy(x => x); + + Assert.Equal(taskHubNames, [ "taskhub1", "taskhub2a" ]); + + var taskHub1 = model.Resources.OfType().Single(x => x.Name == "taskhub1"); + + Assert.Equal("Endpoint={scheduler.bindings.worker.url}/;Authentication=None;TaskHub=taskhub1", taskHub1.ConnectionStringExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.dashboard.url}/subscriptions/default/schedulers/default/taskhubs/taskhub1", (taskHub1 as IResourceWithDashboard).DashboardEndpointExpression.ValueExpression); + + var taskHub2 = model.Resources.OfType().Single(x => x.Name == "taskhub2"); + + Assert.Equal("Endpoint={scheduler.bindings.worker.url}/;Authentication=None;TaskHub=taskhub2a", taskHub2.ConnectionStringExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.dashboard.url}/subscriptions/default/schedulers/default/taskhubs/taskhub2a", (taskHub2 as IResourceWithDashboard).DashboardEndpointExpression.ValueExpression); + } + + [RequiresDocker] + [Fact] + public async Task ResourceStartsAndRespondsOk() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + using var builder = TestDistributedApplicationBuilder.Create(); + + var scheduler = + builder.AddDurableTaskScheduler("scheduler") + .RunAsEmulator(); + + using var app = builder.Build(); + + await app.StartAsync(cts.Token); + + await app.ResourceNotifications.WaitForResourceAsync(scheduler.Resource.Name, KnownResourceStates.Running, cts.Token); + + using var client = app.CreateHttpClient(scheduler.Resource.Name, "dashboard"); + + using var response = await client.SendAsync( + new() + { + Headers = + { + { "x-taskhub", "default" }, + }, + RequestUri = new Uri("/api/v1/taskhubs/ping", UriKind.Relative), + Method = HttpMethod.Get + }, cts.Token); + + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file From c436f52c7924499a60c76577961e0080d262a3ad Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 13 May 2025 23:02:15 +0000 Subject: [PATCH 04/13] Update docs. --- .../DurableTaskSchedulerExtensions.cs | 34 +++++++++++++++++++ .../AzureFunctionsDurableTaskTests.cs | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs index 36113ea1a6a..e9b5a279806 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -38,6 +38,22 @@ public static IResourceBuilder AddDurableTaskSched /// /// 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) @@ -133,6 +149,24 @@ public static IResourceBuilder RunAsEmulator(this /// 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(connectionString.Resource.Value); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs index 55e212d75e7..1f089583dea 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs @@ -9,7 +9,7 @@ using Aspire.TestUtilities; using Microsoft.Extensions.DependencyInjection; -namespace CommunityToolkit.Aspire.Hosting.DurableTask.Tests.Scheduler; +namespace Aspire.Hosting.Azure.Tests; public class AddDurableTaskSchedulerTests { From 7fbbbfd08773792177db79639c0435b0305082f3 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 14 May 2025 20:12:09 +0000 Subject: [PATCH 05/13] Add comment re: existing issue. --- .../DurableTask/QueryParameterReference.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs index 6bd7234c0e6..e91d74b4ba0 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs @@ -6,6 +6,9 @@ namespace Aspire.Hosting.Azure; +/// +/// TODO: Drop this when https://github.com/dotnet/aspire/issues/3117 is resolved. +/// sealed class QueryParameterReference : IValueProvider, IValueWithReferences, IManifestExpressionProvider { public static QueryParameterReference Create(ReferenceExpression reference) => new(reference); From 7be4aaa6e7ad3fb988c8dfb84210c314a9c43e24 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 14 May 2025 14:09:38 -0700 Subject: [PATCH 06/13] Eliminate ParameterOrValue type. --- .../DurableTask/DurableTaskHubResource.cs | 4 +- ...DurableTaskSchedulerDashboardAnnotation.cs | 6 +- .../DurableTaskSchedulerExtensions.cs | 77 ++++++++++++++++--- .../DurableTaskSchedulerResource.cs | 4 +- .../ExistingDurableTaskSchedulerAnnotation.cs | 4 +- .../DurableTask/ParameterOrValue.cs | 36 --------- .../AzureFunctionsDurableTaskTests.cs | 4 +- 7 files changed, 76 insertions(+), 59 deletions(-) delete mode 100644 src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs index 4e00d7eab68..acc7ac454ee 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -57,7 +57,7 @@ ReferenceExpression ResolveDashboardEndpoint() if (this.TryGetLastAnnotation(out var annotation) && annotation.DashboardEndpoint is not null) { - return ReferenceExpression.Create($"{annotation.DashboardEndpoint}"); + return annotation.DashboardEndpoint; } return (this.Parent as IResourceWithDashboard).DashboardEndpointExpression; @@ -68,7 +68,7 @@ ReferenceExpression ResolveDashboardEndpoint() if (this.TryGetLastAnnotation(out var annotation) && annotation.SubscriptionId is not null) { - return ReferenceExpression.Create($"{annotation.SubscriptionId}"); + return annotation.SubscriptionId; } return this.Parent.SubscriptionIdExpression; diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs index 1a0cfec10af..8cf1f4bc49b 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs @@ -5,10 +5,10 @@ namespace Aspire.Hosting.Azure; -sealed class DurableTaskSchedulerDashboardAnnotation(ParameterOrValue? subscriptionId, ParameterOrValue? dashboardEndpoint) +sealed class DurableTaskSchedulerDashboardAnnotation(ReferenceExpression? subscriptionId, ReferenceExpression? dashboardEndpoint) : IResourceAnnotation { - public ParameterOrValue? DashboardEndpoint => dashboardEndpoint; + public ReferenceExpression? DashboardEndpoint => dashboardEndpoint; - public ParameterOrValue? SubscriptionId => subscriptionId; + public ReferenceExpression? SubscriptionId => subscriptionId; } diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs index e9b5a279806..5a54b893d6b 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -169,7 +169,7 @@ public static IResourceBuilder RunAsEmulator(this /// public static IResourceBuilder RunAsExisting(this IResourceBuilder builder, IResourceBuilder connectionString) { - return builder.RunAsExisting(connectionString.Resource.Value); + return builder.RunAsExisting(ReferenceExpression.Create($"{connectionString}")); } /// @@ -178,13 +178,68 @@ public static IResourceBuilder RunAsExisting(this /// 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(ParameterOrValue.Create(connectionString))); + builder.WithAnnotation(new ExistingDurableTaskSchedulerAnnotation(connectionString)); - var connectionStringParameters = ParseConnectionString(connectionString); + var connectionStringParameters = ParseConnectionString(connectionString.ValueExpression); if (connectionStringParameters.TryGetValue("Endpoint", out string? endpoint)) { @@ -229,7 +284,7 @@ static IReadOnlyDictionary ParseConnectionString(string connecti /// public static IResourceBuilder WithDashboard(this IResourceBuilder builder, string dashboardEndpoint) { - return builder.WithDashboard(ParameterOrValue.Create(dashboardEndpoint)); + return builder.WithDashboard(ReferenceExpression.Create($"{dashboardEndpoint}")); } /// @@ -240,10 +295,10 @@ public static IResourceBuilder WithDashboard(this /// public static IResourceBuilder WithDashboard(this IResourceBuilder builder, IResourceBuilder? dashboardEndpoint = null) { - return builder.WithDashboard(dashboardEndpoint is not null ? ParameterOrValue.Create(dashboardEndpoint) : null); + return builder.WithDashboard(dashboardEndpoint is not null ? ReferenceExpression.Create($"{dashboardEndpoint}") : null); } - static IResourceBuilder WithDashboard(this IResourceBuilder builder, ParameterOrValue? dashboardEndpoint) + static IResourceBuilder WithDashboard(this IResourceBuilder builder, ReferenceExpression? dashboardEndpoint) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) { @@ -316,8 +371,8 @@ public static IResourceBuilder WithTaskHubName(this IRes public static IResourceBuilder WithDashboard(this IResourceBuilder builder, string? dashboardEndpoint, string? subscriptionId) { return builder.WithDashboard( - dashboardEndpoint: dashboardEndpoint is not null ? ParameterOrValue.Create(dashboardEndpoint) : null, - subscriptionId: subscriptionId is not null ? ParameterOrValue.Create(subscriptionId) : null); + dashboardEndpoint: dashboardEndpoint is not null ? ReferenceExpression.Create($"{dashboardEndpoint}") : null, + subscriptionId: subscriptionId is not null ? ReferenceExpression.Create($"{subscriptionId}") : null); } /// @@ -330,11 +385,11 @@ public static IResourceBuilder WithDashboard(this IResou public static IResourceBuilder WithDashboard(this IResourceBuilder builder, IResourceBuilder? dashboardEndpoint = null, IResourceBuilder? subscriptionId = null) { return builder.WithDashboard( - dashboardEndpoint: dashboardEndpoint is not null ? ParameterOrValue.Create(dashboardEndpoint) : null, - subscriptionId: subscriptionId is not null ? ParameterOrValue.Create(subscriptionId) : null); + dashboardEndpoint: dashboardEndpoint is not null ? ReferenceExpression.Create($"{dashboardEndpoint}") : null, + subscriptionId: subscriptionId is not null ? ReferenceExpression.Create($"{subscriptionId}") : null); } - static IResourceBuilder WithDashboard(this IResourceBuilder builder, ParameterOrValue? dashboardEndpoint, ParameterOrValue? subscriptionId) + static IResourceBuilder WithDashboard(this IResourceBuilder builder, ReferenceExpression? dashboardEndpoint, ReferenceExpression? subscriptionId) { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) { diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs index 81df6489db4..8aa75cf6963 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs @@ -67,7 +67,7 @@ public sealed class DurableTaskSchedulerResource(string name) { if (this.TryGetLastAnnotation(out DurableTaskSchedulerDashboardAnnotation? annotation) && annotation.SubscriptionId is not null) { - return ReferenceExpression.Create($"{annotation.SubscriptionId}"); + return annotation.SubscriptionId; } return null; @@ -92,7 +92,7 @@ ReferenceExpression CreateConnectionString() { if (this.TryGetLastAnnotation(out ExistingDurableTaskSchedulerAnnotation? annotation)) { - return ReferenceExpression.Create($"{annotation.ConnectionString}"); + return annotation.ConnectionString; } string connectionString = $"Authentication={this.Authentication ?? DurableTaskSchedulerAuthentication.None}"; diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs index 5cb10744dff..e17e6908447 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting.Azure; -sealed class ExistingDurableTaskSchedulerAnnotation(ParameterOrValue connectionString) : IResourceAnnotation +sealed class ExistingDurableTaskSchedulerAnnotation(ReferenceExpression connectionString) : IResourceAnnotation { - public ParameterOrValue ConnectionString => connectionString; + public ReferenceExpression ConnectionString => connectionString; } diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs deleted file mode 100644 index 64fd924f901..00000000000 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/ParameterOrValue.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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; - -sealed class ParameterOrValue(object? parameter) : IValueProvider, IManifestExpressionProvider -{ - public static ParameterOrValue Create(IValueProvider? parameter) - { - return new(parameter); - } - - public static ParameterOrValue Create(object? parameter) - { - return new(parameter); - } - - public ValueTask GetValueAsync(CancellationToken cancellationToken = new CancellationToken()) - { - if (parameter is IValueProvider valueProvider) - { - return valueProvider.GetValueAsync(cancellationToken); - } - else - { - return new ValueTask(parameter?.ToString()); - } - } - - public string ValueExpression => - parameter is IManifestExpressionProvider manifestExpressionProvider - ? manifestExpressionProvider.ValueExpression - : parameter?.ToString() ?? String.Empty; -} diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs index 1f089583dea..47ae55f8d01 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs @@ -1,9 +1,7 @@ // 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; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Azure; using Aspire.Hosting.Testing; using Aspire.Hosting.Utils; using Aspire.TestUtilities; @@ -11,7 +9,7 @@ namespace Aspire.Hosting.Azure.Tests; -public class AddDurableTaskSchedulerTests +public class AzureFunctionsDurableTaskTests { [Fact] public async Task AddDurableTaskScheduler() From 97bd8b2287d452da64d5f3462b154fedb601064e Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 16 May 2025 10:25:44 -0700 Subject: [PATCH 07/13] Update new solution file with playground projects. --- Aspire.slnx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Aspire.slnx b/Aspire.slnx index 712efa83def..f8588e93946 100644 --- a/Aspire.slnx +++ b/Aspire.slnx @@ -153,6 +153,12 @@ + + + + + + From 2bdb505b63c8687a376d264f0575999d993922a2 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 29 May 2025 13:38:22 -0700 Subject: [PATCH 08/13] Remove slash that breaks `dotnet build`. --- Aspire.slnx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Aspire.slnx b/Aspire.slnx index f8588e93946..08f94aaf111 100644 --- a/Aspire.slnx +++ b/Aspire.slnx @@ -154,10 +154,10 @@ - - - - + + + + From 9eb2b7b2f8b9987b080b602a72e6cb0775842343 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 29 May 2025 14:55:15 -0700 Subject: [PATCH 09/13] Refactoring of surrogate resource initialization and sample fixups. --- .../DurableTask.Scheduler.WebApi/Program.cs | 2 +- .../DurableTask.Scheduler.Worker/Program.cs | 2 +- .../Tasks/Echo/EchoActivity.cs | 7 +- .../Tasks/Echo/EchoOrchestrator.cs | 2 +- .../DurableTaskSchedulerExtensions.cs | 79 ++++++++----------- 5 files changed, 41 insertions(+), 51 deletions(-) diff --git a/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs b/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs index 46aaca3eb5d..92ff9cf8328 100644 --- a/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs +++ b/playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs @@ -20,7 +20,7 @@ var app = builder.Build(); -app.MapPost("/create", async ([FromBody] EchoValue value, [FromServices] DurableTaskClient durableTaskClient) => +app.MapPost("/create", async (EchoValue value, DurableTaskClient durableTaskClient) => { string instanceId = await durableTaskClient.ScheduleNewOrchestrationInstanceAsync( "Echo", diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs index 74e3a9213b8..aad6f3b3cd5 100644 --- a/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs +++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs @@ -20,7 +20,7 @@ workerBuilder.AddTasks(r => { r.AddActivity("EchoActivity"); - r.AddOrchestrator("EchoOrchestrator"); + r.AddOrchestrator("Echo"); }); workerBuilder.UseDurableTaskScheduler( builder.Configuration.GetConnectionString("taskhub") ?? throw new InvalidOperationException("Scheduler connection string not configured."), diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs index a94ddc1a03e..65f7d7ed2f8 100644 --- a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs +++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoActivity.cs @@ -3,14 +3,13 @@ namespace DurableTask.Scheduler.Worker.Tasks.Echo; -[DurableTask("EchoActivity")] -sealed class EchoActivity(IHttpClientFactory clientFactory) : TaskActivity +sealed class EchoActivity(IHttpClientFactory clientFactory) : TaskActivity { - public override async Task RunAsync(TaskActivityContext context, EchoInput input) + 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.Text })); + var result = await client.PostAsync("/echo", JsonContent.Create(new EchoInput { Text = input })); var output = await result.Content.ReadFromJsonAsync(); diff --git a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs index 128a62dcbfe..7b5b1c11fe2 100644 --- a/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs +++ b/playground/DurableTask/DurableTask.Scheduler.Worker/Tasks/Echo/EchoOrchestrator.cs @@ -6,7 +6,7 @@ sealed class EchoOrchestrator : TaskOrchestrator { public override async Task RunAsync(TaskOrchestrationContext context, EchoInput input) { - string output = await context.CallActivityAsync("EchoActivity", input); + string output = await context.CallActivityAsync("EchoActivity", input.Text); output = await context.CallActivityAsync("EchoActivity", output); diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs index 5a54b893d6b..8a4963cbf45 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -62,11 +62,30 @@ public static IResourceBuilder RunAsEmulator(this } 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)) .WithEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker, scheme: "http", targetPort: 8080) .WithEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard, scheme: "http", targetPort: 8082) - .WithAnnotation( - new EnvironmentCallbackAnnotation( - async (EnvironmentCallbackContext context) => + .WithEnvironment( + async (EnvironmentCallbackContext context) => { var nameTasks = builder @@ -91,51 +110,23 @@ public static IResourceBuilder RunAsEmulator(this 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 - }) - .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(); - } + configureContainer?.Invoke(surrogateBuilder); - if (configureContainer is not null) + if (surrogateBuilder.Resource.UseDynamicTaskHubs) { - var surrogate = new DurableTaskSchedulerEmulatorResource(builder.Resource); - - var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate); - - configureContainer(surrogateBuilder); - - if (surrogate.UseDynamicTaskHubs) - { - builder.WithAnnotation( - new EnvironmentCallbackAnnotation( - (EnvironmentCallbackContext context) => - { - context.EnvironmentVariables.Add("DTS_USE_DYNAMIC_TASK_HUBS", "true"); - }) - ); - } + surrogateBuilder.WithEnvironment( + (EnvironmentCallbackContext context) => + { + context.EnvironmentVariables.Add("DTS_USE_DYNAMIC_TASK_HUBS", "true"); + }); } builder.Resource.Authentication ??= DurableTaskSchedulerAuthentication.None; @@ -277,7 +268,7 @@ static IReadOnlyDictionary ParseConnectionString(string connecti } /// - /// + /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard in a web browser. /// /// /// @@ -288,7 +279,7 @@ public static IResourceBuilder WithDashboard(this } /// - /// + /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard in a web browser. /// /// /// @@ -361,8 +352,8 @@ public static IResourceBuilder WithTaskHubName(this IRes return builder; } - /// - /// + /// + /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard for the task hub in a web browser. /// /// /// @@ -376,7 +367,7 @@ public static IResourceBuilder WithDashboard(this IResou } /// - /// + /// Adds a command to the resource that opens the Durable Task Scheduler Dashboard for the task hub in a web browser. /// /// /// From c3338d299acda3aa2e3c218e4d6d330a408ad90c Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 29 May 2025 15:01:38 -0700 Subject: [PATCH 10/13] Mark internal types as `internal`. --- .../DurableTask/DurableTaskConstants.cs | 2 +- .../DurableTask/DurableTaskSchedulerDashboardAnnotation.cs | 2 +- .../DurableTask/ExistingDurableTaskSchedulerAnnotation.cs | 2 +- .../DurableTask/IResourceWithDashboard.cs | 2 +- .../DurableTask/QueryParameterReference.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs index a15e36efb26..a603081a7cc 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskConstants.cs @@ -3,7 +3,7 @@ namespace Aspire.Hosting.Azure; -static class DurableTaskConstants +internal static class DurableTaskConstants { public static class Scheduler { diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs index 8cf1f4bc49b..d4b18be2aa6 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerDashboardAnnotation.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting.Azure; -sealed class DurableTaskSchedulerDashboardAnnotation(ReferenceExpression? subscriptionId, ReferenceExpression? dashboardEndpoint) +internal sealed class DurableTaskSchedulerDashboardAnnotation(ReferenceExpression? subscriptionId, ReferenceExpression? dashboardEndpoint) : IResourceAnnotation { public ReferenceExpression? DashboardEndpoint => dashboardEndpoint; diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs index e17e6908447..21f8a13cfe7 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/ExistingDurableTaskSchedulerAnnotation.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting.Azure; -sealed class ExistingDurableTaskSchedulerAnnotation(ReferenceExpression connectionString) : IResourceAnnotation +internal sealed class ExistingDurableTaskSchedulerAnnotation(ReferenceExpression connectionString) : IResourceAnnotation { public ReferenceExpression ConnectionString => connectionString; } diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs index d403ba48798..29282546124 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting.Azure; -interface IResourceWithDashboard : IResource +internal interface IResourceWithDashboard : IResource { ReferenceExpression DashboardEndpointExpression { get; } diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs index e91d74b4ba0..be4f6efb31c 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/QueryParameterReference.cs @@ -9,7 +9,7 @@ namespace Aspire.Hosting.Azure; /// /// TODO: Drop this when https://github.com/dotnet/aspire/issues/3117 is resolved. /// -sealed class QueryParameterReference : IValueProvider, IValueWithReferences, IManifestExpressionProvider +internal sealed class QueryParameterReference : IValueProvider, IValueWithReferences, IManifestExpressionProvider { public static QueryParameterReference Create(ReferenceExpression reference) => new(reference); From 6434fed4bc8202696d58a18c1c7a6077c386ff02 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 29 May 2025 21:02:13 -0700 Subject: [PATCH 11/13] Simplify IResourceWithDashboard. --- .../DurableTask/DurableTaskSchedulerExtensions.cs | 10 +++++----- .../DurableTask/IResourceWithDashboard.cs | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs index 8a4963cbf45..bc82d7bf9b4 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -297,7 +297,7 @@ static IResourceBuilder WithDashboard(this IResour subscriptionId: null, dashboardEndpoint: dashboardEndpoint)); - builder.WithOpenDashboardCommand(); + builder.WithOpenDashboardCommand(isTaskHub: false); } return builder; @@ -388,7 +388,7 @@ static IResourceBuilder WithDashboard(this IResourceBuil subscriptionId: subscriptionId, dashboardEndpoint: dashboardEndpoint)); - builder.WithOpenDashboardCommand(); + builder.WithOpenDashboardCommand(isTaskHub: true); } return builder; @@ -413,12 +413,12 @@ public static IResourceBuilder WithDynamic return builder; } - static IResourceBuilder WithOpenDashboardCommand(this IResourceBuilder builder) where T : IResourceWithDashboard + static IResourceBuilder WithOpenDashboardCommand(this IResourceBuilder builder, bool isTaskHub) where T : IResourceWithDashboard { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) { builder.WithCommand( - builder.Resource.IsTaskHub ? "durabletask-hub-open-dashboard" : "durabletask-scheduler-open-dashboard", + isTaskHub ? "durabletask-hub-open-dashboard" : "durabletask-scheduler-open-dashboard", "Open Dashboard", async context => { @@ -432,7 +432,7 @@ static IResourceBuilder WithOpenDashboardCommand(this IResourceBuilder { Description = "Open the Durable Task Scheduler Dashboard", IconName = "GlobeArrowForward", - IsHighlighted = builder.Resource.IsTaskHub, + IsHighlighted = isTaskHub, }); } diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs index 29282546124..d41e91ca232 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs @@ -8,6 +8,4 @@ namespace Aspire.Hosting.Azure; internal interface IResourceWithDashboard : IResource { ReferenceExpression DashboardEndpointExpression { get; } - - bool IsTaskHub => false; } From a7e6a23fd0458b341f74e8901b697c9bcdfaeeee Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 29 May 2025 21:30:00 -0700 Subject: [PATCH 12/13] Rename dashboard interface. Signed-off-by: Phillip Hoff --- .../DurableTask/DurableTaskHubResource.cs | 8 +++----- .../DurableTask/DurableTaskSchedulerExtensions.cs | 2 +- .../DurableTask/DurableTaskSchedulerResource.cs | 4 ++-- ...board.cs => IDurableTaskResourceWithDashboard.cs} | 2 +- .../AzureFunctionsDurableTaskTests.cs | 12 ++++++------ 5 files changed, 13 insertions(+), 15 deletions(-) rename src/Aspire.Hosting.Azure.Functions/DurableTask/{IResourceWithDashboard.cs => IDurableTaskResourceWithDashboard.cs} (80%) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs index acc7ac454ee..7a89fdc4cd4 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskHubResource.cs @@ -11,7 +11,7 @@ namespace Aspire.Hosting.Azure; /// 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, IResourceWithDashboard + : Resource(name), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithParent, IDurableTaskResourceWithDashboard { /// public ReferenceExpression ConnectionStringExpression => @@ -25,14 +25,12 @@ public class DurableTaskHubResource(string name, DurableTaskSchedulerResource pa /// public string? TaskHubName { get; set; } - ReferenceExpression IResourceWithDashboard.DashboardEndpointExpression => + ReferenceExpression IDurableTaskResourceWithDashboard.DashboardEndpointExpression => this.GetDashboardEndpoint(); internal ReferenceExpression TaskHubNameExpression => ReferenceExpression.Create($"{this.ResolveTaskHubName()}"); - bool IResourceWithDashboard.IsTaskHub => true; - ReferenceExpression GetDashboardEndpoint() { var defaultValue = ReferenceExpression.Create($"default"); @@ -60,7 +58,7 @@ ReferenceExpression ResolveDashboardEndpoint() return annotation.DashboardEndpoint; } - return (this.Parent as IResourceWithDashboard).DashboardEndpointExpression; + return (this.Parent as IDurableTaskResourceWithDashboard).DashboardEndpointExpression; } ReferenceExpression? ResolveSubscriptionId() diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs index bc82d7bf9b4..74668232d05 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -413,7 +413,7 @@ public static IResourceBuilder WithDynamic return builder; } - static IResourceBuilder WithOpenDashboardCommand(this IResourceBuilder builder, bool isTaskHub) where T : IResourceWithDashboard + static IResourceBuilder WithOpenDashboardCommand(this IResourceBuilder builder, bool isTaskHub) where T : IDurableTaskResourceWithDashboard { if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) { diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs index 8aa75cf6963..1e9b8c8ac3d 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerResource.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting.Azure; /// /// The name of the resource. public sealed class DurableTaskSchedulerResource(string name) - : Resource(name), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithDashboard + : Resource(name), IResourceWithConnectionString, IResourceWithEndpoints, IDurableTaskResourceWithDashboard { EndpointReference EmulatorDashboardEndpoint => new(this, DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard); EndpointReference EmulatorSchedulerEndpoint => new(this, DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker); @@ -48,7 +48,7 @@ public sealed class DurableTaskSchedulerResource(string name) /// public string? SchedulerName { get; set; } - ReferenceExpression IResourceWithDashboard.DashboardEndpointExpression => + ReferenceExpression IDurableTaskResourceWithDashboard.DashboardEndpointExpression => this.ResolveDashboardEndpoint(); internal ReferenceExpression DashboardSchedulerEndpointExpression => diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/IDurableTaskResourceWithDashboard.cs similarity index 80% rename from src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs rename to src/Aspire.Hosting.Azure.Functions/DurableTask/IDurableTaskResourceWithDashboard.cs index d41e91ca232..5e586601c5f 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/IResourceWithDashboard.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/IDurableTaskResourceWithDashboard.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting.Azure; -internal interface IResourceWithDashboard : IResource +internal interface IDurableTaskResourceWithDashboard : IResource { ReferenceExpression DashboardEndpointExpression { get; } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs index 47ae55f8d01..8047cb72740 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsDurableTaskTests.cs @@ -35,7 +35,7 @@ public async Task AddDurableTaskScheduler() Assert.Null(scheduler.SubscriptionIdExpression); - Assert.Equal(DurableTaskConstants.Scheduler.Dashboard.Endpoint.ToString(), await (scheduler as IResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal(DurableTaskConstants.Scheduler.Dashboard.Endpoint.ToString(), await (scheduler as IDurableTaskResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); await Assert.ThrowsAsync(async () => await scheduler.DashboardSchedulerEndpointExpression.GetValueAsync(CancellationToken.None)); Assert.Equal("scheduler", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); } @@ -72,7 +72,7 @@ public async Task AddDurableTaskSchedulerWithConfiguration() Assert.Null(scheduler.SubscriptionIdExpression); - Assert.Equal("https://dashboard.durabletask.io/".ToString(), await (scheduler as IResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("https://dashboard.durabletask.io/".ToString(), await (scheduler as IDurableTaskResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); Assert.Equal("https://scheduler.test.io/", scheduler.DashboardSchedulerEndpointExpression.ValueExpression); Assert.Equal("TestSchedulerName", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); } @@ -103,7 +103,7 @@ public async Task AddDurableTaskSchedulerAsExisting() Assert.Null(scheduler.SubscriptionIdExpression); - Assert.Equal("https://dashboard.durabletask.io/".ToString(), await (scheduler as IResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); + Assert.Equal("https://dashboard.durabletask.io/".ToString(), await (scheduler as IDurableTaskResourceWithDashboard).DashboardEndpointExpression.GetValueAsync(CancellationToken.None)); Assert.Equal("https://scheduler.test.io/", scheduler.DashboardSchedulerEndpointExpression.ValueExpression); Assert.Equal("scheduler", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); } @@ -134,7 +134,7 @@ public async Task AddDurableTaskSchedulerAsEmulator() Assert.Null(scheduler.SubscriptionIdExpression); - Assert.Equal("{scheduler.bindings.dashboard.url}/", (scheduler as IResourceWithDashboard).DashboardEndpointExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.dashboard.url}/", (scheduler as IDurableTaskResourceWithDashboard).DashboardEndpointExpression.ValueExpression); Assert.Equal("{scheduler.bindings.dashboard.url}/api/", scheduler.DashboardSchedulerEndpointExpression.ValueExpression); Assert.Equal("default", await scheduler.SchedulerNameExpression.GetValueAsync(CancellationToken.None)); @@ -229,12 +229,12 @@ public async Task AddDurableTaskSchedulerAsEmulatorWithTaskhub() var taskHub1 = model.Resources.OfType().Single(x => x.Name == "taskhub1"); Assert.Equal("Endpoint={scheduler.bindings.worker.url}/;Authentication=None;TaskHub=taskhub1", taskHub1.ConnectionStringExpression.ValueExpression); - Assert.Equal("{scheduler.bindings.dashboard.url}/subscriptions/default/schedulers/default/taskhubs/taskhub1", (taskHub1 as IResourceWithDashboard).DashboardEndpointExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.dashboard.url}/subscriptions/default/schedulers/default/taskhubs/taskhub1", (taskHub1 as IDurableTaskResourceWithDashboard).DashboardEndpointExpression.ValueExpression); var taskHub2 = model.Resources.OfType().Single(x => x.Name == "taskhub2"); Assert.Equal("Endpoint={scheduler.bindings.worker.url}/;Authentication=None;TaskHub=taskhub2a", taskHub2.ConnectionStringExpression.ValueExpression); - Assert.Equal("{scheduler.bindings.dashboard.url}/subscriptions/default/schedulers/default/taskhubs/taskhub2a", (taskHub2 as IResourceWithDashboard).DashboardEndpointExpression.ValueExpression); + Assert.Equal("{scheduler.bindings.dashboard.url}/subscriptions/default/schedulers/default/taskhubs/taskhub2a", (taskHub2 as IDurableTaskResourceWithDashboard).DashboardEndpointExpression.ValueExpression); } [RequiresDocker] From 76c69414af1c1f8bc44c3c98b32e0daebe13ed21 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Thu, 29 May 2025 22:00:19 -0700 Subject: [PATCH 13/13] Use simpler form of WithHttpEndpoint(). Signed-off-by: Phillip Hoff --- .../DurableTask/DurableTaskSchedulerExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs index 74668232d05..c9461c25d85 100644 --- a/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/DurableTask/DurableTaskSchedulerExtensions.cs @@ -82,8 +82,8 @@ public static IResourceBuilder RunAsEmulator(this } var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(new DurableTaskSchedulerEmulatorResource(builder.Resource)) - .WithEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker, scheme: "http", targetPort: 8080) - .WithEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard, scheme: "http", targetPort: 8082) + .WithHttpEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Worker, targetPort: 8080) + .WithHttpEndpoint(name: DurableTaskConstants.Scheduler.Emulator.Endpoints.Dashboard, targetPort: 8082) .WithEnvironment( async (EnvironmentCallbackContext context) => {