Working with ASP.NET Core IDistributedCache Provider for NCache

Cesar Aguirre - Apr 20 '22 - - Dev Community

I originally published this post on my blog a couple of weeks ago. It's part of a content collaboration with Alachisoft, NCache creators.

As we learned last time, when I covered in-memory caching with ASP.NET Core, a cache is a storage layer between an application and an external resource (a database, for example) used to speed up future requests to that resource. In this post, let's use ASP.NET Core IDistributedCache abstractions to write a data caching layer using NCache.

What's NCache?

From NCache official page, "NCache is an Open Source in-memory distributed cache for .NET, Java, and Node.js applications."

Among other things, we can use NCache as a database cache, NHibernate 2nd-level cache, Entity Framework cache, and web cache for sessions and responses.

NCache comes in three editions: Open Source, Professional, and Enterprise. The Open Source version supports up to two nodes and its cache server is only available for .NET Framework version 4.8. For a complete list of differences, check NCache edition comparison.

One of the NCache key features is performance. Based on their own benchmarks, "NCache can linearly scale to achieve 2 million operations per second with a 5-server cache cluster."

How to install NCache on a Windows machine?

Let's see how to install an NCache server on a Windows machine. For this, we need a Windows installer and have a trial license key. Let's install NCache Enterprise, version 5.2 SP1.

After running the installer, we need to select the installation type from three options: Cache server, remote client, and Developer/QA. Let's choose Cache Server.

NCache Installation Types

NCache Installation Types

Then, we need to enter a license key. Let's make sure to have a license key for the same version we're installing. Otherwise, we will get an "invalid license key" error. We receive the license key in a message sent to the email address we used during registration.

NCache License Key

NCache License Key

Next, we need to enter the full name, email, and organization we used to register ourselves while requesting the trial license.

Enter User Information

Enter User Information

Then, we need to select an IP address to bind our NCache server to. Let's stick to the defaults.

Configure IP Binding

Configure IP Binding

Next, we need to choose an account to run NCache. Let's use the Local System Account.

Account to run NCache

Account to run NCache

Once the installation finishes, our default browser will open with the Web Manager. By default, NCache has a default cache named demoCache.

NCache Web Manager

NCache Web Manager

Next time, we can fire the Web Manager by navigating to http://localhost:8251.

NCache's official site recommends a minimum of two servers for redundancy purposes. But, for our sample app, let's use a single-node server for testing purposes.

So far, we have covered the installation instructions for a Windows machine. But, we can also install NCache in Linux and Docker containers. And, we can use NCache as virtual machines in Azure and AWS.

Storage unit, Mexico

A cache is a fast storage unit. Photo by Jezael Melgoza on Unsplash

How to add and retrieve data from an NCache cache?

Now, we're ready to start using our NCache server from a .NET app. In Visual Studio, let's create a solution with a .NET 6 "MSTest Test Project" and a class file to learn the basic caching operations with NCache.

Connecting to an NCache cache

Before connecting to our NCache server, we need to first install the client NuGet package: Alachisoft.NCache.SDK. Let's use the latest version: 5.2.1.

To start a connection, we need the GetCache() method with a cache name. For our sample app, let's use the default cache: demoCache.

Let's start writing a test to add and retrieve movies from a cache.

using Alachisoft.NCache.Client;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace NCacheDemo.Tests;

[TestClass]
public class NCacheTests
{
    [TestMethod]
    public void AddItem()
    {
        var cacheName = "demoCache";
        ICache cache = CacheManager.GetCache(cacheName);

        // We will fill in the details later
    }
}
Enter fullscreen mode Exit fullscreen mode

If you're new to unit testing, start looking at how to write your first unit test in C# with MSTest.

Notice we didn't have to use a connection string to connect to our cache. We only used a cache name. The same one as in the Web Manager: demoCache.

NCache uses a client.ncconf file instead of connection strings. We can define this file at the application or installation level. For our tests, we're relying on the configuration file at the installation level. That's why we only needed the cache name.

Adding items

To add a new item to the cache, we need to use the Add() and AddAsync() methods with a key and a CacheItem to cache. The key is an identifier and the item is a wrapper for the object to cache.

Every item to cache needs an expiration. The CacheItem has an Expiration property for that.

There are two basic expiration types: Absolute and Sliding.

A cached item with Absolute expiration expires after a given time. Let's say, a few seconds. But, an item with Sliding expiration gets renewed every time it's accessed. If within the sliding time, we don't retrieve the item, it expires.

Let's update our test to add a movie to our cache.

using Alachisoft.NCache.Client;
using Alachisoft.NCache.Runtime.Caching;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Threading.Tasks;

namespace NCacheDemo.Tests;

[TestClass]
public class NCacheTests
{
    private const string CacheName = "demoCache";

