State Management in Azure Service Fabric

Introduction

This article uses code from my previous article Implementing Actor Model in Azure Service Fabric

Road Map

  • Creating Checkout Service
  • Testing Using Postman
  • Use Cases

Creating Checkout service

In the last module, we built a user actor service on top of the actor framework, making user baskets extremely scalable. There is one last piece left to do, checkout service. Checkout service, just like the product catalog, is a stateful service. Its purpose is to make the user basket perform the checkout process. This simply means taking stock from the product catalog service, generating the receipt, and clearing the basket.

Step 1. Create a new service fabric Stateful service named CheckoutService.

CheckoutService

Fabric service

Step 2. Now create a new .Net standard library project and name it Ecommerce.CheckoutService.Model.

Library project

Step 3. Create an interface named ICheckoutService in this project.

public interface ICheckoutService : IService {
}

Step 4. Install the missing package.

Step 5. Create a new class, CheckoutSummary, on the same project.

public class CheckoutSummary {
    public List<CheckoutProduct> Products { get; set; }
    public double TotalPrice { get; set; }
    public DateTime Date { get; set; }
}

CheckoutSummary will hold the result of the checkout process and will include the total price of all the products we check out, the date of the checkout, and the list of checkout products. The CheckoutProduct class is not defined yet. I'll add it here as a simple class.

Step 6. Create a new class CheckoutProduct in the same Model project as shown.

