State management in Azure Service Fabric

In this module, we are going to sharpen our Service Fabric skills by understanding how state management works internally.

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 to take stock from product catalog service, generate the receipt, and clear the basket.
 
Step 1: Create a new service fabric Stateful service named CheckoutService.
 
image1
 
image2
 
Step 2: Now create a new .Net standard library project and name it Ecommerce.CheckoutService.Model.
 
image3
 
Step 3: Create an interface named ICheckoutService in this project.
  1. public interface ICheckoutService : IService{   
  2. }   
Step 4: Install the missing package.
 
Step 5: Create a new class, CheckoutSummary, on the same project.
  1. public class CheckoutSummary{    
  2.     public List<CheckoutProduct> Products{get;set;}    
  3.     public double TotalPrice {get;set;}    
  4.     public Datatime Date {get;set;}    
  5. }   
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
  1. public class CheckoutProduct{    
  2.     public Product prodcut{get;set;}    
  3.     public int Quantity {get;set;}    
  4.     public double Price {get;set;}    
  5. }   
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:
  1. public interface ICheckoutSerivce:IService{    
  2.        Task<CheckoutSummary> CheckoutAsync(string userId);    
  3.        Task<CheckoutSummary[]> GetOrderHistoryAsync(string userId);    
  4. }   
Step 8: Now go to the Ecommerce.API project and create a new controller CheckoutController. Add the following code:
  1. private static readonly Random rnd = new Random(DateTime.UtcNow.Second);    
  2. [Route(“{userId}”)]    
  3. public async task<ApiCheckoutSummary> CheckoutSummary(string userId){    
  4.     CheckoutSummary summary = await GetCheckoutService().CheckoutAsync(userId);    
  5.     return ToApiCheckoutSummary(summary);    
  6. }    
  7. [Route(“history/{userId}”)]    
  8. public async task<IEnumerable<ApiCheckoutSumamry>>GetHistoryAsync(string userId){    
  9.     IEnumerable<CheckoutSummary> history = await GetChecoutService().GetOrderHistoryAsync(userId);    
  10.     return history.Select(ToApiCheckoutSummary);    
  11. }   
Step 9: Add the two classes ApiCheckoutProduct and ApiCheckoutSummary to the model folder of Ecommerce.API project.
  1. public class ApiCheckoutProduct{    
  2. [JsonProperty(“productId”)]    
  3. public Guid ProductId{get;set;}    
  4. [JsonProperty(“productname”)]    
  5. public string ProductName {get;set;}    
  6. [JsonProperty(“quantity”)]    
  7. public int Quantity{get;set;}    
  8. [JsonProperty(“price”)]    
  9. public double Price{get;set;}    
  10. }   
  1. public class ApiCheckoutSummary{    
  2. [JsonProperty(“products”)]    
  3. public List<ApiCheckoutproduct> Products{get;set;}    
  4. [JsonProperty(“date”)]    
  5. public DateTime Date {get;set;}    
  6. [JsonProperty(“totalprice”)]    
  7. public double TotalPrice{get;set;}    
  8. }   
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:
  1. private ApiCheckoutSummary ToApiCheckoutSummary(CheckoutSummary model){    
  2.     return new  ApiCheckoutSummary{    
  3.     Products = model.Products.Select(p=> new ApiCheckoutProduct{    
  4.         ProductId = p.ProductId,ProductName = p.ProductName, Price = p.Price, Quantity = p.Quantity}).ToList(), Date = model.Date, TotalPrice= model.TotalPrice};    
  5. }    
  6. private ICheckoutService GetCheckoutService(){    
  7.     long key = LongRandom();    
  8.     var proxyFactory = new ServiceProxyFactory(c=> new FabricTransportServiceRemotingClientFactory());    
  9.     return proxyFactory.CreateServiceProxy<ICheckoutService>(new Uri(“fabric:/Ecommerce/Ecommerce.ProductCatalog”),new ServicePartitionKey(key));    
  10. }   
  1. private long LongRandom(){    
  2.     byte[] buf = new byte[8];    
  3.     rnd.NextBytes(buf);    
  4.     long longRand = BitConverter.ToInt64(buf, 0);    
  5.     return longRand;    
  6. }   
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
  1. protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListener(){  
  2.     return new[]{  
  3.         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. Lets 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 IProductCatalog service. This is contained within ECommerce.ProductCatalog.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 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 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:
  1. public async Task<CheckoutSummary> CheckoutAsync(string userId){  
  2.     var result = new CheckoutSummary();  
  3.     result.Date = DateTime.UtcNow;  
  4.     result.Products = new List<CheckoutProduct>();  
  5.     IUserActor userActor = GetUserActor(userId);  
  6.     BasketItem[] basket = await userActor.GetBasket();  
  7.     IProductCatalogService catalogService = GetProductCatalogService();  
  8.     foreach(BasketItem basketLine in basket){  
  9.         Product product = await CatalogService.GetProductAsync(basketLine.ProductId);  
  10.         var checkoutProduct= new CheckoutProduct{  
  11.             Product = product,  
  12.             Price = product.Price  
  13.             Quantity = basketLine.Quantity  
  14. };   
  15. result.Products.Add(checkoutProduct)  
  16. }  
  17. result.TotalPrice = result.Products.Sum(p=>p.price);  
  18. await userActor.ClearBasket();  
  19. await AddToHistoryAsync(result);  
  20. return result;  
Step 13: Add the following helper methods:
  1. private IUserActor GetUserActor(string userId){  
  2.     return ActorProxy.Create<IUserActor>(new ActorId(userId), new Uri(“fabric:/Ecommerce/UserActorService”));  
  1. private IProductCatalogService GetProductCatalogService(){  
  2.     var proxyFactory = new ServiceProxyFactory(c=> new FabricTransportServiceRemotingClientFactory());  
  3.     return proxyFactory.CreateServiceProxy<IProductCatalogService>( new Uri(“fabric:/Ecommerce/Ecommerce.ProductCatalog”), new ServicePartitionKey(0));  
  1. private async Task AddToHistoryAsync(CheckoutSummary checkout){  
  2.     IReliableDictionary<DateTime, CheckoutSummary> history =  await StateManager.GetOrAddAsync<IReliableDictionary<Datetime, CheckoutSummary>>(“history”);  
  3.     using(ITransaction tx = StateManager.CreateTransaction()){  
  4.         await history.AddAsync(tx, checkout.Date, checkout);  
  5.         await tx.CommitAsync();  
  6.     }  
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 summary.
 
Step 14: Implement GetOrderHistoryAsync as follows:
  1. public async Task<CheckoutSummary[]> GetOrderHistoryAsync(string userId){  
  2.     var result = new List<CheckoutSummary>();  
  3.     IReliableDictionary<DateTime, CheckoutSummary> history = await StateManager.GetOrAddAsync<IReliableDictionary<Datetime, CheckoutSummary>>(“history”);  
  4. using (ITransaction tx = StateManager.CreateTransaction()){  
  5.     IAsyncEnumerable<KeyValuepair<DateTime, CheckoutSummary>> allProducts = await history.CreateEnumerableAsync(tx , EnumerableMode.Unordered);  
  6.     using(IAsyncEnumerator<KeyValuePair<DateTime, CheckoutSummary>> enumerator = allProducts.GetAsyncEnumerator()){  
  7.         while(await enumerator.MoveNextAsync(CancellationToken.None)){  
  8.             KeyValuePair<DateTime, CheckoutSummary> current = enumerator.Current;  
  9.             result.Add(current.Value);  
  10.         }  
  11.     }  
  12. }  
  13. 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.
 
output-image1
 
Let's add a product into the user's basket by calling the Post method.
 
output-image2 
 
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.
 
output-image3
 

Applicable Cases

  • Avoid complex system involving dependencies and coordinating shared state
  • When you want to avoid using explicit locks to protect the shared state
  • Classic synchronization problems like 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.