Azure Durable Functions extends Azure Functions with stateful orchestration capabilities. Unlike Step Functions (JSON-based ASL), Durable Functions uses code—C#, JavaScript, Python, or PowerShell—to define workflows. This code-first approach enables IDE support, unit testing, and complex control flow. This comprehensive guide covers the core patterns: Function Chaining, Fan-Out/Fan-In, Human Interaction, and the Actor Pattern with Durable Entities.
Orchestrator Function Fundamentals
Durable Functions introduces three function types:
- Client Function: Starts orchestrations (HTTP trigger, queue trigger)
- Orchestrator Function: Coordinates activities and sub-orchestrations
- Activity Function: Performs actual work (idempotent, stateless)
flowchart TB
Client["Client Function (HTTP Trigger)"] --> Orchestrator["Orchestrator Function"]
Orchestrator --> Activity1["Activity: Validate"]
Orchestrator --> Activity2["Activity: Process"]
Orchestrator --> Activity3["Activity: Notify"]
subgraph StateManagement ["State Management (Automatic)"]
History["Execution History"]
Checkpoints["Checkpoints"]
end
Orchestrator --> StateManagement
style Orchestrator fill:#E1F5FE,stroke:#0277BD
Pattern 1: Function Chaining
Sequential execution with output piped to next activity:
[FunctionName("OrderProcessingOrchestrator")]
public static async Task<OrderResult> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<Order>();
// Activities run sequentially, each awaited
var validationResult = await context.CallActivityAsync<ValidationResult>(
"ValidateOrder", order);
if (!validationResult.IsValid)
{
return new OrderResult { Status = "Rejected", Reason = validationResult.Errors };
}
var paymentResult = await context.CallActivityAsync<PaymentResult>(
"ProcessPayment", order);
var shipmentResult = await context.CallActivityAsync<ShipmentResult>(
"CreateShipment", new { order, paymentResult.TransactionId });
await context.CallActivityAsync("SendConfirmation", new { order, shipmentResult });
return new OrderResult { Status = "Completed", TrackingNumber = shipmentResult.TrackingNumber };
}
[FunctionName("ValidateOrder")]
public static ValidationResult ValidateOrder([ActivityTrigger] Order order, ILogger log)
{
// Validation logic - idempotent, no orchestration context
return new ValidationResult { IsValid = order.Items.Any() && order.Total > 0 };
}
Pattern 2: Fan-Out/Fan-In
Parallel execution of multiple activities, waiting for all to complete:
[FunctionName("ParallelProcessingOrchestrator")]
public static async Task<List<ProcessResult>> RunParallel(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var items = context.GetInput<List<WorkItem>>();
// Fan-out: Start all activities in parallel
var tasks = items.Select(item =>
context.CallActivityAsync<ProcessResult>("ProcessItem", item));
// Fan-in: Wait for all to complete
var results = await Task.WhenAll(tasks);
// Aggregate results
var summary = await context.CallActivityAsync<Summary>(
"AggregateResults", results.ToList());
return results.ToList();
}
Pattern 3: Human Interaction (Approval)
Wait for external event (human approval) with timeout:
[FunctionName("ApprovalOrchestrator")]
public static async Task<ApprovalResult> RunApproval(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var request = context.GetInput<ApprovalRequest>();
// Send approval request email
await context.CallActivityAsync("SendApprovalEmail", request);
// Wait for external event OR timeout (72 hours)
using var cts = new CancellationTokenSource();
var approvalEvent = context.WaitForExternalEvent<ApprovalResponse>("Approved");
var rejectionEvent = context.WaitForExternalEvent<ApprovalResponse>("Rejected");
var timeout = context.CreateTimer(
context.CurrentUtcDateTime.AddHours(72), cts.Token);
var winner = await Task.WhenAny(approvalEvent, rejectionEvent, timeout);
cts.Cancel(); // Cancel the timer
if (winner == timeout)
{
return new ApprovalResult { Status = "Timeout" };
}
var response = winner == approvalEvent
? await approvalEvent
: await rejectionEvent;
return new ApprovalResult {
Status = response.Approved ? "Approved" : "Rejected",
ApprovedBy = response.ApproverEmail
};
}
// External API to submit approval
[FunctionName("SubmitApproval")]
public static async Task<IActionResult> SubmitApproval(
[HttpTrigger] HttpRequest req,
[DurableClient] IDurableOrchestrationClient client)
{
var body = await req.ReadAsStringAsync();
var approval = JsonSerializer.Deserialize<ApprovalSubmission>(body);
await client.RaiseEventAsync(
approval.OrchestrationId,
approval.Approved ? "Approved" : "Rejected",
new ApprovalResponse {
Approved = approval.Approved,
ApproverEmail = approval.Email
});
return new OkResult();
}
Pattern 4: Durable Entities (Actor Model)
Stateful actors that handle operations serially:
[JsonObject(MemberSerialization.OptIn)]
public class Counter : ICounter
{
[JsonProperty("value")]
public int Value { get; set; }
public void Add(int amount) => Value += amount;
public void Reset() => Value = 0;
public int Get() => Value;
[FunctionName(nameof(Counter))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
=> ctx.DispatchAsync<Counter>();
}
// Using the entity from an orchestrator
[FunctionName("EntityOrchestrator")]
public static async Task UseCounter(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var entityId = new EntityId(nameof(Counter), "myCounter");
// Signal (fire-and-forget)
context.SignalEntity(entityId, "Add", 5);
// Call (request-response)
var currentValue = await context.CallEntityAsync<int>(entityId, "Get");
}
Error Handling and Retries
var retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: 3)
{
BackoffCoefficient = 2.0,
MaxRetryInterval = TimeSpan.FromMinutes(5),
RetryTimeout = TimeSpan.FromHours(1)
};
try
{
await context.CallActivityWithRetryAsync(
"UnreliableActivity",
retryOptions,
input);
}
catch (FunctionFailedException ex)
{
// All retries exhausted
await context.CallActivityAsync("HandleFailure", ex.Message);
}
Key Takeaways
- Durable Functions provides code-first workflow orchestration
- Orchestrator functions must be deterministic (no I/O, no random)
- Activities are idempotent work units
- Fan-Out/Fan-In enables parallel processing
- External events enable human-in-the-loop workflows
- Durable Entities provide actor-model state management
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.