Chaining Pattern - Part 2 - Azure Durable Functions

2022, Mar 04

If you need to define a workflow that executes tasks in sequence, the Chaining Pattern is the application pattern to use with Durable Functions.

This is a series where I am explaining the components of durable functions1 and the different application patterns of implementation.

Check the PlayGoKids repository for this article demo.

Understanding the Flow

Chaining

The Chaining pattern allows for the execution of activities in sequence, and in-between, states are saved. In this example we have an Order example, where different steps are executed in sequence: Initialize Order, Validate Order, Process Order and Save Order.

The Order is created as the activities are executed.

Starter Function (aka DurableClient)

The starter receives an Order as input:

public class Order
{
    public int OrderId { get; set; }
    public string OrderNumber { get; set; }
    public string ProductSKu { get; set; }
    public int Quantity { get; set; }
    public DateTime OrderDate { get; set; }
    public string OrderStatus { get; set; }
}
[FunctionName($"{nameof(Chaining)}_HttpStart")]
public async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req,
    [DurableClient] IDurableOrchestrationClient starter,
    ILogger log)
{
    var order = await req.Content.ReadAsAsync<Order>();

    var instanceId = await starter.StartNewAsync(nameof(Orchestrator), order);

    log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

    return starter.CreateCheckStatusResponse(req, instanceId);
}

This is an HTTPTrigger function that receives the Order input, which initially receives a ProductSku and Quantity as available in this CURL request:

curl --location --request GET 'http://localhost:7071/Chaining_HttpStart' \
--header 'Content-Type: application/json' \
--data-raw '{
    "ProductSku": "Milk",
    "Quantity": 1
}'

Orchestrator Function

The Orchestrator contains the calls to all Order activities. It receives an Order as input and returns the outputs (different Order properties). Check the PlayGoKids repository for details of IOrderService.

[FunctionName(nameof(Orchestrator))]
public async Task<List<string>> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var input = context.GetInput<Order>(); // sku and quantity

    var outputs = new List<string>
    {
        await context.CallActivityAsync<string>(nameof(IOrderService.InitializeOrder), input),
        await context.CallActivityAsync<string>(nameof(IOrderService.ValidateOrder), input),
        await context.CallActivityAsync<string>(nameof(IOrderService.ProcessOrder), input),
        await context.CallActivityAsync<string>(nameof(IOrderService.SaveOrder), input),
    };

    return outputs;
}

Activity Functions

The activities were created in order Top-down, displayed below:

public class Activity
{
    private readonly IOrderService _orderService;

    public Activity(IOrderService orderService)
    {
        _orderService = orderService;
    }

    [FunctionName(nameof(IOrderService.InitializeOrder))]
    public string Initialize([ActivityTrigger] Order order, ILogger log)
    {
        log.LogInformation($"Initializing order.");
        var initializedOrder = _orderService.InitializeOrder(order);
        return initializedOrder.OrderNumber;
    }

    [FunctionName(nameof(IOrderService.ValidateOrder))]
    public string ValidateOrder([ActivityTrigger] Order order, ILogger log)
    {
        log.LogInformation($"Validating order.");
        var validatedOrder = _orderService.ValidateOrder(order);
        return validatedOrder.Quantity.ToString();
    }

    [FunctionName(nameof(IOrderService.ProcessOrder))]
    public string ProcessOrder([ActivityTrigger] Order order, ILogger log)
    {
        log.LogInformation($"Processing order.");
        var processedOrder = _orderService.ProcessOrder(order);
        return processedOrder.OrderDate.ToString("o");
    }

    [FunctionName(nameof(IOrderService.SaveOrder))]
    public string SaveOrder([ActivityTrigger] Order order, ILogger log)
    {
        log.LogInformation($"Saving order.");
        var savedOrder = _orderService.SaveOrder(order);
        return savedOrder.OrderId.ToString();
    }
}

The OrderService is just a dummy that implements the Activity actions:

public class OrderService : IOrderService
{
    public Order InitializeOrder(Order order)
    {
        order.OrderNumber = $"ON-RANDOM"; // generate number
        order.OrderStatus = nameof(InitializeOrder);
        return order;
    }

    public Order ValidateOrder(Order order)
    {
        if (order.Quantity == 0) // validate quantity
        {
            order.Quantity = 1; // at least one item
        }

        order.OrderStatus = nameof(ValidateOrder);
        return order;
    }

    public Order ProcessOrder(Order order)
    {
        order.OrderDate = DateTime.Now; // generate date
        order.OrderStatus = nameof(ProcessOrder);
        return order;
    }

    public Order SaveOrder(Order order)
    {
        order.OrderId = 1000; // generate id
        order.OrderStatus = nameof(SaveOrder);
        return order;
    }
}

This is an example of how to use the Chaining pattern, applied to the creation of Orders.

In the next article of the series let's check out how to use the Fan out/fan in pattern.

Full Series

Major components

Function chaining

Fan out/fan in

Async HTTP APIs

Monitor

Human interaction

Aggregator (stateful entities)


  1. Azure Durable Functions link