    [TestMethod]
    public async Task AddItem()
    {
        var movie = new Movie(1, "Titanic");
        var cacheKey = movie.ToCacheKey();
        var cacheItem = ToCacheItem(movie);

        ICache cache = CacheManager.GetCache(CacheName);
        // Let's add Titanic to the cache...
        await cache.AddAsync(cacheKey, cacheItem);

        // We will fill in the details later
    }

    private CacheItem ToCacheItem(Movie movie)
        => new CacheItem(movie)
        {
            Expiration = new Expiration(ExpirationType.Absolute, TimeSpan.FromSeconds(1))
        };
}

[Serializable]
public record Movie(int Id, string Name)
{
    public string ToCacheKey()
        => $"{nameof(Movie)}:{Id}";
}
Enter fullscreen mode Exit fullscreen mode

Notice, we used two helper methods: ToCacheKey() to create the key from every movie and ToCacheItem() to create a cache item from a movie.

We used records from C# 9.0 to create our Movie class. Also, we needed to annotate it with the [Serializable] attribute.

Retrieving items

After adding items, let's retrieve them. For this, we need the Get<T>() method with a key.

Let's complete our first unit test to retrieve the object we added.

using Alachisoft.NCache.Client;
using Alachisoft.NCache.Runtime.Caching;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Threading.Tasks;

namespace NCacheDemo.Tests;

[TestClass]
public class NCacheTests
{
    private const string CacheName = "demoCache";

    [TestMethod]
    public async Task AddItem()
    {
        var movie = new Movie(1, "Titanic");
        var cacheKey = movie.ToCacheKey();
        var cacheItem = ToCacheItem(movie);

        ICache cache = CacheManager.GetCache(CacheName);
        await cache.AddAsync(cacheKey, cacheItem);

        // Let's bring Titanic back...
        var cachedMovie = cache.Get<Movie>(cacheKey);
        Assert.AreEqual(movie, cachedMovie);
    }

    private CacheItem ToCacheItem(Movie movie)
        => new CacheItem(movie)
        {
            Expiration = new Expiration(ExpirationType.Absolute, TimeSpan.FromSeconds(1))
        };
}

[Serializable]
public record Movie(int Id, string Name)
{
    public string ToCacheKey()
        => $"{nameof(Movie)}:{Id}";
}
Enter fullscreen mode Exit fullscreen mode

Updating items

If we try to add an item with the same key using the Add() or AddAsync() methods, they will throw an OperationFailedException. Try to add a unit test to prove that.

To either add a new item or update an existing one, we should use the Insert() or InserAsync() methods instead. Let's use them in another test.

using Alachisoft.NCache.Client;
using Alachisoft.NCache.Runtime.Caching;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Threading.Tasks;

namespace NCacheDemo.Tests;

[TestClass]
public class NCacheTests
{
    // Our previous test is the same

    [TestMethod]
    public async Task UpdateItem()
    {
        ICache cache = CacheManager.GetCache(CacheName);

        var movie = new Movie(2, "5th Element");
        var cacheKey = movie.ToCacheKey();
        var cacheItem = ToCacheItem(movie);
        // Let's add the 5th Element here...
        await cache.AddAsync(cacheKey, cacheItem);

        var updatedMovie = new Movie(2, "Fifth Element");
        var updatedCacheItem = ToCacheItem(updatedMovie);
        // There's already a cache item with the same key...
        await cache.InsertAsync(cacheKey, updatedCacheItem);

        var cachedMovie = cache.Get<Movie>(cacheKey);
        Assert.AreEqual(updatedMovie, cachedMovie);
    }

    // Rest of the file...
}
Enter fullscreen mode Exit fullscreen mode

Notice we used the InsertAsync() method to add an item with the same key. When we retrieved it, it contained the updated version of the item.

There's another basic method: Remove() and RemoveAsync(). We can guess what they do. Again, try to write a test to prove that.

How to use ASP.NET Core IDistributedCache with NCache?

Up to this point, we have NCache installed and know how to add, retrieve, update, and remove items.

Let's revisit our sample application from our post about using a Redis-powered cache layer.

Let's remember the example from that last post. We had an endpoint that uses a service to access a database, but it takes a couple of seconds to complete. Let's think of retrieving complex object graphs or doing some computations with the data before returning it.

Something like this,

using DistributedCacheWithNCache.Responses;

namespace DistributedCacheWithNCache.Services;

