askill
durable-functions-dotnet

durable-functions-dotnetSafety 100Repository

Build durable, fault-tolerant workflows using Azure Durable Functions with .NET isolated worker and Durable Task Scheduler backend. Use when creating serverless orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, async HTTP APIs, human interaction, monitoring, or stateful aggregators. Applies to Azure Functions apps requiring durable execution, state persistence, or distributed coordination with built-in HTTP management APIs and Azure integration.

46 stars
1.2k downloads
Updated 2/5/2026

Package Files

Loading files...
SKILL.md

Azure Durable Functions (.NET Isolated) with Durable Task Scheduler

Build fault-tolerant, stateful serverless workflows using Azure Durable Functions connected to Azure Durable Task Scheduler.

Quick Start

Required NuGet Packages

<ItemGroup>
  <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.*" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.*" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.*" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.*" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.*" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="*" />
  <PackageReference Include="Azure.Identity" Version="1.*" />
</ItemGroup>

host.json Configuration (Durable Task Scheduler)

{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "storageProvider": {
        "type": "azureManaged",
        "connectionStringName": "DTS_CONNECTION_STRING"
      },
      "hubName": "%TASKHUB_NAME%"
    }
  }
}

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None",
    "TASKHUB_NAME": "default"
  }
}

Minimal Example (Function-Based)

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;

public static class DurableFunctionsApp
{
    // HTTP Starter - triggers orchestration
    [Function("HttpStart")]
    public static async Task<HttpResponseData> HttpStart(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req,
        [DurableClient] DurableTaskClient client,
        string functionName,
        FunctionContext executionContext)
    {
        string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(functionName);
        
        var logger = executionContext.GetLogger("HttpStart");
        logger.LogInformation("Started orchestration with ID = '{instanceId}'", instanceId);
        
        return await client.CreateCheckStatusResponseAsync(req, instanceId);
    }

    // Orchestrator function
    [Function(nameof(MyOrchestration))]
    public static async Task<string> MyOrchestration(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration));
        logger.LogInformation("Starting orchestration");
        
        var result1 = await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo");
        var result2 = await context.CallActivityAsync<string>(nameof(SayHello), "Seattle");
        var result3 = await context.CallActivityAsync<string>(nameof(SayHello), "London");
        
        return $"{result1}, {result2}, {result3}";
    }

    // Activity function
    [Function(nameof(SayHello))]
    public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
    {
        var logger = executionContext.GetLogger(nameof(SayHello));
        logger.LogInformation("Saying hello to {name}", name);
        return $"Hello {name}!";
    }
}

Program.cs Setup

using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .Build();

await host.RunAsync();

Pattern Selection Guide

PatternUse When
Function ChainingSequential steps where each depends on the previous
Fan-Out/Fan-InParallel processing with aggregated results
Async HTTP APIsLong-running operations with HTTP status polling
MonitorPeriodic polling with configurable timeouts
Human InteractionWorkflow pauses for external input/approval
Aggregator (Entities)Stateful objects with operations (counters, accounts)

See references/patterns.md for detailed implementations.

Two Approaches: Function-Based vs Class-Based

Function-Based (Default)

[Function(nameof(MyOrchestration))]
public static async Task<string> MyOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    string input = context.GetInput<string>()!;
    return await context.CallActivityAsync<string>(nameof(MyActivity), input);
}

[Function(nameof(MyActivity))]
public static string MyActivity([ActivityTrigger] string input)
{
    return $"Processed: {input}";
}

Class-Based (With Source Generator)

Requires Microsoft.DurableTask.Generators package:

[DurableTask(nameof(MyOrchestration))]
public class MyOrchestration : TaskOrchestrator<string, string>
{
    public override async Task<string> RunAsync(TaskOrchestrationContext context, string input)
    {
        ILogger logger = context.CreateReplaySafeLogger<MyOrchestration>();
        return await context.CallActivityAsync<string>(nameof(MyActivity), input);
    }
}

[DurableTask(nameof(MyActivity))]
public class MyActivity : TaskActivity<string, string>
{
    private readonly ILogger<MyActivity> _logger;
    
    // Activities support DI - orchestrations do NOT
    public MyActivity(ILogger<MyActivity> logger)
    {
        _logger = logger;
    }

    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. When an orchestration resumes, it replays all previous code to rebuild state. Non-deterministic code produces different results on replay, causing NonDeterministicOrchestrationException.

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
  • Thread.Sleep(), Task.Delay() → Use context.CreateTimer()
  • Non-deterministic LINQ (parallel, unordered)
  • Task.Run(), ConfigureAwait(false)
  • Static mutable variables
  • Environment variables that may change → Pass as input or use activities

ALWAYS safe:

  • context.CallActivityAsync<T>()
  • context.CallSubOrchestrationAsync<T>()
  • context.CallHttpAsync()
  • context.CreateTimer()
  • context.WaitForExternalEvent<T>()
  • context.CurrentUtcDateTime
  • context.NewGuid()
  • context.SetCustomStatus()
  • context.CreateReplaySafeLogger()

Non-Determinism Patterns (WRONG vs CORRECT)

Getting Current Time

// WRONG - DateTime.UtcNow returns different value on replay
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    DateTime currentTime = DateTime.UtcNow;  // Non-deterministic!
    if (currentTime.Hour < 12)
    {
        await context.CallActivityAsync(nameof(MorningActivity), null);
    }
}

