---
name: durable-functions-dotnet
description: 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.
---
# 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
```xml
```
### host.json Configuration (Durable Task Scheduler)
```json
{
"version": "2.0",
"extensions": {
"durableTask": {
"storageProvider": {
"type": "azureManaged",
"connectionStringName": "DTS_CONNECTION_STRING"
},
"hubName": "%TASKHUB_NAME%"
}
}
}
```
### local.settings.json
```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)
```csharp
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 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 MyOrchestration(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration));
logger.LogInformation("Starting orchestration");
var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo");
var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle");
var result3 = await context.CallActivityAsync(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
```csharp
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.Build();
await host.RunAsync();
```
## 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 |
| **Async HTTP APIs** | Long-running operations with HTTP status polling |
| **Monitor** | Periodic polling with configurable timeouts |
| **Human Interaction** | Workflow pauses for external input/approval |
| **Aggregator (Entities)** | Stateful objects with operations (counters, accounts) |
See [references/patterns.md](references/patterns.md) for detailed implementations.
## Two Approaches: Function-Based vs Class-Based
### Function-Based (Default)
```csharp
[Function(nameof(MyOrchestration))]
public static async Task MyOrchestration(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string input = context.GetInput()!;
return await context.CallActivityAsync(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:
```csharp
[DurableTask(nameof(MyOrchestration))]
public class MyOrchestration : TaskOrchestrator
{
public override async Task RunAsync(TaskOrchestrationContext context, string input)
{
ILogger logger = context.CreateReplaySafeLogger();
return await context.CallActivityAsync(nameof(MyActivity), input);
}
}
[DurableTask(nameof(MyActivity))]
public class MyActivity : TaskActivity
{
private readonly ILogger _logger;
// Activities support DI - orchestrations do NOT
public MyActivity(ILogger logger)
{
_logger = logger;
}
public override Task 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()`
- `context.CallSubOrchestrationAsync()`
- `context.CallHttpAsync()`
- `context.CreateTimer()`
- `context.WaitForExternalEvent()`
- `context.CurrentUtcDateTime`
- `context.NewGuid()`
- `context.SetCustomStatus()`
- `context.CreateReplaySafeLogger()`
### Non-Determinism Patterns (WRONG vs CORRECT)
#### Getting Current Time
```csharp
// 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
```csharp
// WRONG - Guid.NewGuid() generates different value on replay
[Function(nameof(BadOrchestration))]
public static async Task 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 GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
string orderId = context.NewGuid().ToString(); // Deterministic
await context.CallActivityAsync(nameof(CreateOrder), orderId);
return orderId;
}
```
#### Random Numbers
```csharp
// 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(nameof(GetRandomDelay), null);
await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None);
}
```
#### Sleeping/Delays
```csharp
// 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
```csharp
// WRONG - HttpClient in orchestrator is non-deterministic
[Function(nameof(BadOrchestration))]
public static async Task 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 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 FetchData([ActivityTrigger] string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // OK in activity
}
[Function(nameof(GoodOrchestration2))]
public static async Task GoodOrchestration2([OrchestrationTrigger] TaskOrchestrationContext context)
{
return await context.CallActivityAsync(nameof(FetchData), "https://api.example.com/data");
}
```
#### Database Access
```csharp
// 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 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 GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
string userId = context.GetInput()!;
return await context.CallActivityAsync(nameof(GetUser), userId);
}
```
#### Environment Variables
```csharp
// 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()!;
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
```csharp
// WRONG - Dictionary iteration order may vary
[Function(nameof(BadOrchestration))]
public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context)
{
var items = context.GetInput>()!;
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>()!;
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:
```csharp
[Function(nameof(MyOrchestration))]
public static async Task 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(nameof(MyActivity), "input");
logger.LogInformation("Activity completed with result: {Result}", result);
return result;
}
```
### Error Handling
```csharp
[Function(nameof(OrchestrationWithErrorHandling))]
public static async Task OrchestrationWithErrorHandling(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
string input = context.GetInput()!;
try
{
return await context.CallActivityAsync(nameof(RiskyActivity), input);
}
catch (TaskFailedException ex)
{
// Activity failed - implement compensation
context.SetCustomStatus(new { Error = ex.Message });
return await context.CallActivityAsync(nameof(CompensationActivity), input);
}
}
```
### Retry Policies
```csharp
var options = new TaskOptions
{
Retry = new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5),
backoffCoefficient: 2.0,
maxRetryInterval: TimeSpan.FromMinutes(1))
};
await context.CallActivityAsync(nameof(UnreliableActivity), input, options);
```
## HTTP Management APIs
Durable Functions exposes built-in HTTP APIs for orchestration management:
### CreateCheckStatusResponse
```csharp
[Function("HttpStart")]
public static async Task 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
```csharp
[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
```csharp
// 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="
```
Note: Durable Task Scheduler supports identity-based authentication only - no connection strings with keys.
## Local Development with Emulator
```bash
# 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):
```csharp
[Function(nameof(CallExternalApi))]
public static async Task 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](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations, Monitor)
- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning, deployment, and project templates