Skip to content

Initial integration of Durable Task Scheduler (i.e. emulator) #9294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@
<Folder Name="/playground/dockerfile/">
<Project Path="playground/withdockerfile/WithDockerfile.AppHost/WithDockerfile.AppHost.csproj" />
</Folder>
<Folder Name="/playground/DurableTask/">
<Project Path="playground/DurableTask/DurableTask.Scheduler.AppHost/DurableTask.Scheduler.AppHost.csproj" />
<Project Path="playground/DurableTask/DurableTask.Scheduler.ExternalAppHost/DurableTask.Scheduler.ExternalAppHost.csproj" />
<Project Path="playground/DurableTask/DurableTask.Scheduler.WebApi/DurableTask.Scheduler.WebApi.csproj" />
<Project Path="playground/DurableTask/DurableTask.Scheduler.Worker/DurableTask.Scheduler.Worker.csproj" />
</Folder>
<Folder Name="/playground/EventHubs/">
<Project Path="playground/AspireEventHub/EventHubs.AppHost/EventHubs.AppHost.csproj" />
<Project Path="playground/AspireEventHub/EventHubsApi/EventHubsApi.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
<PackageVersion Include="KubernetesClient" Version="16.0.7" />
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.10.0-preview.1" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.10.0-preview.1" />
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.11.9" />
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.11.9" />
<PackageVersion Include="Milvus.Client" Version="2.3.0-preview.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\..\KnownResourceNames.cs" Link="KnownResourceNames.cs" />
</ItemGroup>

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.Functions" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DurableTask.Scheduler.WebApi\DurableTask.Scheduler.WebApi.csproj" />
<ProjectReference Include="..\DurableTask.Scheduler.Worker\DurableTask.Scheduler.Worker.csproj" />
</ItemGroup>

</Project>
23 changes: 23 additions & 0 deletions playground/DurableTask/DurableTask.Scheduler.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Aspire.Hosting.Azure;

var builder = DistributedApplication.CreateBuilder(args);

var scheduler =
builder.AddDurableTaskScheduler("scheduler")
.RunAsEmulator(
options =>
{
options.WithDynamicTaskHubs();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this explicit call needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a demonstration of configuring options on the emulator. (Not every Aspire application will want to use dynamic task hub names.)

});

var taskHub = scheduler.AddTaskHub("taskhub");

var webApi =
builder.AddProject<Projects.DurableTask_Scheduler_WebApi>("webapi")
.WithReference(taskHub);

builder.AddProject<Projects.DurableTask_Scheduler_Worker>("worker")
.WithReference(webApi)
.WithReference(taskHub);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\..\KnownResourceNames.cs" Link="KnownResourceNames.cs" />
</ItemGroup>

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Azure.Functions" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DurableTask.Scheduler.WebApi\DurableTask.Scheduler.WebApi.csproj" />
<ProjectReference Include="..\DurableTask.Scheduler.Worker\DurableTask.Scheduler.Worker.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Aspire.Hosting.Azure;

var builder = DistributedApplication.CreateBuilder(args);

var scheduler =
builder.AddDurableTaskScheduler("scheduler")
.RunAsExisting(builder.AddParameter("scheduler-connection-string"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this in the sample?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent was to have examples that demonstrate the two main DTS scenarios: use of the DTS emulator and use of an existing DTS instance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm having a hard time understanding is the deployment story here. Is this an azure resource as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, DTS is an Azure resource (though I'm not intending to support deployment in this initial pass). I did a little experimenting with the Bicep base resource type that other Azure resources are built upon, but they rely on Azure provisioning libraries that do not exist, yet, for DTS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this model is going to be:
AddAzureDurableTaskScheudler().RunAsEmulator() yes?


var taskHub =
scheduler.AddTaskHub("taskhub")
.WithTaskHubName(builder.AddParameter("taskhub-name"));

var webApi =
builder.AddProject<Projects.DurableTask_Scheduler_WebApi>("webapi")
.WithReference(taskHub);

builder.AddProject<Projects.DurableTask_Scheduler_Worker>("worker")
.WithReference(webApi)
.WithReference(taskHub);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
},
"Parameters": {
"scheduler-connection-string": "<connection string>",
"taskhub-name": "<name>"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@HostAddress = http://localhost:5142

POST {{HostAddress}}/create
Content-Type: application/json

{
"text": "hello world"
}

###

47 changes: 47 additions & 0 deletions playground/DurableTask/DurableTask.Scheduler.WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
});
Comment on lines +13 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we build a client integration?

Copy link
Contributor Author

@philliphoff philliphoff May 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...yes? It's complicated because consuming applications can be either DTS "clients" or "workers" (and sometimes both), and there are separate SDKs for each which means multiple client integrations. Also, there are two sets of SDKs that apps can use (and you might consider another scenario a third) which further expands the matrix.

I'd say we should start with client integrations for the "modern" SDK, worker first and then client, as the former is the most common. Then, if there's demand, look at integrations for the "older" SDKs. That said, any client integrations would be follow up PRs.

});

var app = builder.Build();

app.MapPost("/create", async (EchoValue value, DurableTaskClient durableTaskClient) =>
{
string instanceId = await durableTaskClient.ScheduleNewOrchestrationInstanceAsync(
"Echo",
value);

await durableTaskClient.WaitForInstanceCompletionAsync(instanceId);

return Results.Ok();
})
.WithName("CreateOrchestration");

app.MapPost("/echo", ([FromBody] EchoValue value) =>
{
return new EchoValue { Text = $"Echoed: {value.Text}" };
})
.WithName("EchoText");

app.Run();

public record EchoValue
{
[JsonPropertyName("text")]
public required string Text { get; init; }
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
</ItemGroup>
</Project>
35 changes: 35 additions & 0 deletions playground/DurableTask/DurableTask.Scheduler.Worker/Program.cs
Original file line number Diff line number Diff line change
@@ -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>("EchoActivity");
r.AddOrchestrator<EchoOrchestrator>("Echo");
});
workerBuilder.UseDurableTaskScheduler(
builder.Configuration.GetConnectionString("taskhub") ?? throw new InvalidOperationException("Scheduler connection string not configured."),
options =>
{
options.AllowInsecureCredentials = true;
});
});

var host = builder.Build();

host.Run();
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Net.Http.Json;
using Microsoft.DurableTask;

namespace DurableTask.Scheduler.Worker.Tasks.Echo;

sealed class EchoActivity(IHttpClientFactory clientFactory) : TaskActivity<string, string>
{
public override async Task<string> RunAsync(TaskActivityContext context, string input)
{
HttpClient client = clientFactory.CreateClient("Echo");

var result = await client.PostAsync("/echo", JsonContent.Create(new EchoInput { Text = input }));

var output = await result.Content.ReadFromJsonAsync<EchoInput>();

return output?.Text ?? throw new InvalidOperationException("Invalid response from echo service!");
}
}
Loading