8 min read

Building RequestBin with Durable Functions

Justin Yoo

In my previous post, I discussed the workflow orchestration using Azure Durable Functions. It's possible because of the "Stateful" nature of Durable Functions. If we make use of these characteristics, we can build an Azure Functions app in more various use cases that require the "Stateful-ness". Throughout this post, I'm going to implement the RequestBin application feature as an experiment, to understand its working mechanism. If you want to see the more complete example, find another post written by my fellow MVP, Paco.

You can find the sample code from GitHub, Durable RequestBin Sample.

We used to deal with this nostalgic RequestBin app pretty often.

It's now gone to history, but the source code is open for anyone to build and use. There's a sample Heroku app, but it's not surprising whenever it goes down. Therefore, if you want to run your own one, host this code somewhere, using a Docker container. With regards to this, I introduced two sample codes – one using Azure Container Instances and the other using Azure App Service. They're totally OK to use. By the way, the original RequestBin app consists of two parts – an application and Redis Cache. The cache is evaporating, not sustainable. If you want to look for some old webhook history, the original app might not be a solution.

Azure Durable Functions internally implements Event Sourcing Pattern using Azure Table Storage. As all events are statefully stored within the storage, it's a perfect example to build the RequestBin app.

Stateful (or Durable) Entity

The orchestration feature of Durable Functions IMPLICITLY stores the "State" through the IDurableOrchestrationClient instance. On the other hand, if we use the stateful entity or durable entity, we can EXPLICITLY handle the "State" via the IDurableClient instance. Let's have a look at the code below; the DurableClient binding goes with IDurableClient (line #4).

[FunctionName(nameof(CreateBin))]
public async Task<IActionResult> CreateBin(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route="bins")] HttpRequest req,
[DurableClient] IDurableClient client)
{
...
}
view raw 01-create-bin.cs hosted with ❤ by GitHub

Now, we need to create a reference entity that stores the "State" (line #7). The binId can be anything as long as it guarantees its uniqueness. GUID can be the right candidate for it (line #6).

[FunctionName(nameof(CreateBin))]
public async Task<IActionResult> CreateBin(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route="bins")] HttpRequest req,
[DurableClient] IDurableClient client)
{
var binId = Guid.NewGuid();
var bin = new EntityId("Bin", binId.ToString());
...
}
view raw 02-create-bin.cs hosted with ❤ by GitHub

Have a look at the code above. The "Bin" is the name of the entity that EXPLICITLY stores the "State". What's interesting here is the entity follows the concept of the Actor Model, which stores the entity state and defines its behaviours. The following code illustrates the actions or behaviours of the entity through the IBin interface. Here in this exercise, we only use "add" and "reset" states.

public interface IBin
{
void Add(BinItem item);
void Reset();
}
view raw 03-ibin.cs hosted with ❤ by GitHub

And we implement the interface with the Bin class. The class has the property, History that stores the "State" (line #5). You probably have noticed that I use the JSON serialiser option of MemberSerialization.OptIn (line #1). With this option, only properties that explicitly decorated with JsonProperty are serialised (line #4). There is a static method, Run() (line #23). It dispatches the event and stores the "State" to the table storage.

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class Bin : IBin
{
[JsonProperty("history")]
public virtual List<BinItem> History { get; set; } = new List<BinItem>();
public void Add(BinItem item)
{
if (item == null)
{
return;
}
this.History.Insert(0, item);
}
public void Reset()
{
this.History.Clear();
}
[FunctionName(nameof(Bin))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx) => ctx.DispatchAsync<Bin>();
}
view raw 04-bin.cs hosted with ❤ by GitHub

Create Bin

In order to store the "State" to the entity, we use this SignalEntityAsync() method. As its name suggests, it sends a signal only (fire and forget) to the behaviour in the actor (line #8), Bin in this example. At this time, we only need a Bin, so we only pass null.

[FunctionName(nameof(CreateBin))]
public async Task<IActionResult> CreateBin(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route="bins")] HttpRequest req,
[DurableClient] IDurableClient client)
{
...
await client.SignalEntityAsync<IBin>(bin, o => o.Add(null));
...
}
view raw 05-create-bin.cs hosted with ❤ by GitHub

At this stage, we can see the records in the Table Storage. We can only see the empty array in the history field because we've only created the bin itself.

