In Part 1 of our series, we explored the concept of the Asynchronous Request-Reply pattern. Now, let’s dive into a practical implementation using Azure Functions. We’ll create a serverless API that demonstrates this pattern, focusing on three key functions: SubmitRequest, CheckResult, and GetResult.
Setting Up the Azure Function Project
First, ensure you have the Azure Functions Core Tools and the .NET SDK installed. Create a new Azure Functions project using your preferred IDE or the command line.
Implementing the SubmitRequest Function
The SubmitRequest function is responsible for accepting the initial request and starting the long-running process.
public class Function1
{
private static readonly ConcurrentDictionary<string, Status> _fakeDataStore = new();
private readonly ILogger<Function1> _logger;
public Function1(ILogger<Function1> logger)
{
_logger = logger;
}
[Function("SubmitRequest")]
public IActionResult SubmitRequest([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req)
{
string correlationId = Guid.NewGuid().ToString();
// Simulate asynchronous/background processing
_ = Task.Run(async () =>
{
_logger.LogInformation("Processing Started");
_fakeDataStore.TryAdd(correlationId, Status.Processing);
await Task.Delay(20 * 1000); // Simulate a long-running task
_fakeDataStore[correlationId] = Status.Completed;
_logger.LogInformation("Processing Finished");
});
req.HttpContext.Response.Headers.Append("Access-Control-Allow-Origin", "*");
req.HttpContext.Response.Headers.Append("Access-Control-Expose-Headers", "Location, Retry-After");
req.HttpContext.Response.Headers.Append("Retry-After", "5");
return new AcceptedResult(location: $"http://localhost:7279/api/CheckResult/{correlationId}", value: null);
}
// ... other methods
}
Key points:
- We generate a unique correlationId for each request.
- The long-running task is simulated using Task.Run and Task.Delay, mimicking real-world scenarios like queues, service buses etc.
- We use a ConcurrentDictionary as a simple in-memory store for request status, mimicking the behavior of databases or data stores.
- The function returns an AcceptedResult with a “Location” header for status checking and a “Retry-After” header that indicates the number of seconds to wait before polling again for updates.
Implementing the CheckResult Function
The CheckResult function allows the client to check the status of their request.
[Function("CheckResult")]
public IActionResult CheckResult([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "CheckResult/{correlationId}")] HttpRequest req, string correlationId)
{
// Check the status of the request in the fake store
if (!_fakeDataStore.TryGetValue(correlationId, out var status))
{
return new NotFoundObjectResult(new { Message = "Correlation ID not found" });
}
if (status == Status.Completed)
{
req.HttpContext.Response.Headers.Append("Location", $"http://localhost:7279/api/GetResult/{correlationId}");
return new StatusCodeResult(StatusCodes.Status302Found);
}
return new OkResult();
}
Key points:
- We use the correlationId to look up the request status.
- If the request is completed, we return a 302 Found status with a location header for result retrieval.
- If the request is still processing, we return a 200 OK status.
Implementing the GetResult Function
The GetResult function is responsible for returning the final result of the processed request.
[Function("GetResult")]
public IActionResult GetResult([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "GetResult/{correlationId}")] HttpRequest req, string correlationId)
{
return new OkObjectResult($"Resource with correlation ID: {correlationId} has been successfully retrieved.");
}
Key points:
- This function is simple in our example, but in a real-world scenario, it would retrieve and return the actual processed data.
Handling Concurrent Requests and State Management
In this example, we use a ConcurrentDictionary for simplicity. However, for production scenarios, consider using more robust storage solutions like Azure Table Storage or Cosmos DB for state management. This ensures data persistence across function executions and allows for better scalability.
private static readonly ConcurrentDictionary<string, Status> _fakeDataStore = new();
enum Status
{
None,
Processing,
Completed
}
Video Demonstration
Here’s a video walkthrough of the Asynchronous Request-Reply pattern implementation:
Conclusion
This implementation demonstrates the core components of the Asynchronous Request-Reply pattern using Azure Functions:
- SubmitRequest initiates the long-running process and returns immediately.
- CheckResult allows the client to poll for status updates.
- GetResult provides the final processed data.
In a production environment, you’d want to add error handling, implement proper storage solutions, and possibly use Durable Functions for more complex workflows.
In the next part of our series, we’ll explore how to create a vanilla JavaScript client to interact with this Azure Function-based API, demonstrating the full cycle of the Asynchronous Request-Reply pattern.
Additional Resources
For more information on Azure Functions and serverless architectures, check out:
Azure Functions documentation | Microsoft Learn
This resource provides comprehensive guidance on developing and deploying Azure Functions.
For a complete working example of this Asynchronous Request-Reply pattern implementation, you can refer to this GitHub repository:
keke1210/Async-Request-Reply-Pattern (github.com)
This repository contains the full source code for both the Azure Functions backend and the vanilla JavaScript client, allowing you to see the entire implementation in action.