We will be building a fully functional “microservice” that will be able to add, remove, and return a list of products. For this example we will model the way this microservice will be consumed will be done via REST. Get your coding hat on, we are about to start.
The setup
Before we begin you should have a new function project setup and the durable functions set-up. If you are not very sure about how to achieve this, here is a link to the official documentation.
Pro tip: in some IDEs (I use Rider) you have the option on the project setup to chose durable functions, so you don’t need to install the required NuGet packages by hand.
Durable entities and the actor pattern
So, since you found this article via searching or by clicking somewhere on social media, you should already know what durable entities are, or at least have an idea about them. But, just to be sure that we are all on the same page we will be doing a small introduction on durable entities, and why, when, and where we might be able to benefit from them.
“Durable entities” or “entity functions”, how they are called nowadays, are mostly inspired by the virtual actors pattern from the Orleans project, which in term are quite inspired by Akka/ Akka.net. Going into this topic is well outside of our scope, but you will find an introduction in them in the links provided, also I highly recommend looking a bit into them, because understanding the strengths and weaknesses of the actor pattern will help you when trying to determine if the durable entities might be a good match for you or should you use a more traditional SQL or NoSql storage.
Well, probably you haven’t clicked any of the links above … in case you did, well done!
Anyway, long story short, an actor is an isolated construct that holds it’s own state and has its behavior, they do not share memory and the communication is done asynchronously using some kind of message box/queues. They are kept in memory while in use and when disposed they are usually serialized to disk. They can run in grate numbers, and depending on the implementation they can be hierarchical. In general actor, systems are self-healing.
So, more or less the durable entities are some kind of actors. They are very good at keeping track of some small to mid-sized objects state and behavior, and also quite interestingly they hold the state in the table storage of the storage account that is used by the function app. This can be seen as an advantage through the lens of scalability and availability since both of these are handled automatically by Azure storage. Also, what is quite important to know is that the entities themselves are event-sourced.
Before we dive in the actual code there is one more thing I still need to address, why use entities for a shopping cart rather than using a classic SQL or NoSql DB. And this is quite a good question. As I usually say, almost all the things could be built in a lot of ways, each of them having particular benefits and downsides, so next, we will be diving in and see why it might be beneficial to use entities rather than SQL or CosmosDB.
We are operating on the assumption that azure functions are running on a consumption plan.
So one of the most appealing parts of the whole serverless movement and compute offer is the near “infinite” auto-scaling. So, by utilizing something that is also “serverless” for holding the state seems to be a good match. One would argue that CosmosDB is also serverless, but although the connection between Cosmos and functions could be done via binding it still is not as seamless as with the entities functions. Regarding SQL, well it is kind of evident why something that can scale as much and as fast as required should not be paired with something that isn’t able to scale as fast.
Also, one of the benefits of actors in general, and in our case durable entities, is that they are kept in memory for a time whiteout being offloaded, using them we avoid the latency of a round trip to the DB, since we are essentially using in-memory objects, that get serialized and serialized behind the scene.
The Code
Regarding the overall design, the microservice will expose a restful endpoint with the following API methods:
AddItemToCart: [POST] http://localhost:7071/api/cart/{id}/add
GetCartForSession: [GET] http://localhost:7071/api/cart/{id}
RemoveItemFromCart: [POST] http://localhost:7071/api/cart/{id}/remove
As you can see here, there is an {id} that should be a GUID generated by the client, this way in theory this type of microservice could be used with any client software, without the need of knowing anything about it. Regarding the GUID type, this could be changed to a regular string, but this way ensures against collisions.
The heart of the whole demo is the durable entity, which is defined as follows:
[JsonObject(MemberSerialization.OptIn)]
public class ShoppingCartEntity : IShoppingCart
{
[JsonProperty("list")]
private List<CartItem> list { get; set; } = new List<CartItem>();
public void Add(CartItem item)
{
// Get existing
var existingItem = this.list.FirstOrDefault(i => i.ProductId == item.ProductId);
if (existingItem == null)
{
this.list.Add(item);
}
else
{
existingItem.Count += item.Count;
}
}
public void Remove(CartItem item)
{
var existingItem = this.list.FirstOrDefault(i => i.ProductId == item.ProductId);
if (existingItem == null)
{
return;
}
if (existingItem.Count > item.Count)
{
existingItem.Count -= item.Count;
}
else
{
this.list.Remove(existingItem);
}
}
public Task<ReadOnlyCollection<CartItem>> GetCartItems()
{
return Task.FromResult(this.list.AsReadOnly());
}
[FunctionName(nameof(ShoppingCartEntity))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
=> ctx.DispatchAsync<ShoppingCartEntity>();
}
After a glance at the code, some interesting things could be noticed. It looks more or less like a regular domain entity with properties and behavior. Also, the property that keeps the actual state is private (I wouldn’t go as far as saying encapsulated) and only modified using the exposed public methods, and also it’s content is also returned via a public method. One thing that we need to keep in mind is that a method of an entity function can only return void, Task or Task, nothing else.
For the state container we also use the following class:
public abstract class CartItem
{
public Guid ProductId { get; set; }
public int Count { get; set; }
}
In this class*, we could have all the imaginable properties as long as they are serializable*, for sake of the example I tried to keep it as short as possible, but here you could have also the price when added, or the name and description, even a link to a product image or whatever.
Now that we have set up the entity itself, we will manipulate it using rest triggers, again for simplicity’s sake, the recommended way of interacting with entities is using orchestrator/durable functions, but the interactions are required in our example could be done directly from the triggers.
So for adding an item to the cart here is the trigger code:
[FunctionName("AddItemToCart")]
public static async Task<ActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "cart/{id}/add")] HttpRequestMessage req,
Guid id,
[DurableClient] IDurableEntityClient client )
{
if (id == Guid.Empty)
{
return (ActionResult) new BadRequestObjectResult("Id is required");
}
var entityId = new EntityId(nameof(ShoppingCartEntity), id.ToString());
var data = await req.Content.ReadAsAsync<CartItem>();
await client.SignalEntityAsync<IShoppingCart>(entityId, proxy => proxy.Add(data));
return (ActionResult) new AcceptedResult(); // req.CreateResponse(HttpStatusCode.Accepted);
}
So, here we are using a command approach, with a simple accepted response.
For removing an item from the cart, the code is remarkably similar:
[FunctionName("RemoveItemFromCart")]
public static async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "cart/{id}/remove")] HttpRequestMessage req,
Guid id,
[DurableClient] IDurableClient client, ILogger log)
{
if (id == Guid.Empty)
{
return (ActionResult) new BadRequestObjectResult("Id is required");
}
var entityId = new EntityId(nameof(ShoppingCartEntity),id.ToString());
var data = await req.Content.ReadAsAsync<CartItem>();
await client.SignalEntityAsync<IShoppingCart>(entityId, proxy => proxy.Remove(data));
return (ActionResult) new AcceptedResult();
}
And for retrieving the elements from a cart:
[FunctionName("GetCartForSession")]
public static async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "cart/{id}")] HttpRequestMessage req,
Guid id,
[DurableClient] IDurableClient client,
ILogger log)
{
if (id == Guid.Empty)
{
return (ActionResult) new BadRequestObjectResult("The ID is required");
}
var entityId = new EntityId(nameof(ShoppingCartEntity), id.ToString());
var stateResponse = await client.ReadEntityStateAsync<ShoppingCartEntity>(entityId);
if (!stateResponse.EntityExists)
{
return (ActionResult) new NotFoundObjectResult("No cart with this id");
}
var response = stateResponse.EntityState.GetCartItems();
return (ActionResult) new OkObjectResult(response.Result);
}
And more or less, this is it. We now have a serverless microservice that could scale on demand.
Before we end this, I just want to remind you that this is a high level presentation of the concept, if you have any question, or would like to see more content about this feel free to add a comment on reach out to me on twitter.
As usual, you could get a closer look at the code on this github repository.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.