Add Webhook Requests

Let's add the webhook request items. The endpoint is pretty similar to the previous one. But this time, instead of passing null, we need to put the actual request data like timestamp, request method, header, query string and payload (line #10-14).

[FunctionName(nameof(AddHistory))]
public async Task<IActionResult> AddHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "patch", "delete", Route="bins/{binId}")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
var history = new BinItem();
using (var reader = new StreamReader(req.Body))
{
history.Timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz");
history.Method = req.Method;
history.Headers = req.Headers.AsEnumerable().ToDictionary(p => p.Key, p => string.Join(";", p.Value));
history.Queries = req.Query.ToDictionary(p => p.Key, p => string.Join(";", p.Value));
history.Body = await reader.ReadToEndAsync();
}
...
}
view raw 06-add-history.cs hosted with ❤ by GitHub

Then, create the bin reference with the binId and call the SignalEntityAsync() method to add the history (line #11).

[FunctionName(nameof(AddHistory))]
public async Task<IActionResult> AddHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "patch", "delete", Route="bins/{binId}")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
...
var bin = new EntityId("Bin", binId);
await client.SignalEntityAsync<IBin>(bin, o => o.Add(history));
...
}
view raw 07-add-history.cs hosted with ❤ by GitHub

Once completed, send a webhook request to this endpoint. Then the records in the Table Storage has changed.

View Webhook Requests History

We also need to see the list of webhooks that we've sent so far. Create the bin reference first (line #7).

[FunctionName(nameof(GetHistory))]
public async Task<IActionResult> GetHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route="bins/{binId}/history")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
var bin = new EntityId("Bin", binId);
...
}
view raw 08-get-history.cs hosted with ❤ by GitHub

Then, invoke the ReadEntityStateAsync() method that replays the events and converts into the response (line #9).

[FunctionName(nameof(GetHistory))]
public async Task<IActionResult> GetHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route="bins/{binId}/history")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
...
var entity = await client.ReadEntityStateAsync<Bin>(bin);
var payload = entity.EntityState;
var result = new JsonObjectContentResult(HttpStatusCode.OK, payload);
return result;
}
view raw 09-get-history.cs hosted with ❤ by GitHub

By doing so, we can see the list of histories stored in the bin.

Reset Webhook Requests

Let's remove all the webhook requests from the bin. Create the bin reference first (line #7).

[FunctionName(nameof(ResetHistory))]
public async Task<IActionResult> ResetHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route="bins/{binId}/reset")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
var bin = new EntityId("Bin", binId);
...
}
view raw 10-reset-history.cs hosted with ❤ by GitHub

Call the SignalEntityAsync() method again, but this time it invokes the actor behaviour of Reset() (line #9).

[FunctionName(nameof(ResetHistory))]
public async Task<IActionResult> ResetHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route="bins/{binId}/reset")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
...
await client.SignalEntityAsync<IBin>(bin, o => o.Reset());
...
}
view raw 11-reset-history.cs hosted with ❤ by GitHub

Check the Table Storage, and you will be able to see the empty array in the history field.

Delete Bin

If you don't need the bin itself, then delete the bin completely. Create the bin reference first (line #7).

[FunctionName(nameof(PurgeBin))]
public async Task<IActionResult> PurgeBin(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route="bins/{binId}/purge")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
var bin = new EntityId("Bin", binId);
...
}
view raw 12-purge-bin.cs hosted with ❤ by GitHub

This time, call the PurgeInstanceHistoryAsync() method that completely remove the entity from the Table Storage (line #9).

[FunctionName(nameof(PurgeBin))]
public async Task<IActionResult> PurgeBin(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route="bins/{binId}/purge")] HttpRequest req,
[DurableClient] IDurableClient client,
string binId)
{
...
await client.PurgeInstanceHistoryAsync($"@{bin.EntityName}@{bin.EntityKey}");
var result = new NoContentResult();
return result;
}
view raw 13-purge-bin.cs hosted with ❤ by GitHub

Once completed, all records related to the entity have gone.


So far, we've implemented a very simple RequestBin app, using Durable Functions. If we add UI, it will be more elegant. The point of this exercise is to experiment with the "Stateful-ness" of Durable Functions not just for orchestration purpose but also for direct handling purpose. I hope this experiment can give you a more useful idea for your work.