NashTech Blog

Orchestrating Workflows with .NET 8 Durable Functions

Table of Contents

Introduction

Azure Durable Functions in .NET 8 offer a powerful framework for building stateful workflows within serverless architectures. The enhancements introduced in .NET 8, such as significant performance improvements and better integration capabilities, empower developers to create efficient, maintainable, and scalable solutions. In this blog, we will explore common patterns in .NET 8 Durable Functions, showcase real-world use cases, and provide a comprehensive sample project using the out-of-proc worker model in C#.

What’s New in .NET 8 Durable Functions?

.NET 8 introduces several significant enhancements for Azure Functions, such as:

  • Improved cold start times: Azure Functions often face challenges with cold starts, particularly in serverless scenarios where functions may go idle. .NET 8 optimizes cold start performance, allowing functions to respond more quickly to requests, thus enhancing user experience.
  • Enhanced integration with cloud-native tooling: The new version provides better integration with tools such as Azure Monitor, Application Insights, and other observability features. This makes monitoring and debugging applications easier.
  • Better support for out-of-proc workers: The out-of-proc model allows developers to separate function execution from the Azure Functions host, which leads to clearer separation of concerns. This architecture enhances maintainability and scalability by allowing developers to choose the best runtime for their functions.

These improvements enable developers to build more reliable, maintainable, and scalable workflows.

Prerequisites

Implementation of Durable Function Patterns in .NET 8

1: Create a New Azure Functions Project

mkdir DurableFunctionsDemo
cd DurableFunctionsDemo
func init --worker-runtime dotnet --language CSharp

2: Install Durable Functions Extension

func extensions install --package Microsoft.Azure.Functions.Extensions.DurableTask 

3: Create Function Files & Project Structure

Create the following files in your project:

  • OrderProcessingOrchestrator.cs
  • ValidateOrderActivity.cs
  • ChargePaymentActivity.cs
  • CheckInventoryActivity.cs
  • ShipOrderActivity.cs
  • OrderDetails.cs
  • OrderResult.cs
  • HttpStart.cs
  • local.settings.json

4: Implement the Orchestrator and Activities

OrderDetails.cs

public class OrderDetails
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
    public string CustomerId { get; set; }
} 

OrderResult.cs

public class OrderResult
{
    public bool IsSuccessful { get; set; }
} 

OrderProcessingOrchestrator.cs

Function Chaining Pattern

The Function Chaining pattern executes a series of functions in a predefined order, where the output of one function serves as the input to the next.

Real-World Use Case: Order Fulfillment System

In an e-commerce platform, the system processes orders by executing steps such as order validation, payment processing, inventory checks, and shipping initiation sequentially. In our example, the OrderProcessingOrchestrator sequentially calls the activities: ValidateOrderActivity, ChargePaymentActivity, CheckInventoryActivity, and ShipOrderActivity. Each function’s output is necessary for the next step, ensuring a smooth workflow for order processing.

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
using System;
using System.Threading.Tasks;

public class OrderProcessingOrchestrator
{
    [Function("OrderProcessingOrchestrator")]
    public static async Task<OrderResult> RunOrderOrchestrator(
        [OrchestrationTrigger] IDurableOrchestrationContext context)
    {
        var orderDetails = context.GetInput<OrderDetails>();
        
        var validationResult = await context.CallActivityAsync<bool>("ValidateOrderActivity", orderDetails);
        if (!validationResult) throw new InvalidOperationException("Order validation failed");
        
        var paymentResult = await context.CallActivityAsync<bool>("ChargePaymentActivity", orderDetails);
        if (!paymentResult) throw new InvalidOperationException("Payment processing failed");
        
        var inventoryResult = await context.CallActivityAsync<bool>("CheckInventoryActivity", orderDetails);
        if (!inventoryResult) throw new InvalidOperationException("Inventory check failed");
        
        var shippingResult = await context.CallActivityAsync<bool>("ShipOrderActivity", orderDetails);
        return new OrderResult { IsSuccessful = shippingResult };
    }
} 

ValidateOrderActivity.cs

using Microsoft.Azure.Functions.Worker;

public class ValidateOrderActivity
{
    [Function("ValidateOrderActivity")]
    public static bool Run([ActivityTrigger] OrderDetails orderDetails)
    {
        // Simple validation logic
        return !string.IsNullOrEmpty(orderDetails.ProductId) && orderDetails.Quantity > 0;
    }
} 

ChargePaymentActivity.cs

using Microsoft.Azure.Functions.Worker;

public class ChargePaymentActivity
{
    [Function("ChargePaymentActivity")]
    public static bool Run([ActivityTrigger] OrderDetails orderDetails)
    {
        // Simulate charging payment
        return true; // Assume payment was successful
    }
} 

CheckInventoryActivity.cs

using Microsoft.Azure.Functions.Worker;

public class CheckInventoryActivity
{
    [Function("CheckInventoryActivity")]
    public static bool Run([ActivityTrigger] OrderDetails orderDetails)
    {
        // Simulate inventory check
        return true; // Assume inventory is sufficient
    }
} 