// CORRECT - context.CurrentUtcDateTime replays consistently
[Function(nameof(GoodOrchestration))]
public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    DateTime currentTime = context.CurrentUtcDateTime;  // Deterministic
    if (currentTime.Hour < 12)
    {
        await context.CallActivityAsync(nameof(MorningActivity), null);
    }
}

Generating GUIDs

// WRONG - Guid.NewGuid() generates different value on replay
[Function(nameof(BadOrchestration))]
public static async Task<string> BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    string orderId = Guid.NewGuid().ToString();  // Non-deterministic!
    await context.CallActivityAsync(nameof(CreateOrder), orderId);
    return orderId;
}

// CORRECT - context.NewGuid() replays the same value
[Function(nameof(GoodOrchestration))]
public static async Task<string> GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    string orderId = context.NewGuid().ToString();  // Deterministic
    await context.CallActivityAsync(nameof(CreateOrder), orderId);
    return orderId;
}

Random Numbers

// WRONG - Random produces different values on replay
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    int delay = new Random().Next(1, 10);  // Non-deterministic!
    await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None);
}

// CORRECT - generate random in activity, pass to orchestrator
[Function(nameof(GetRandomDelay))]
public static int GetRandomDelay([ActivityTrigger] object? input)
{
    return new Random().Next(1, 10);  // OK in activity
}

[Function(nameof(GoodOrchestration))]
public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    int delay = await context.CallActivityAsync<int>(nameof(GetRandomDelay), null);
    await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None);
}

Sleeping/Delays

// WRONG - Thread.Sleep/Task.Delay don't persist and block
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    await context.CallActivityAsync(nameof(Step1), null);
    await Task.Delay(60000);  // Non-durable! Lost on restart, wastes resources
    await context.CallActivityAsync(nameof(Step2), null);
}

// CORRECT - context.CreateTimer is durable
[Function(nameof(GoodOrchestration))]
public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    await context.CallActivityAsync(nameof(Step1), null);
    await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), CancellationToken.None);  // Durable
    await context.CallActivityAsync(nameof(Step2), null);
}

HTTP Calls and I/O

// WRONG - HttpClient in orchestrator is non-deterministic
[Function(nameof(BadOrchestration))]
public static async Task<string> BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    using var client = new HttpClient();
    var response = await client.GetStringAsync("https://api.example.com/data");  // Non-deterministic!
    return response;
}

// CORRECT Option 1 - use CallHttpAsync (built-in durable HTTP)
[Function(nameof(GoodOrchestration1))]
public static async Task<string> GoodOrchestration1([OrchestrationTrigger] TaskOrchestrationContext context)
{
    DurableHttpResponse response = await context.CallHttpAsync(
        HttpMethod.Get, new Uri("https://api.example.com/data"));  // Deterministic
    return response.Content;
}

// CORRECT Option 2 - move I/O to activity
[Function(nameof(FetchData))]
public static async Task<string> FetchData([ActivityTrigger] string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);  // OK in activity
}

[Function(nameof(GoodOrchestration2))]
public static async Task<string> GoodOrchestration2([OrchestrationTrigger] TaskOrchestrationContext context)
{
    return await context.CallActivityAsync<string>(nameof(FetchData), "https://api.example.com/data");
}

Database Access

// WRONG - database query in orchestrator
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    using var conn = new SqlConnection(connectionString);  // Non-deterministic!
    await conn.OpenAsync();
    // ...
}

// CORRECT - database access in activity
[Function(nameof(GetUser))]
public static async Task<User> GetUser([ActivityTrigger] string userId)
{
    using var conn = new SqlConnection(connectionString);  // OK in activity
    await conn.OpenAsync();
    // ...
    return user;
}

[Function(nameof(GoodOrchestration))]
public static async Task<User> GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    string userId = context.GetInput<string>()!;
    return await context.CallActivityAsync<User>(nameof(GetUser), userId);
}

Environment Variables

// WRONG - env var might change between replays
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    string apiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT")!;  // Could change!
    await context.CallActivityAsync(nameof(CallApi), apiEndpoint);
}

// CORRECT - pass config as input
[Function(nameof(GoodOrchestration))]
public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var config = context.GetInput<WorkflowConfig>()!;
    string apiEndpoint = config.ApiEndpoint;  // From input, deterministic
    await context.CallActivityAsync(nameof(CallApi), apiEndpoint);
}

// ALSO CORRECT - read env var in activity
[Function(nameof(CallApi))]
public static async Task CallApi([ActivityTrigger] object? input)
{
    string apiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT")!;  // OK in activity
    // make the call...
}

Collection Iteration Order

// WRONG - Dictionary iteration order may vary
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var items = context.GetInput<Dictionary<string, object>>()!;
    foreach (var key in items.Keys)  // Order not guaranteed!
    {
        await context.CallActivityAsync(nameof(Process), key);
    }
}

