The article show how an Azure Durable Function can be used to process a HTTP API request which waits for the completion result. This can be required when you have no control over the client application calling the API and the process requires asynchronous operations like further API calls and so on. The Azure Durable Function could call other APIs, run separate processes and it is unknown when this is finished. If you could control the client starting the process, you would not wait, but use a callback, for example in the last activity.
Code: https://github.com/damienbod/AzureDurableFunctions
History
- 2025-11-23 Updated to .NET 10, Azure functions V4, Azurite
- 2021-03-07 Update packages and using DefaultAzureCredential for Azure Key vault access
- 2020-09-18 Updated Configuration, updated Nuget packages
Posts in this series
- Using External Inputs in Azure Durable functions
- Azure Functions Configuration and Secrets Management
- Using Key Vault and Managed Identities with Azure Functions
- Waiting for Azure Durable Functions to complete
- Azure Durable Functions Monitoring and Diagnostics
- Retry Error Handling for Activities and Orchestrations in Azure Durable Functions
The API call underneath handles the client request using a HTTP POST request. The response is or can be specific for the client. The Azure Durable Function is implemented and processed in the Processing class. This returns the result directly. The data received in the body of the request is passed as a parameter. The data returned also needs to be in the format required by the client, and not the format you use.
using DurableWait.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace DurableWait.Apis;
public class BeginFlowWithHttpPost
{
private readonly Processing _processing;
private readonly ILogger<BeginFlowWithHttpPost> _logger;
public BeginFlowWithHttpPost(ILogger<BeginFlowWithHttpPost> logger, Processing processing)
{
_logger = logger;
_processing = processing;
}
[Function(Constants.BeginFlowWithHttpPost)]
public async Task<IActionResult> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest request,
[DurableClient] DurableTaskClient client)
{
_logger.LogInformation("Started new flow");
var beginRequestData = await JsonSerializer.DeserializeAsync<BeginRequestData>(request.Body);
_logger.LogInformation("Started new flow with ID = '{beginRequestDataId}'.", beginRequestData.Id);
return await _processing.ProcessFlow(beginRequestData, request, client);
}
}
The Processing class starts the Azure Durable Function and waits for this to complete. The IDurableOrchestrationClient interface is passed as a parameter from the Azure Function. The MyOrchestration orchestration is started and the method waits for this to complete or timeout using the WaitForCompletionOrCreateCheckStatusResponseAsync method. If the process times out, the result is returned without a completed status. An InternalServerError 500 result could be returned for this and the status can be set to terminated. If the Azure Durable Function completes successfully, the result needs to be mapped to the caller’s client API required body result, not the output of the Azure Durable Function. This can be created using the data from the status request. The CompleteResponseData data is produced using the data from the Azure Durable Function output and returned to the client.
using DurableWait.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using System.Net;
namespace DurableWait;
public class Processing
{
private readonly ILogger<Processing> _logger;
public Processing(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<Processing>();
}
public async Task<IActionResult> ProcessFlow(
BeginRequestData beginRequestData,
HttpRequest request,
DurableTaskClient client)
{
var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(Constants.MyOrchestration, beginRequestData);
_logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);
// Create a timeout using CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
OrchestrationMetadata data;
try
{
data = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, cts.Token);
}
catch (OperationCanceledException)
{
// Timeout occurred
await client.TerminateInstanceAsync(instanceId, "Timeout something took too long");
return new ContentResult()
{
Content = "{ \"error\": \"Timeout something took too long\" }",
ContentType = "application/json",
StatusCode = (int)HttpStatusCode.InternalServerError
};
}
// Check if completed
if (data == null || data.RuntimeStatus != OrchestrationRuntimeStatus.Completed)
{
await client.TerminateInstanceAsync(instanceId, "Timeout something took too long");
return new ContentResult()
{
Content = "{ \"error\": \"Timeout something took too long\" }",
ContentType = "application/json",
StatusCode = (int)HttpStatusCode.InternalServerError
};
}
var output = data.ReadOutputAs<MyOrchestrationDto>();
var completeResponseData = new CompleteResponseData
{
BeginRequestData = output.BeginRequest,
Id2 = output.BeginRequest.Id + ".v2",
MyActivityTwoResult = output.MyActivityTwoResult
};
return new OkObjectResult(completeResponseData);
}
}
The MyOrchestration class implements the Azure Durable Function orchestration. This has two activities and uses the body from the client API call as the input data. The result of each activity is added to the orchestration data.
using DurableWait.Model;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
namespace DurableWait.Orchestrations;
public class MyOrchestration
{
private readonly ILogger<MyOrchestration> _logger;
public MyOrchestration(ILogger<MyOrchestration> logger)
{
_logger = logger;
}
[Function(Constants.MyOrchestration)]
public async Task<MyOrchestrationDto> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var myOrchestrationDto = new MyOrchestrationDto
{
BeginRequest = context.GetInput<BeginRequestData>()
};
if (!context.IsReplaying)
{
_logger.LogWarning("begin MyOrchestration with input id {myOrchestrationDtoBeginRequestId}", myOrchestrationDto.BeginRequest.Id);
}
var myActivityOne = await context.CallActivityAsync<string>(
Constants.MyActivityOne, context.GetInput<BeginRequestData>());
myOrchestrationDto.MyActivityOneResult = myActivityOne;
if (!context.IsReplaying)
{
_logger.LogWarning("myActivityOne completed {myActivityOne}", myActivityOne);
}
var myActivityTwo = await context.CallActivityAsync<string>(
Constants.MyActivityTwo, myOrchestrationDto);
myOrchestrationDto.MyActivityTwoResult = myActivityTwo;
if (!context.IsReplaying)
{
_logger.LogWarning("myActivityTwo completed {myActivityTwo}", myActivityTwo);
}
return myOrchestrationDto;
}
}
The Startup classes adds the services to the DI so that construction injection can be used in the implementation classes.
using Azure.Identity;
using DurableWait;
using DurableWait.Activities;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Reflection;
var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();
builder.Services
.AddApplicationInsightsTelemetryWorkerService()
.ConfigureFunctionsApplicationInsights();
builder.Services.AddOptions<MyConfiguration>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("MyConfiguration").Bind(settings);
});
builder.Services.AddOptions<MyConfigurationSecrets>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("MyConfigurationSecrets").Bind(settings);
});
builder.Services.AddLogging();
builder.Services.AddScoped<MyActivities>();
builder.Services.AddScoped<Processing>();
var keyVaultEndpoint = builder.Configuration["AzureKeyVaultEndpoint"];
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
// using Key Vault, either local dev or deployed
builder.Configuration
.SetBasePath(Environment.CurrentDirectory)
.AddAzureKeyVault(new Uri(keyVaultEndpoint), new DefaultAzureCredential())
.AddJsonFile("local.settings.json", optional: true)
.AddEnvironmentVariables();
}
else
{
// local dev no Key Vault
builder.Configuration
.SetBasePath(Environment.CurrentDirectory)
.AddJsonFile("local.settings.json", optional: true)
.AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true)
.AddEnvironmentVariables();
}
builder.Build().Run();
If the process completes successfully, the result gets returned as required.

If the process fails, an error message is returned after the timeout. This was simulated using a thread sleep in an activity. The API call is set to timeout after 7 seconds.

Links:
https://docs.microsoft.com/en-us/azure/azure-functions/durable/
https://github.com/Azure/azure-functions-durable-extension
Running Local Azure Functions in Visual Studio with HTTPS
Microsoft Azure Storage Explorer
Microsoft Azure Storage Emulator

[…] Waiting for Azure Durable Functions to complete (Damien Bowden) […]
Thanks for this post! This gave me a bunch of inspiration for my azure function that leads into a service bus
—
andreaslengkeek@ssw.com.au
Thanks
Greetings Damien
Why timeout = 7 seconds? It looks like not enough.
Is it ready to use in production?