Skip to content

Latest commit

 

History

History
216 lines (166 loc) · 7.04 KB

File metadata and controls

216 lines (166 loc) · 7.04 KB
name durable-task-dotnet
description Build durable, fault-tolerant workflows in .NET using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or stateful agents. Applies to any .NET application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency.

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