// CORRECT - use sorted keys for deterministic order
[Function(nameof(GoodOrchestration))]
public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var items = context.GetInput<Dictionary<string, object>>()!;
    foreach (var key in items.Keys.OrderBy(k => k))  // Guaranteed order
    {
        await context.CallActivityAsync(nameof(Process), key);
    }
}

Logging in Orchestrations

Use CreateReplaySafeLogger to avoid duplicate log entries during replay:

[Function(nameof(MyOrchestration))]
public static async Task<string> MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
    ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration));
    logger.LogInformation("Orchestration started");  // Only logs once, not on each replay
    
    var result = await context.CallActivityAsync<string>(nameof(MyActivity), "input");
    
    logger.LogInformation("Activity completed with result: {Result}", result);
    return result;
}

Error Handling

[Function(nameof(OrchestrationWithErrorHandling))]
public static async Task<string> OrchestrationWithErrorHandling(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    string input = context.GetInput<string>()!;
    try
    {
        return await context.CallActivityAsync<string>(nameof(RiskyActivity), input);
    }
    catch (TaskFailedException ex)
    {
        // Activity failed - implement compensation
        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);

HTTP Management APIs

Durable Functions exposes built-in HTTP APIs for orchestration management:

CreateCheckStatusResponse

[Function("HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    string functionName)
{
    // Parse input from request body
    string? input = await new StreamReader(req.Body).ReadToEndAsync();
    
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(functionName, input);
    
    // Returns 202 Accepted with management URLs in response
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

Response includes:

  • statusQueryGetUri - GET endpoint to check status
  • sendEventPostUri - POST endpoint to raise events
  • terminatePostUri - POST endpoint to terminate
  • purgeHistoryDeleteUri - DELETE endpoint to purge history

Client Operations

[DurableClient] DurableTaskClient client

// Schedule new orchestration
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input);

// Schedule with custom instance ID
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
    "MyOrchestration", input, new StartOrchestrationOptions { InstanceId = "my-custom-id" });

// Get status
OrchestrationMetadata? state = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true);

// Wait for completion
OrchestrationMetadata? result = await client.WaitForInstanceCompletionAsync(
    instanceId, getInputsAndOutputs: true, cancellationToken);

// Raise external event
await client.RaiseEventAsync(instanceId, "ApprovalEvent", approvalData);

// Terminate
await client.TerminateInstanceAsync(instanceId, "User cancelled");

// Suspend/Resume
await client.SuspendInstanceAsync(instanceId, "Pausing for maintenance");
await client.ResumeInstanceAsync(instanceId, "Resuming operation");

// Purge history
await client.PurgeInstanceAsync(instanceId);

Connection & Authentication

Connection String Formats

// Local emulator (no auth)
"Endpoint=http://localhost:8080;Authentication=None"

// Azure with Managed Identity (recommended for production)
"Endpoint=https://my-scheduler.region.durabletask.io;Authentication=ManagedIdentity"

// Azure with specific client ID (user-assigned managed identity)
"Endpoint=https://my-scheduler.region.durabletask.io;Authentication=ManagedIdentity;ClientId=<client-id>"

Note: Durable Task Scheduler supports identity-based authentication only - no connection strings with keys.

Local Development with Emulator

# Start Azurite (required for Azure Functions)
azurite start

# Pull and run the Durable Task Scheduler 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

# Start the function app
func start

Durable HTTP Calls

Make HTTP calls directly from orchestrations (persisted and replay-safe):

[Function(nameof(CallExternalApi))]
public static async Task<string> CallExternalApi([OrchestrationTrigger] TaskOrchestrationContext context)
{
    // Simple GET
    DurableHttpResponse response = await context.CallHttpAsync(HttpMethod.Get, new Uri("https://api.example.com/data"));
    
    if (response.StatusCode != HttpStatusCode.OK)
    {
        throw new Exception($"API call failed: {response.StatusCode}");
    }
    
    return response.Content;
}

// With headers and body
var request = new DurableHttpRequest(
    HttpMethod.Post,
    new Uri("https://api.example.com/data"))
{
    Headers = { ["Content-Type"] = "application/json" },
    Content = JsonSerializer.Serialize(payload)
};

DurableHttpResponse response = await context.CallHttpAsync(request);

// With managed identity authentication
var request = new DurableHttpRequest(
    HttpMethod.Get,
    new Uri("https://management.azure.com/..."))
{
    TokenSource = new ManagedIdentityTokenSource("https://management.azure.com/.default")
};

References

  • patterns.md - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations, Monitor)
  • setup.md - Azure Durable Task Scheduler provisioning, deployment, and project templates

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/6/2026

An exceptionally high-quality technical guide for Azure Durable Functions in .NET. It provides comprehensive setup instructions, pattern guides, and critical safety rules regarding determinism with clear 'Wrong vs Correct' examples.

100
100
100
90
100

Metadata

Licenseunknown
Version-
Updated2/5/2026
PublisherAzure-Samples

Tags

apici-cddatabasegithub-actionsobservabilitysecurity