Search

Using Hazelcast in an ASP.NET Core Application

Hazelcast is an open-source in-memory computing product. It provides caching, distributed processing, and distributed messaging with excellent speed, scalability, and security. In-memory data grids (IMDGs) allow applications to quickly perform big data operations compared to conventional databases and data store technologies.

This article demonstrates how to install and set up a local Hazelcast cluster that works as a server. We highlight how to create an ASP.NET Core application to connect to and access Hazelcast in a client-server fashion.

We’ll create a sample shopping application to demonstrate how this setup works. You should know C# to follow along.

Installing and Starting a Local Hazelcast Cluster

We need to begin by creating a local cluster on our development machine.

First, access the Get Started page and download either a TAR or a ZIP file. You can install and run a Hazelcast cluster on Linux, macOS, or Windows machines. You can also download a Docker image to run Hazelcast.

Following the Get Started page, run the appropriate command (bin/start.bat in Windows or bin/start.sh in Linux) to create and start a new cluster.

If we run the start command three times, Hazelcast starts three members that automatically discover each other and form a cluster — even if they’re running locally.

Using the ASP.NET Core Demo Web App

To use the ASP.NET Web app, follow these steps:

  1. Install Visual Studio 2019 Community Edition or superior.
  2. Download the working code from this GitHub repository to follow this article.
  3. Open the downloaded solution in Visual Studio, where you see two ASP.NET Core projects with .NET 5. These two projects are similar, representing two versions of the same application that simulates e-commerce.

Using a Wrapper Class for the Hazelcast .NET Client

The ECommerce and ECommerce-Hazelcast projects have two different wrapper classes encapsulating the data access functionalities. They are ECommerceData and ECommerceDataHazelcast.

The ECommerceData class uses the standard Dictionary and Queue classes:

				
					public interface IECommerceData : IBaseECommerceData
{
  void Initialize();
  List<CartItem> GetCartItems();
  void AddCartItem(CartItem cartItem);
  void Checkout();
  List<Order> OrdersAwaitingPayment();
  List<Order> OrdersForDelivery();
  List<Order> OrdersRejected();
  void ApprovePayment();
  void RejectPayment();
}
				
			

The ECommerceDataHazelcast wrapper class replaces these standard classes with the IHMap and the IHQueue structures from Hazelcast that mirror the IEcommerceData interface. The difference is that the latter uses tasks and async in methods because Hazelcast exposes asynchronous operations.

				
					public interface IECommerceData : IBaseECommerceData
{
  void Initialize();
  List<CartItem> GetCartItems();
  void AddCartItem(CartItem cartItem);
  void Checkout();
  List<Order> OrdersAwaitingPayment();
  List<Order> OrdersForDelivery();
  List<Order> OrdersRejected();
  void ApprovePayment();
  void RejectPayment();
}
				
			

Hazelcast .NET Client Startup and Shutdown

To properly use Hazelcast in ASP.NET Core, we need to deal with the Hazelcast .NET client’s lifecycle events. Our ECommerceDataHazelCast wrapper class exposes a method that initializes the Hazelcast map. Note how the Program class in the ECommerce-Hazelcast project calls the InitializeAsync method.

				
					public static async Task Main(string[] args)
{
  var webHost = CreateHostBuilder(args).Build();
  await SetupHazelCast(webHost);
  webHost.Run();
}

private static async Task SetupHazelCast(IHost webHost)
{
  await webHost.Services.GetRequiredService<IECommerceDataHazelCast>().InitializeAsync();
}
				
			

The Configure method of Startup.cs registers the ASP.NET Core application lifecycle event triggered when our web app stops. We obtain an instance of IECommerceDataHazelCast to call the ShutdownAsync method.

				
					hostApplicationLifetime.ApplicationStopping.Register(() =>
{
  using (var scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
  {
    var eCommerceDataHazelCast = scope.ServiceProvider.GetService<IECommerceDataHazelCast>();
    eCommerceDataHazelCast.ShutdownAsync().Wait();
  }
});
				
			

Dependency Injection for the Wrapper Class

Although we can use Hazelcast as a distributed cache, we want a more straightforward approach: using ASP.NET Core to use Hazelcast directly.

We don’t want to create a new connection to Hazelcast every time we call it. That is why we’re registering the Hazelcast client wrapper class in the ASP.NET Core dependency injection (DI) as a singleton. We want to ensure we can connect it to a Hazelcast cluster once and reuse the connection.

The code below demonstrates how we call AddSingleton in the ConfigureServices method of the Startup class to add a singleton service of the IECommerceDataHazelCast type:

				
					public void ConfigureServices(IServiceCollection services)
{
  services.AddRazorPages();
  services.AddSingleton<IECommerceDataHazelCast, ECommerceDataHazelCast>();
}
				
			
To start a new instance of the Hazelcast client with the automatic options, we call the StartNewClientAsync method of HazelcastClientFactory. Here’s how we implement it in the StartAsync method of the ECommerceDataHazelCast class:
				
					private void StartAsync()
{
  var options = HazelcastOptions.Build();
  // create an Hazelcast client and connect to a server running on localhost
  this.hazelcastClient = HazelcastClientFactory.StartNewClientAsync(options).Result;
}

				
			
At the end of the ASP.NET Core Web app’s lifecycle, we call the methods to destroy both the IHMap and the IHQueue instances we created:
				
					public async Task ShutdownAsync()
{
  // destroy the map
  await hazelcastClient.DestroyAsync(cartItemsMap);
  // destroy the queue
  await hazelcastClient.DestroyAsync(ordersAwaitingPaymentQueue);

  Debug.WriteLine("ShutdownAsync finished successfully.");
}

				
			

Using a Hazelcast Map in .NET

When we open the ECommerce project in this solution, built using only standard .NET structures, we notice the shopping cart items are Dictionary<int, CartItem>:

				
					private Dictionary<int, CartItem> cartItems;
				
			

The Dictionary<TKey, TValue> (where the TKey represents the product ID) ensures that the shopping cart doesn’t contain duplicate items of the same product.

In Hazelcast.NET, IHMap represents a distributed map, which is the equivalent to the standard Dictionary<TKey, TValue> .NET collection:

				
					private IHMap<int, CartItem> cartItemsMap;
				
			

Now let’s see how we can seamlessly write to and read from IHMap using .NET code.

Set the ECommerce-Hazelcast project as the solution’s default project and run the app. The Index.cshtml Razor page displays a set of predefined items already in the customer’s shopping cart:

The code obtains these shopping cart items when the user requests the Razor page:

				
					public async Task OnGetAsync()
{
  await InitializePageAsync();
}

private async Task InitializePageAsync()
{
  this.CartItems = await eCommerceData.GetCartItemsAsync();
}

				
			

How does the page obtain the eCommerceData instance? Note that we already registered the IECommerceDataHazelCast interface as a singleton in Startup.cs, so we just provide it as a parameter for the Razor page constructor:

				
					public IndexModel(ILogger<IndexModel> logger, IECommerceDataHazelCast eCommerceData)
{
  this.logger = logger;
  this.eCommerceData = eCommerceData;
}
				
			
The InitializePageAsync method above requests the GetCartItemsAsync method in the wrapper class. This method obtains all cart items from cartItemsMap by converting the IHMap into a list.
				
					private IHMap<int, CartItem> cartItemsMap;

public async Task InitializeAsync()
{
  // Get the Distributed Map from Cluster.
  cartItemsMap = await hazelcastClient.GetMapAsync<int, CartItem>("distributed-cartitem-map");
}

public async Task<List<CartItem>> GetCartItemsAsync()
{
  return (await cartItemsMap.GetValuesAsync()).ToList();
}
				
			
The GetMapAsync method of the Hazelcast .NET Client API first initialized the cartItemsMap variable before its use. Now, look at the buttons at the bottom of the page:

The Check out and the Add to Cart buttons execute form actions that redirect the user to the AddToCart Razor page or the checkout operation, respectively.

Here’s how ECommerceDataHazelCast.cs implements the CheckoutAsync method:

				
					public async Task CheckoutAsync()
{
  int orderId = ++MaxOrderId;

  var cartItems = await cartItemsMap.GetValuesAsync();
  var order = new Order(orderId, DateTime.Now, cartItems.Count, cartItems.Sum(i => i.Quantity * i.UnitPrice));
  await ordersAwaitingPaymentQueue.PutAsync(order);
  await cartItemsMap.ClearAsync();
}
				
			

The Add to Cart button redirects users to the AddToCart.cshtml Razor page:

The AddToCart.cshtml page has two elements that trigger AJAX requests: quantity input and product selection. Changing either of these elements causes AJAX to invoke the OnGetCartItem server-side endpoint in AddToCart.cshtml.cs with the recalculated total:

				
					public JsonResult OnGetCartItem(CartItem cartItem)
{
  var product = eCommerceData.GetProductList().Where(p => p.Id == cartItem.ProductId).Single();
  var newItem = new CartItem(cartItem.Id, product.Id, product.Icon, product.Description, product.UnitPrice, cartItem.Quantity);
  return new JsonResult(newItem);
}
				
			

Once the user clicks Confirm, the item, quantity, and product appear on the Razor page, invoking AddCartItemAsync on the ECommerceDataHazelcast client wrapper class. This action calls PutAsync on the IHMap, representing the shopping cart items map. The PutAsync method asynchronously puts the cart item key and the item value in the cartItemsMap map.

				
					private IHMap<int, CartItem> cartItemsMap;

public async Task AddCartItemAsync(CartItem cartItem)
{
  var product = GetProductList().Where(p => p.Id == cartItem.ProductId).Single();
  var newItem = new CartItem(cartItem.Id, product.Id, product.Icon, product.Description, product.UnitPrice, cartItem.Quantity);
  await cartItemsMap.PutAsync(newItem.ProductId, newItem);
}
				
			
Note that the code calls PutAsync using newItem.ProductId as the item key. So, if we add another item to the shopping cart using an existing key, PutAsync replaces the current item’s value instead of adding a duplicate item.

Using Hazelcast IHQueue in .NET

If we open the ECommerce project in this solution, built using only standard .NET structures, the shopping cart items are Queue<Order>.
				
					private Queue<Order> ordersAwaitingPayment;
				
			

Queue<T> (where the TKey represents the Order object) ensures that the code processes orders awaiting payment in a “first-in-first-out” sequence.

In Hazelcast.NET, the IHQueue structure represents a distributed, non-partitioned, and visual queue, which is the equivalent to the standard Queue<T> .NET collection:

				
					private IHQueue<Order> ordersAwaitingPaymentQueue;
				
			

Now let’s see how we can quickly fill and consume an IHQueue using .NET code.

When the shopping cart is complete, a user clicks Check out to post a new order:

The AddToCartModel class then invokes the ECommerceDataHazelcast wrapper class to call the PutAsync method on the ordersAwaitingPaymentQueue queue.

				
					public async Task CheckoutAsync()
{
  int orderId = ++MaxOrderId;

  var cartItems = await cartItemsMap.GetValuesAsync();
  var order = new Order(orderId, DateTime.Now, cartItems.Count, cartItems.Sum(i => i.Quantity * i.UnitPrice));
  await ordersAwaitingPaymentQueue.PutAsync(order);
  await cartItemsMap.ClearAsync();
}
				
			

This code also calls the ClearAsync method on the cartItemsMap object to clear the shopping cart:

Now click Payment Approval to see how the shopping cart turns into a new order:

The Payment.cshtml Razor page calls the Hazelcast wrapper class to request a list of orders from the ordersAwaitingPaymentQueue queue:
				
					public async Task OnGetAsync()
{
  await InitializePageAsync();
}

private async Task InitializePageAsync()
{
  this.OrdersAwaitingPayment = await eCommerceData.OrdersAwaitingPaymentAsync();
}

				
			

The list of orders is in ECommerceDataHazelcast.cs, as below:

				
					public async Task<List<Order>> OrdersAwaitingPaymentAsync()
{
  var list = await ordersAwaitingPaymentQueue.GetAllAsync();
  return list.OrderByDescending(o => o.Id).ToList();
}
				
			

The Payment.cshtml Razor page allows us to approve or reject orders as submitted. Approved orders go to ordersForDeliveryQueue. Rejected orders go to ordersRejectedQueue IHQueues.

				
					public async Task ApprovePaymentAsync()
{
  var order = await ordersAwaitingPaymentQueue.TakeAsync();
  await ordersForDeliveryQueue.PutAsync(order);
}

public async Task RejectPaymentAsync()
{
  var order = await ordersAwaitingPaymentQueue.TakeAsync();
  await ordersRejectedQueue.PutAsync(order);
}

				
			

Note that the PutAsync and the TakeAsync methods work by adding and removing items to and from a queue, similarly to the Enqueue and Dequeue methods in a .NET Queue collection.

Now click Order Tracking to see the orders prepared for delivery and the orders with rejected payment:

When a user opens the Tracking.cshtml  Razor page, it fills OrdersForDelivery and the OrdersRejected lists with values from the Hazelcast wrapper class:

				
					public async Task OnGetAsync()
{
  await InitializePageAsync();
}

private async Task InitializePageAsync()
{
  this.OrdersForDelivery = await eCommerceData.OrdersForDeliveryAsync();
  this.OrdersRejected = await eCommerceData.OrdersRejectedAsync();
}

				
			

The ECommerceDataHazelcast wrapper class provides delivery and rejected orders from the ordersForDeliveryQueue and the ordersRejectedQueue IHQueues:

				
					public async Task<List<Order>> OrdersForDeliveryAsync()
{
  var list = await ordersForDeliveryQueue.GetAllAsync();
  return list.OrderByDescending(o => o.Id).ToList();
}

public async Task<List<Order>> OrdersRejectedAsync()
{
  var list = await ordersRejectedQueue.GetAllAsync();
  return list.OrderByDescending(o => o.Id).ToList();
}
				
			

Other Hazelcast Data Structures

Besides IHMap and IHQueue, the Hazelcast .NET client can access other Hazelcast distributed objects managed by the Hazelcast cluster. You can start with the demo web app in this article to explore various possibilities for distributed stores:
  • HMultiMap is a distributed key/value store like a .NET Dictionary but can store multiple values in a single key. For example, you can store different values in the same SKU to represent various product colors, versions, or flavors that an e-commerce Web app offers.
  • HReplicatedMap is a distributed key/value store that replicates data for all members in a cluster.
  • HSet is a distributed set store similar to the .NET HashSet, which does not allow duplicates and where you can add values without a key.
  • HTopic is a distributed message-publishing store (pub/sub) that you can use in a notification system alerting e-commerce users as they navigate the website.

Next Steps

In-memory data grids like Hazelcast have cluster-based architectures offering data access with high-speed data processing and low latency. As .NET developers, it’s exciting to learn how to bring the benefits of fast in-memory data grids to our projects. You just saw Hazelcast in action within a realistic ASP.NET Core application. Hazelcast.NET is a helpful member of the .NET community, bringing the power of in-memory data grids to the Microsoft .NET ecosystem. It provides data structures such as distributed maps and queues to integrate into new or existing .NET solutions easily. To learn more about the available Open Source, Cloud, and Enterprise distributions, visit the Get Started and Download page and explore what Hazelcast can do for your organization. Happy Hazelcasting! If you’re interested in developing expert technical content that performs, let’s have a conversation today.
Facebook
Twitter
LinkedIn
Reddit
Email

POST INFORMATION

If you work in a tech space and aren’t sure if we cover you, hit the button below to get in touch with us. Tell us a little about your content goals or your project, and we’ll reach back within 2 business days. 

Share via
Copy link
Powered by Social Snap