public class CheckoutProduct {
    public Product Product { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

The CheckoutProduct contains a few fields: quantity of the number of products bought, a price we paid for this purchase, and a reference to the original product. Now, this original product is contained within the ProductCatalog service we've created before; therefore, add a reference to this project from the checkout service model product. So now we're all good.

Now I will define two operations for our checkout service. This should be an interface, of course. One of them is called checkout, which, given a user ID, will return us a checkout summary. And the other one will return the order history for this user in the form of a collection of checkout summaries.

Step 7. Add the following code to the ICheckoutService interface.

public interface ICheckoutService : IService {
    Task<CheckoutSummary> CheckoutAsync(string userId);
    Task<CheckoutSummary[]> GetOrderHistoryAsync(string userId);
}

Step 8. Now go to the e-commerce.API project and create a new controller CheckoutController. Add the following code.

private static readonly Random rnd = new Random(DateTime.UtcNow.Second);

[Route("{userId}")]
public async Task<ApiCheckoutSummary> CheckoutSummary(string userId)
{
    CheckoutSummary summary = await GetCheckoutService().CheckoutAsync(userId);
    return ToApiCheckoutSummary(summary);
}

[Route("history/{userId}")]
public async Task<IEnumerable<ApiCheckoutSummary>> GetHistoryAsync(string userId)
{
    IEnumerable<CheckoutSummary> history = await GetCheckoutService().GetOrderHistoryAsync(userId);
    return history.Select(ToApiCheckoutSummary);
}

Step 9. Add the two classes ApiCheckoutProduct and ApiCheckoutSummaryto the model folder of Ecommerce.API project.

public class ApiCheckoutProduct {
    [JsonProperty("productId")]
    public Guid ProductId { get; set; }
    [JsonProperty("productname")]
    public string ProductName { get; set; }
    [JsonProperty("quantity")]
    public int Quantity { get; set; }
    [JsonProperty("price")]
    public double Price { get; set; }
}
public class ApiCheckoutSummary {
    [JsonProperty("products")]
    public List<ApiCheckoutproduct> Products { get; set; }
    [JsonProperty("date")]
    public DateTime Date { get; set; }
    [JsonProperty("totalprice")]
    public double TotalPrice { get; set; }
}

ApiCheckoutProduct simply contains the product ID, product name, quantity, and price. This is very similar to the checkout product entity in our other model project. Furthermore, ApiCheckoutSummary contains the list of products, the total price, and the date of the checkout. In this controller, we have just two methods. The first one is checkout, which simply forwards the call to the checkout service, calls the identical checkout method, and converts it to the ApiCheckoutSummary. As you can see, it's a simple conversion between two classes. The second one is GetHistory, which also forwards calls to the GetOrderHistory method on the checkout service class and, again, converts it to the ApiCheckoutSummary.

Step 10. Add the following three functions to CheckoutController.

private ApiCheckoutSummary ToApiCheckoutSummary(CheckoutSummary model) {
    return new ApiCheckoutSummary {
        Products = model.Products.Select(p => new ApiCheckoutProduct {
            ProductId = p.ProductId,
            ProductName = p.ProductName,
            Price = p.Price,
            Quantity = p.Quantity
        }).ToList(),
        Date = model.Date,
        TotalPrice = model.TotalPrice
    };
}

private ICheckoutService GetCheckoutService() {
    long key = LongRandom();
    var proxyFactory = new ServiceProxyFactory(c => new FabricTransportServiceRemotingClientFactory());
    return proxyFactory.CreateServiceProxy<ICheckoutService>(new Uri("fabric:/Ecommerce/Ecommerce.ProductCatalog"), new ServicePartitionKey(key));
}
private long LongRandom() {
    byte[] buf = new byte[8];
    rnd.NextBytes(buf);
    long longRand = BitConverter.ToInt64(buf, 0);
    return longRand;
}

Now, let's implement the checkout service. We've already done it before with other services, but I'll speed through it again. First of all, I'll delete the RunAsync method since we simply don't need it, remove the unnecessary comments, and as you remember, in order to expose an endpoint, we need to replace CreateServiceReplicaListener with this identical code we've written before. Of course, in order for that to work, you need to either reference the Microsoft.ServiceFabric.Services.Remoting Library.

protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListener() {
    return new[]{
        new ServiceReplicaListener(context => new FabricTransportServiceRemotingListener(context, this))
    };
}

Step 11. Derive the CheckoutService Class from the ICheckoutService. Of course, Visual Studio doesn't know about it, so we need to add a reference to the model project.

And now let's simply implement this interface. We've got two methods, Checkout and GetOrderHistory, which are not implemented yet. Let's implement them and explain how it works.

First, we will create the CheckoutSummary class instance which holds the results of the checkout, sets the date, and initializes the product with an empty collection. Before we check out, we need to get the user basket. Therefore, we must get a reference to the IUserActor.

To get the user actor, I wrote a helper method, GetUserActor, which simply creates an actor proxy, calling the UserActor service. Then we need the IProductCatalog service. This is contained within ECommerce.Product catalog.Model Project and again, I was able to write a trivial helper method for this. It simply creates a service proxy for the product catalog service.

Now that we have both user basket and catalog service, what we would like to do in order to proceed to the checkout is to enumerate through the basket. For each basket line, get a product from the basket, create the CheckoutProduct entity, and fill in all the required fields (i.e. product, price, quantity, calculate the total price, clear the basket, and add this purchase to the history). To add the product to the history, I wrote another helper method. All it does is create an instance of a reliable collection called history, open a transaction, and add the CheckoutSummary to that history. That will help us later retrieve the user's purchasing history through the same API.

Step 12. Implement the CheckoutAsync method as follows.

public async Task<CheckoutSummary> CheckoutAsync(string userId)
{
    var result = new CheckoutSummary();
    result.Date = DateTime.UtcNow;
    result.Products = new List<CheckoutProduct>();
    IUserActor userActor = GetUserActor(userId);
    BasketItem[] basket = await userActor.GetBasket();
    IProductCatalogService catalogService = GetProductCatalogService();
    foreach (BasketItem basketLine in basket)
    {
        Product product = await catalogService.GetProductAsync(basketLine.ProductId);
        var checkoutProduct = new CheckoutProduct
        {
            Product = product,
            Price = product.Price,
            Quantity = basketLine.Quantity
        };
        result.Products.Add(checkoutProduct);
    }
    result.TotalPrice = result.Products.Sum(p => p.Price);
    await userActor.ClearBasket();
    await AddToHistoryAsync(result);
    return result;
}

Step 13. Add the following helper methods.

private IUserActor GetUserActor(string userId)
{
    return ActorProxy.Create<IUserActor>(new ActorId(userId), new Uri("fabric:/Ecommerce/UserActorService"));
}
private IProductCatalogService GetProductCatalogService()
{
    var proxyFactory = new ServiceProxyFactory(c => new FabricTransportServiceRemotingClientFactory());
    return proxyFactory.CreateServiceProxy<IProductCatalogService>(new Uri("fabric:/Ecommerce/Ecommerce.ProductCatalog"), new ServicePartitionKey(0));
}
private async Task AddToHistoryAsync(CheckoutSummary checkout) {
    IReliableDictionary<DateTime, CheckoutSummary> history = await StateManager.GetOrAddAsync<IReliableDictionary<DateTime, CheckoutSummary>>("history");
    using (ITransaction tx = StateManager.CreateTransaction()) {
        await history.AddAsync(tx, checkout.Date, checkout);
        await tx.CommitAsync();
    }
}

Now, implementing order history is easy considering we've already written the history item for the history collection. Of course, this method has to be async. We get the reference to the history collection, create a transaction, simply enumerate through all of the items in the history collection, and return it as a list of checkout summaries.

Step 14. Implement GetOrderHistoryAsync as follows.

public async Task<CheckoutSummary[]> GetOrderHistoryAsync(string userId)
{
    var result = new List<CheckoutSummary>();
    IReliableDictionary<DateTime, CheckoutSummary> history = await StateManager.GetOrAddAsync<IReliableDictionary<DateTime, CheckoutSummary>>("history");
    using (ITransaction tx = StateManager.CreateTransaction())
    {
        IAsyncEnumerable<KeyValuePair<DateTime, CheckoutSummary>> allProducts = await history.CreateEnumerableAsync(tx, EnumerableMode.Unordered);
        using (IAsyncEnumerator<KeyValuePair<DateTime, CheckoutSummary>> enumerator = allProducts.GetAsyncEnumerator())
        {
            while (await enumerator.MoveNextAsync(CancellationToken.None))
            {
                KeyValuePair<DateTime, CheckoutSummary> current = enumerator.Current;
                result.Add(current.Value);
            }
        }
    }
    return result.ToArray();
}

Test Using Postman

We can test it by launching our application, and again using this wonderful Postman tool. We'll start by calling the product's API to get the product list from the product catalog.

 API

Let's add a product to the user's basket by calling the Post method.

Post method

Now, we perform a checkout for user 1. And as you can see, we have the checkout summary. Additionally, we still have the price, date, and list of products. All seems to be correct, so this is done. That’s how we manage the state in service fabric.

Send

Applicable Cases

  • Avoid complex systems involving dependencies and coordinating shared state
  • When you want to avoid using explicit locks to protect the shared state
  • Classic synchronization problems like the dining philosopher's and the sleeping barber's problem
  • Highly available services
  • Scalable services

Summary

In this article, we learned about managing state in Azure Service Fabric.