public class SettingsService
{
    public async Task<SettingsResponse> GetAsync(int propertyId)
    {
        // Beep, boop...Aligning satellites...
        await Task.Delay(3 * 1000);

        return new SettingsResponse
        {
            PropertyId = propertyId,
            Value = "Anything"
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice we emulated a database call with a 3-second delay.

Also, we wrote a set of extensions methods on top of the IDistributedCache to add and retrieve objects from a cache.

There were the extension methods we wrote last time,

using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;

namespace DistributedCacheWithNCache.Services;

public static class DistributedCacheExtensions
{
    public static readonly DistributedCacheEntryOptions DefaultDistributedCacheEntryOptions
        = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60),
            SlidingExpiration = TimeSpan.FromSeconds(10),
        };

    public static async Task<TObject> GetOrSetValueAsync<TObject>(this IDistributedCache cache,
                                                                  string key,
                                                                  Func<Task<TObject>> factory,
                                                                  DistributedCacheEntryOptions options = null)
        where TObject : class
    {
        var result = await cache.GetValueAsync<TObject>(key);
        if (result != null)
        {
            return result;
        }

        result = await factory();

        await cache.SetValueAsync(key, result, options);

        return result;
    }

    private static async Task<TObject> GetValueAsync<TObject>(this IDistributedCache cache,
                                                              string key)
        where TObject : class
    {
        var data = await cache.GetStringAsync(key);
        if (data == null)
        {
            return default;
        }

        return JsonConvert.DeserializeObject<TObject>(data);
    }

    private static async Task SetValueAsync<TObject>(this IDistributedCache cache,
                                                     string key,
                                                     TObject value,
                                                     DistributedCacheEntryOptions options = null)
        where TObject : class
    {
        var data = JsonConvert.SerializeObject(value);

        await cache.SetStringAsync(key, data, options ?? DefaultDistributedCacheEntryOptions);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice we used Newtonsoft.Json to serialize and deserialize objects.

NCache and the IDistributedCache interface

Now, let's use a .NET 6 "ASP.NET Core Web App," those extension methods on top of IDistributedCache, and NCache to speed up the SettingsService.

First, we need to install the NuGet package NCache.Microsoft.Extensions.Caching. This package implements the IDistributedCache interface using NCache, of course.

After installing that NuGet package, we need to add the cache into the ASP.NET dependencies container in the Program.cs file. To achieve this, we need the AddNCacheDistributedCache() method.

// Program.cs
using Alachisoft.NCache.Caching.Distributed;
using DistributedCacheWithNCache;
using DistributedCacheWithNCache.Services;

var (builder, services) = WebApplication.CreateBuilder(args);

services.AddControllers();
// We add the NCache implementation here...
services.AddNCacheDistributedCache((options) =>
{
    options.CacheName = "demoCache";
    options.EnableLogs = true;
    options.ExceptionsEnabled = true;
});
services.AddTransient<SettingsService>();

var app = builder.Build();
app.MapControllers();
app.Run();
Enter fullscreen mode Exit fullscreen mode

Notice, we continued to use the same cache name: demoCache. And, also we relied on a Deconstruct method to have the builder and services variables deconstructed. I took this idea from Khalid Abuhakmeh's Adding Clarity To .NET Minimal Hosting APIs.

Back in the SettingsService, we can use the IDistributedCache interface injected into the constructor and the extension methods in the DistributedCacheExtensions class. Like this,

using DistributedCacheWithNCache.Responses;
using Microsoft.Extensions.Caching.Distributed;

namespace DistributedCacheWithNCache.Services
{
    public class SettingsService
    {
        private readonly IDistributedCache _cache;

        public SettingsService(IDistributedCache cache)
        {
            _cache = cache;
        }

        public async Task<SettingsResponse> GetAsync(int propertyId)
        {
            var key = $"{nameof(propertyId)}:{propertyId}";
            // Here we wrap the GetSettingsAsync method around the cache logic
            return await _cache.GetOrSetValueAsync(key, async () => await GetSettingsAsync(propertyId));
        }

        private static async Task<SettingsResponse> GetSettingsAsync(int propertyId)
        {
            // Beep, boop...Aligning satellites...
            await Task.Delay(3 * 1000);

            return new SettingsResponse
            {
                PropertyId = propertyId,
                Value = "Anything"
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice, we wrapped the GetSettingsAsync() with actual retrieving logic around the caching logic in the GetOrSetValueAsync(). At some point, we will have the same data in our caching and storage layers.

With the caching in place, if we hit one endpoint that uses that service, we will see faster response times after the first call delayed by 3 seconds.

Faster response times after using NCache

A few miliseconds reading it from NCache

Also, if we go back to NCache Web Manager, we should see some activity in the server.

NCache Dashboard

NCache Dashboard showing our first request

In this scenario, all the logic to add and retrieve items is abstracted behind the IDistributedCache interface. That's why we didn't need to directly call the Add() or Get<T>() method. Although, if we take a look at the NCache source code, we will find those methods here and here.

Voilà! That's NCache and how to use it with the IDistributedCache interface. With NCache, we have a distributed cache server with few configurations and a dashboard out-of-the-box. Also, we can add all the caching logic into decorators and have our services as clean as possible.

To follow along with the code we wrote in this post, check my Ncache Demo repository over on GitHub.

canro91/NCacheDemo - GitHub

To read more content, check my Unit Testing 101 series to learn from how to write your first unit tests to mocks.

Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .