Try Live
Add Docs
Rankings
Pricing
Docs
Install
Theme
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Durable Task .NET Client SDK
https://github.com/microsoft/durabletask-dotnet
Admin
The Durable Task .NET SDK is a standalone .NET library for implementing Durable Task orchestrations,
...
Tokens:
8,148
Snippets:
38
Trust Score:
9.5
Update:
5 months ago
Context
Skills
Chat
Benchmark
83.1
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Durable Task .NET Client SDK ## Introduction The Durable Task .NET Client SDK is a standalone library for implementing durable task orchestrations, activities, and entities in .NET applications. It provides a programming model for writing long-running, stateful workflows that can survive process failures and restarts by persisting execution state to durable storage. The SDK is designed to connect to a sidecar process or managed Azure endpoint, making it compatible with Azure Functions .NET Isolated worker processes and the Azure Durable Task Scheduler. The SDK follows a distributed application pattern where orchestrators coordinate workflow logic, activities perform the actual work, and entities maintain stateful operations. It supports both function-based and class-based programming models, with optional source generators for type-safe invocations. The framework handles automatic replay of orchestrations to rebuild state, provides deterministic execution guarantees, and includes features like retry policies, timers, external events, and sub-orchestrations for building complex distributed applications. ## APIs and Key Functions ### Starting an Orchestration - Function-Based Approach Basic HTTP-triggered function that schedules a new orchestration instance and returns status endpoints for monitoring. ```csharp using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.Extensions.Logging; [Function("StartHelloCitiesUntyped")] public static async Task<HttpResponseData> StartHelloCitiesUntyped( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, [DurableClient] DurableTaskClient client, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger("StartHelloCitiesUntyped"); // Schedule new orchestration with auto-generated instance ID string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("HelloCitiesUntyped"); logger.LogInformation("Created orchestration with instance ID = {instanceId}", instanceId); // Returns HTTP response with status query URLs return client.CreateCheckStatusResponse(req, instanceId); } // Example response includes URLs for: // - statusQueryGetUri: Check orchestration status // - sendEventPostUri: Send external events // - terminatePostUri: Terminate orchestration ``` ### Defining an Orchestrator - Sequential Activity Calls Orchestrator function that calls activities in sequence, maintaining deterministic execution across replays. ```csharp [Function("HelloCitiesUntyped")] public static async Task<string> HelloCitiesUntyped( [OrchestrationTrigger] TaskOrchestrationContext context) { // Call activities sequentially - each awaits before proceeding string result = ""; result += await context.CallActivityAsync<string>("SayHelloUntyped", "Tokyo") + " "; result += await context.CallActivityAsync<string>("SayHelloUntyped", "London") + " "; result += await context.CallActivityAsync<string>("SayHelloUntyped", "Seattle"); // Returns: "Hello, Tokyo! Hello, London! Hello, Seattle!" return result; } // IMPORTANT: Orchestrators must be deterministic - never use: // - DateTime.Now (use context.CurrentUtcDateTime instead) // - Random numbers (use context.NewGuid() instead) // - Non-durable async operations // - I/O operations (delegate to activities) ``` ### Defining an Activity - Work Execution Activity function that performs the actual work and can safely perform I/O operations, unlike orchestrators. ```csharp [Function("SayHelloUntyped")] public static string SayHelloUntyped( [ActivityTrigger] string cityName, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger("SayHelloUntyped"); logger.LogInformation("Saying hello to {name}", cityName); // Activities can: // - Make HTTP calls // - Query databases // - Perform CPU-intensive work // - Call external APIs return $"Hello, {cityName}!"; } // Activities guarantee at-least-once execution // Make activities idempotent when possible ``` ### Class-Based Orchestrator with Type Safety Class-based orchestrator using source generators for type-safe method invocations without string-based names. ```csharp using Microsoft.DurableTask; using Microsoft.DurableTask.Client; // Source generators create ScheduleNewHelloCitiesTypedInstanceAsync() extension method [DurableTask(nameof(HelloCitiesTyped))] public class HelloCitiesTyped : TaskOrchestrator<string?, string> { public async override Task<string> RunAsync( TaskOrchestrationContext context, string? input) { // Type-safe CallSayHelloTypedAsync() generated from activity class name string result = ""; result += await context.CallSayHelloTypedAsync("Tokyo") + " "; result += await context.CallSayHelloTypedAsync("London") + " "; result += await context.CallSayHelloTypedAsync("Seattle"); return result; } } // Starting the typed orchestrator public static async Task<HttpResponseData> StartHelloCitiesTyped( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, [DurableClient] DurableTaskClient client, FunctionContext executionContext) { // Type-safe extension method with IntelliSense support string instanceId = await client.ScheduleNewHelloCitiesTypedInstanceAsync(); return client.CreateCheckStatusResponse(req, instanceId); } ``` ### Class-Based Activity with Dependency Injection Activity class supporting constructor-based dependency injection for services like logging and database contexts. ```csharp [DurableTask(nameof(SayHelloTyped))] public class SayHelloTyped : TaskActivity<string, string> { readonly ILogger logger; // Constructor receives services from FunctionContext.InstanceServices public SayHelloTyped(ILogger<SayHelloTyped> logger) { this.logger = logger; } public override Task<string> RunAsync(TaskActivityContext context, string cityName) { this.logger.LogInformation("Saying hello to {name}", cityName); return Task.FromResult($"Hello, {cityName}!"); } } // Benefits: // - Full DI support (ILogger, IConfiguration, DbContext, etc.) // - Testable with mocked dependencies // - Instance created per execution ``` ### Sub-Orchestrations and Parallel Execution Recursive orchestrator demonstrating sub-orchestrations and fan-out/fan-in pattern for parallel task execution. ```csharp [Function("FibOrchestration")] public static async Task<int> FibOrchestration( [OrchestrationTrigger] TaskOrchestrationContext context) { int input = context.GetInput<int>()!; switch (input) { case 0 or 1: return await context.CallActivityAsync<int>("FibActivity", input); default: // Fan-out: Start both sub-orchestrations without awaiting Task<int> left = context.CallSubOrchestratorAsync<int>("FibOrchestration", input - 1); Task<int> right = context.CallSubOrchestratorAsync<int>("FibOrchestration", input - 2); // Fan-in: Wait for all to complete return (await left) + (await right); } } // Can also use Task.WhenAll for multiple parallel operations: // var tasks = new[] { task1, task2, task3 }; // await Task.WhenAll(tasks); ``` ### Durable Timers and External Events Creating durable timers and waiting for external events with timeout handling. ```csharp [Function("ApprovalOrchestrator")] public static async Task<string> ApprovalOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { // Wait for approval event with 30-minute timeout using CancellationTokenSource cts = new(); Task timeoutTask = context.CreateTimer(TimeSpan.FromMinutes(30), cts.Token); Task<bool> approvalTask = context.WaitForExternalEvent<bool>("Approval", cts.Token); Task winner = await Task.WhenAny(timeoutTask, approvalTask); if (winner == approvalTask) { cts.Cancel(); // Cancel timer bool approved = await approvalTask; return approved ? "Approved" : "Rejected"; } else { return "Timed out waiting for approval"; } } // Send event from client: // await client.RaiseEventAsync(instanceId, "Approval", true); ``` ### Retry Policies for Fault Tolerance Configuring automatic retry policies with exponential backoff for resilient task execution. ```csharp [Function("RobustOrchestrator")] public static async Task<string> RobustOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { var retryPolicy = new RetryPolicy( maxNumberOfAttempts: 5, firstRetryInterval: TimeSpan.FromSeconds(5), backoffCoefficient: 2.0, // Exponential: 5s, 10s, 20s, 40s, 80s maxRetryInterval: TimeSpan.FromMinutes(5), retryTimeout: TimeSpan.FromHours(1) ); var options = new TaskOptions { RetryPolicy = retryPolicy }; // Activity will automatically retry on failure according to policy string result = await context.CallActivityAsync<string>( "UnreliableActivity", "input", options); return result; } // Conditional retry based on exception type: var retryWithHandler = new RetryPolicy(3, TimeSpan.FromSeconds(1)) { HandleFailure = (TaskFailureDetails details) => { // Retry only on specific errors return details.ErrorType == "TransientException"; } }; ``` ### Durable Entities - Stateful Operations Entity pattern for maintaining stateful operations with concurrent access control. ```csharp // Entity implementation with dependency injection public class Counter : TaskEntity<int> { readonly ILogger logger; public Counter(ILogger<Counter> logger) { this.logger = logger; } public int Add(int input) { this.logger.LogInformation("Adding {Input} to {State}", input, this.State); return this.State += input; } public int Get() => this.State; public void Reset() => this.State = 0; [Function("Counter")] public Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher) { return dispatcher.DispatchAsync(this); } } // Calling entities from orchestrators [Function("CounterOrchestration")] public static async Task<int> RunOrchestrationAsync( [OrchestrationTrigger] TaskOrchestrationContext context, EntityInstanceId entityId) { // Call entity operation and get result int result = await context.Entities.CallEntityAsync<int>( entityId, "add", 10); // Signal entity without waiting for result context.Entities.SignalEntity(entityId, "reset"); return result; } // Client operations on entities public static async Task<HttpResponseData> GetCounterAsync( [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { var entityId = new EntityInstanceId("counter", id); // Read entity state int? state = await client.Entities.GetEntityAsync<int>(entityId); // Signal entity to perform operation await client.Entities.SignalEntityAsync(entityId, "add", 5); HttpResponseData response = request.CreateResponse(System.Net.HttpStatusCode.OK); await response.WriteAsJsonAsync(state); return response; } ``` ### Querying Orchestration Status and Metadata Client operations for querying, waiting for, and managing orchestration instances. ```csharp public static async Task ManageOrchestrationAsync( DurableTaskClient client, string instanceId) { // Get current orchestration status OrchestrationMetadata metadata = await client.GetInstanceAsync( instanceId, getInputsAndOutputs: true); if (metadata != null) { Console.WriteLine($"Status: {metadata.RuntimeStatus}"); Console.WriteLine($"Created: {metadata.CreatedAt}"); Console.WriteLine($"Last Updated: {metadata.LastUpdatedAt}"); Console.WriteLine($"Input: {metadata.SerializedInput}"); Console.WriteLine($"Output: {metadata.SerializedOutput}"); } // Wait for orchestration to start (exit Pending state) OrchestrationMetadata started = await client.WaitForInstanceStartAsync( instanceId, getInputsAndOutputs: false); // Wait for orchestration to complete OrchestrationMetadata completed = await client.WaitForInstanceCompletionAsync( instanceId, getInputsAndOutputs: true); // Terminate running orchestration await client.TerminateInstanceAsync(instanceId, "User requested termination"); // Query multiple instances with filter var query = new OrchestrationQuery( CreatedFrom: DateTime.UtcNow.AddDays(-7), Statuses: new[] { OrchestrationRuntimeStatus.Running, OrchestrationRuntimeStatus.Pending } ); await foreach (OrchestrationMetadata instance in client.GetAllInstancesAsync(query)) { Console.WriteLine($"Instance {instance.InstanceId}: {instance.RuntimeStatus}"); } } ``` ### Continue As New for Eternal Orchestrations Restarting orchestrations with new input to prevent unbounded history growth in long-running workflows. ```csharp [Function("EternalOrchestrator")] public static async Task<int> EternalOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { int currentValue = context.GetInput<int>() ?? 0; ILogger logger = context.CreateReplaySafeLogger("EternalOrchestrator"); // Perform work await context.CallActivityAsync("DoWork", currentValue); // Wait for next trigger (e.g., daily at midnight) DateTime nextRun = context.CurrentUtcDateTime.Date.AddDays(1); await context.CreateTimer(nextRun, CancellationToken.None); int newValue = currentValue + 1; // Restart with new input, clearing history to prevent memory growth if (newValue < 1000) { logger.LogInformation("Continuing as new with value {Value}", newValue); context.ContinueAsNew(newValue, preserveUnprocessedEvents: true); } return newValue; } // Without ContinueAsNew, history grows indefinitely // With ContinueAsNew, history is truncated periodically ``` ### Replay-Safe Logging and Deterministic Operations Using context-provided APIs for deterministic orchestrator execution across replays. ```csharp [Function("DeterministicOrchestrator")] public static async Task<OrderResult> DeterministicOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { // CORRECT: Use replay-safe logger to avoid duplicate logs ILogger logger = context.CreateReplaySafeLogger("OrderProcessor"); logger.LogInformation("Processing order"); // Only logs once, not on replays // CORRECT: Use context.CurrentUtcDateTime for deterministic time DateTime orderTime = context.CurrentUtcDateTime; // CORRECT: Use context.NewGuid() for deterministic GUIDs Guid orderId = context.NewGuid(); // CORRECT: Check version for conditional logic if (context.CompareVersionTo("2.0") >= 0) { // New behavior for version 2.0+ await context.CallActivityAsync("EnhancedProcessing", orderId); } else { // Legacy behavior await context.CallActivityAsync("LegacyProcessing", orderId); } // WRONG examples (do NOT do this): // DateTime.UtcNow - non-deterministic across replays // Guid.NewGuid() - generates different values on replay // Random.Next() - non-deterministic // File I/O or network calls - must be in activities return new OrderResult(orderId, orderTime); } public record OrderResult(Guid OrderId, DateTime ProcessedAt); ``` ## Summary and Integration Patterns The Durable Task .NET SDK is primarily used for building resilient, long-running workflows in distributed applications. Common use cases include order processing pipelines that coordinate multiple services with compensating transactions, approval workflows with human-in-the-loop patterns using external events, scheduled task automation that runs periodically with continue-as-new patterns, and stateful gaming or IoT scenarios using durable entities for concurrent state management. The SDK excels at scenarios requiring guaranteed execution, automatic retries, and the ability to survive process restarts without losing progress. Integration typically follows one of two patterns: Azure Functions isolated worker integration where the SDK is added via NuGet packages and orchestrations are exposed as HTTP-triggered functions, or standalone worker integration where applications host the worker service directly and connect to Azure Durable Task Scheduler or a sidecar process via gRPC. Both patterns support dependency injection, structured logging, and comprehensive monitoring through built-in status APIs. The class-based programming model with source generators provides a modern, type-safe development experience while maintaining backward compatibility with function-based approaches, making it suitable for both greenfield projects and gradual migration of existing Durable Functions applications.