This is the ninth part in a series of articles. If you’re not familiar with Durable Functions you should check out the previous articles before reading this.
If your orchestration takes a while to execute, you may not want the end client (for example a web app that triggers the orchestration via an HTTP call) to wait around for a response. Instead you may want to provide the client with a way of querying (polling) if the long-running process is complete. In a previous article in this series we looked at how to get results from orchestrations. In this article we’ll dig into this in a bit more detail.
The Asynchronous HTTP API Pattern means the client calls an HTTP API which does not return the end result, but rather returns a way of checking for the completion of the task, for example by being providing with a status URL. This pattern may also be referred to as the Polling Consumer Pattern.
Recall from this previous article that when the client HTTP function is called, it returns some body content with management URLS for the orchestration instance that was started, for example:
{
"id": "1bf95a9fa5084745bce24363e9ee781b",
"statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
"sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
"terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
"rewindPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b/rewind?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
"purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA=="
}
Notice that this information provides the client with a lot of information, including the URLs to terminate the orchestration, purge the history etc. Notice the response also include the key: code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==
In the Azure Portal, this is the durabletask_extension key for the function app. With this key the client can perform admin/management operations on orchestration instances using the API including getting results from arbitrary orchestrations, terminating running orchestrations, etc..
The response also contains headers, including one called Location that also points to the check status URL, e.g. http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==
If we look at the client function, this information is generated with the line: return starter.CreateCheckStatusResponse(req, instanceId);
[FunctionName("AsyncApiPatternExample_HttpStartV1")]
public static async Task<HttpResponseMessage> HttpStartV1(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequestMessage req,
[OrchestrationClient]DurableOrchestrationClient starter,
ILogger log)
{
string instanceId = await starter.StartNewAsync("AsyncApiPatternExample", null);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
You probably do not want to expose the durabletask_extension key to clients as this will allow them to perform operations they should not have access to. Instead we can modify the client function as follows:
[FunctionName("AsyncApiPatternExample_HttpStartV2")]
public static async Task<HttpResponseMessage> HttpStartV2(
[HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
[OrchestrationClient]DurableOrchestrationClient starter,
ILogger log)
{
string instanceId = await starter.StartNewAsync("AsyncApiPatternExample", null);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
// Create the URL to allow the client to check status of a request (excluding the function key in the code querystring)
string checkStatusUrl = string.Format("{0}://{1}/api/AsyncApiPatternExample_Status?id={2}", req.RequestUri.Scheme, req.RequestUri.Host, instanceId);
// Create the response and add headers
var response = new HttpResponseMessage()
{
StatusCode = System.Net.HttpStatusCode.Accepted,
Content = new StringContent(checkStatusUrl),
};
response.Headers.Add("Location", checkStatusUrl);
response.Headers.Add("Retry-After", "10");
return response;
}
In this new version of the client function, we control what is passed back to the client, we don’t include any sensitive management URLs/keys, a response from calling this function would look like:
Response body (text): https://localhost:7071/api/AsyncApiPatternExample_Status?id=d69a847230e5411ca57659723cb14c55
Response status: 202Accepted
Response Headers:
Location = https://localhost:7071/api/AsyncApiPatternExample_Status?id=d69a847230e5411ca57659723cb14c55
Retry-After = 10
etc.
The client can then GET the status URL (+ the function key): https://localhost:7071/api/AsyncApiPatternExample_Status?id=d69a847230e5411ca57659723cb14c55&code=XXXXXXXXXX
This will return:
{
"currentStatus": "Running",
"result": null
}
And once the orchestration has complete this will return:
{
"currentStatus": "Completed",
"result": "Hello London!"
}
The actual status function look like the following:
[FunctionName("AsyncApiPatternExample_Status")]
public static async Task<IActionResult> Status(
[HttpTrigger(AuthorizationLevel.Function, "get")]HttpRequest req,
[OrchestrationClient]DurableOrchestrationClient orchestrationClient,
ILogger log)
{
var orchestrationInstanceId = req.Query["id"];
if (string.IsNullOrWhiteSpace(orchestrationInstanceId))
{
return new NotFoundResult();
}
// Get the status for the passed in instanceId
DurableOrchestrationStatus status = await orchestrationClient.GetStatusAsync(orchestrationInstanceId);
if (status is null)
{
return new NotFoundResult();
}
var shortStatus = new
{
currentStatus = status.RuntimeStatus.ToString(),
result = status.Output
};
return new OkObjectResult(shortStatus);
// We could also expand this and check status.RuntimeStatus and for example return a 202 if processing is still underway
}
The key thing in the preceding code is the call: DurableOrchestrationStatus status = await orchestrationClient.GetStatusAsync(orchestrationInstanceId); This allows the status to be obtained for the orchestration id that was passed in as a querystring parameter.
The output to the client is chosen in the anonymous object shortStatus. Now the client does not get sensitive information returned such as the management URLs and keys. A client could however still retrieve the status/result from orchestrations started by other clients.If you want more fine grained control/authentication you should check out the other options in the documentation.
If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.
SHARE: