Durable Task .NET SDK with Durable Task Scheduler
Build fault-tolerant, stateful workflows in .NET applications using the Durable Task SDK connected to Azure Durable Task Scheduler.
Quick Start
Required NuGet Packages
<ItemGroup> <PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" Version="1." /> <PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1." /> <PackageReference Include="Microsoft.DurableTask.Generators" Version="1." OutputItemType="Analyzer" /> <PackageReference Include="Azure.Identity" Version="1." /> <PackageReference Include="Grpc.Net.Client" Version="2.*" /> </ItemGroup>
Minimal Worker Setup
using Microsoft.DurableTask; using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Connection string format: "Endpoint={url};TaskHub={name};Authentication={type}" var connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
builder.Services.AddDurableTaskWorker() .AddTasks(registry => { registry.AddAllGeneratedTasks(); // Registers all [DurableTask] decorated classes }) .UseDurableTaskScheduler(connectionString);
var host = builder.Build(); await host.RunAsync();
Minimal Client Setup
using Microsoft.DurableTask.Client;
var connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();
// Schedule an orchestration string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input);
// Wait for completion var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
Pattern Selection Guide
Pattern Use When
Function Chaining Sequential steps where each depends on the previous
Fan-Out/Fan-In Parallel processing with aggregated results
Human Interaction Workflow pauses for external input/approval
Durable Entities Stateful objects with operations (counters, accounts)
Sub-Orchestrations Reusable workflow components or version isolation
See references/patterns.md for detailed implementations.
Orchestration Structure
Basic Orchestration
[DurableTask(nameof(MyOrchestration))] public class MyOrchestration : TaskOrchestrator<string, string> { public override async Task<string> RunAsync(TaskOrchestrationContext context, string input) { // Call activities var result1 = await context.CallActivityAsync<string>(nameof(Step1Activity), input); var result2 = await context.CallActivityAsync<string>(nameof(Step2Activity), result1); return result2; } }
Basic Activity
[DurableTask(nameof(MyActivity))] public class MyActivity : TaskActivity<string, string> { private readonly ILogger<MyActivity> _logger;
public MyActivity(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<MyActivity>();
}
public override Task<string> RunAsync(TaskActivityContext context, string input)
{
_logger.LogInformation("Processing: {Input}", input);
return Task.FromResult($"Processed: {input}");
}
}
Critical Rules
Orchestration Determinism
Orchestrations replay from history - all code MUST be deterministic:
NEVER do inside orchestrations:
-
DateTime.Now , DateTime.UtcNow → Use context.CurrentUtcDateTime
-
Guid.NewGuid() → Use context.NewGuid()
-
Random → Pass random values from activities
-
Direct I/O, HTTP calls, database access → Move to activities
-
Task.Delay() → Use context.CreateTimer()
-
Non-deterministic LINQ (parallel, unordered)
ALWAYS safe:
-
context.CallActivityAsync<T>()
-
context.CallSubOrchestrationAsync<T>()
-
context.CreateTimer()
-
context.WaitForExternalEvent<T>()
-
context.CurrentUtcDateTime
-
context.NewGuid()
-
context.SetCustomStatus()
Error Handling
public override async Task<string> RunAsync(TaskOrchestrationContext context, string input) { try { return await context.CallActivityAsync<string>(nameof(RiskyActivity), input); } catch (TaskFailedException ex) { // Activity failed - implement compensation or retry context.SetCustomStatus(new { Error = ex.Message }); return await context.CallActivityAsync<string>(nameof(CompensationActivity), input); } }
Retry Policies
var options = new TaskOptions { Retry = new RetryPolicy( maxNumberOfAttempts: 3, firstRetryInterval: TimeSpan.FromSeconds(5), backoffCoefficient: 2.0, maxRetryInterval: TimeSpan.FromMinutes(1)) };
await context.CallActivityAsync<string>(nameof(UnreliableActivity), input, options);
Connection & Authentication
Connection String Formats
// Local emulator (no auth) "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
// Azure with DefaultAzureCredential "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure"
// Azure with Managed Identity "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity"
// Azure with specific credential "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=AzureCLI"
Authentication Helper
static string GetConnectionString() { var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:8080"; var taskHub = Environment.GetEnvironmentVariable("TASKHUB") ?? "default";
var authType = endpoint.StartsWith("http://localhost") ? "None" : "DefaultAzure";
return $"Endpoint={endpoint};TaskHub={taskHub};Authentication={authType}";
}
Local Development with Emulator
Pull and run the emulator
docker pull mcr.microsoft.com/dts/dts-emulator:latest docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
Dashboard available at http://localhost:8082
References
-
patterns.md - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations)
-
setup.md - Azure Durable Task Scheduler provisioning and deployment