ShipOrderActivity.cs

using Microsoft.Azure.Functions.Worker;

public class ShipOrderActivity
{
    [Function("ShipOrderActivity")]
    public static bool Run([ActivityTrigger] OrderDetails orderDetails)
    {
        // Simulate shipping process
        return true; // Assume shipping was successful
    }
} 

HttpStart.cs

Asynchronous HTTP API Pattern

The Asynchronous HTTP API pattern allows clients to initiate long-running operations via an HTTP call and then poll for the result using a status endpoint.

Use Case: Report Generation

In a business application that generates complex financial reports, this pattern is useful for operations that may take several minutes to complete. The HttpStart function exemplifies this pattern by allowing clients to initiate a long-running operation via an HTTP call and check its status using a unique URL.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
using System.Threading.Tasks;

public class HttpStart
{
    [Function("HttpStart")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
        [DurableClient] IDurableOrchestrationClient starter)
    {
        var orderDetails = await req.ReadFromJsonAsync<OrderDetails>();
        string instanceId = await starter.StartNewAsync("OrderProcessingOrchestrator", orderDetails);
        return starter.CreateCheckStatusResponse(req, instanceId);
    }
} 

5: Update local.settings.json

Add the following to your local.settings.json file:

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet"
    }
} 

6: Run and Test the Application

func start 

// You can use tools like Postman or cURL to test your HTTP trigger:

curl -X POST http://localhost:7071/api/HttpStart \
-H "Content-Type: application/json" \
-d '{"ProductId": "123", "Quantity": 2, "CustomerId": "abc"}' 

Expected Response: You’ll receive a response with a status URL to check the progress of your orchestration.

Check the Status of the Orchestration

Use the status URL from the previous response to check the current status of your orchestration, allowing for monitoring of long-running processes.

Explanation of other Durable Functions Patterns

Fan-Out/Fan-In Pattern

This pattern allows parallel execution of multiple tasks (fan-out) and then aggregates their results (fan-in).

Use Case: Image Processing

A content platform processes a large batch of uploaded images by resizing them into different resolutions concurrently.

Implementation of Orchestrator Function

[Function("ImageProcessingOrchestrator")]
public static async Task<List<string>> RunImageOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var imageUrls = context.GetInput<List<string>>();
    var tasks = new List<Task<string>>();
    foreach (var imageUrl in imageUrls)
    {
        tasks.Add(context.CallActivityAsync<string>("ProcessImageActivity", imageUrl));
    }
    var processedImageUrls = await Task.WhenAll(tasks);
    return processedImageUrls.ToList();
}

Benefits:

  • Handles high throughput workloads.
  • Parallel execution speeds up processing.

Human Interaction Pattern

This pattern handles workflows that require human interaction or approval, pausing until an external input trigger the continuation.

Use Case: Loan Approval System

A loan processing system requires approval from a human officer at certain stages.

Implementation of Orchestrator Function:

[Function("LoanApprovalOrchestrator")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var loanApplication = context.GetInput<LoanApplication>();
    await context.CallActivityAsync("NotifyApprovalActivity", loanApplication);
    var approvalResult = await context.WaitForExternalEvent<bool>("LoanApprovalEvent");
    if (approvalResult)
    {
        await context.CallActivityAsync("ProcessLoanActivity", loanApplication);
    }
} 

Benefits:

  • Effectively handles human-in-the-loop workflows.
  • Flexible and adaptive to real-world scenarios.

Aggregator (Monitor Pattern)

The Monitor pattern enables recurring checks until a specific condition is met.

Use Case: Inventory Monitoring

An e-commerce platform monitors inventory levels for out-of-stock items and triggers alerts or restocks when needed.

Implementation of Orchestrator Function:

[Function("InventoryMonitorOrchestrator")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var productId = context.GetInput<string>();
    while (true)
    {
        var inventoryStatus = await context.CallActivityAsync<bool>("CheckInventoryActivity", productId);
        if (inventoryStatus)
        {
            await context.CallActivityAsync("RestockProductActivity", productId);
            break;
        }
        var nextCheck = context.CurrentUtcDateTime.AddMinutes(10);
        await context.CreateTimer(nextCheck, CancellationToken.None);
    }
} 

Benefits:

  • Ideal for polling or monitoring scenarios.
  • Efficiently handles recurring checks.

Conclusion

Durable Functions in .NET 8 provide a robust framework for building complex, stateful workflows. By leveraging patterns such as Function Chaining, Fan-Out/Fan-In, Asynchronous HTTP APIs, Human Interaction, and Monitor patterns, developers can effectively address real-world scenarios like order processing, image processing, report generation, human approvals, and inventory monitoring. The enhancements introduced in .NET 8, particularly the out-of-proc worker model, greatly improve maintainability and scalability, positioning Durable Functions as a key component for stateful orchestration in Azure. With these improvements, developers can create more capable and resilient serverless applications than ever before.

Picture of akshaychirde

akshaychirde

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top