I am attempting to write a unit test for an ASP.NET Core application that utilizes IMemoryCache. Initially, I encountered two issues. 

First, whenever I attempted to set a value in the IMemoryCache, I encountered a NullReferenceError. 

Second, I am seeking guidance on how to implement caching in my data access layer. Specifically, I aim to have the MemoryCache return a value, and if not found, retrieve the data from the database. Subsequently, I want to set it to the MemoryCache and return the required value. In this post, we will discuss solutions for both challenges that I faced during my development.

Certainly! Let's consider a scenario where we have a service that interacts with IMemoryCache to cache some data. We'll write a unit test for this service using Moq to mock the IMemoryCache.

Suppose we have a service called DataService that uses IMemoryCache to cache some data. 



using Microsoft.Extensions.Caching.Memory;
using System;

public class DataService
{
    private readonly IMemoryCache _memoryCache;

    public DataService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public void SetData(string key, string value)
    {
        _memoryCache.Set(key, value, TimeSpan.FromMinutes(30));
    }

    public string GetData(string key)
    {
        return _memoryCache.Get<string>(key);
    }
}
Now, let's write a unit test for DataService using Moq to mock IMemoryCache:
using Xunit;
using Moq;
using Microsoft.Extensions.Caching.Memory;
using System;

public class DataServiceTests
{
    [Fact]
    public void SetData_CachesData_UsingMemoryCache()
    {
        // Arrange
        var mockMemoryCache = new Mock<IMemoryCache>();
        var service = new DataService(mockMemoryCache.Object);
        var key = "name";
        var value = "Anita";

        // Act
        service.SetData(key, value);

        // Assert
        mockMemoryCache.Verify(cache => cache.Set(key, value, It.IsAny<MemoryCacheEntryOptions>()), Times.Once);
    }

    [Fact]
    public void GetData_RetrievesData_FromMemoryCache()
    {
        // Arrange
        var mockMemoryCache = new Mock<IMemoryCache>();
        var service = new DataService(mockMemoryCache.Object);
        var key = "name";
        var expectedValue = "Anita";
        mockMemoryCache.Setup(cache => cache.Get<string>(key)).Returns(expectedValue);

        // Act
        var actualValue = service.GetData(key);

        // Assert
        Assert.Equal(expectedValue, actualValue);
    }
}

  • In above code we're mocking IMemoryCache using Moq by creating a mock object and in the SetData_CachesData_UsingMemoryCache() test, we arrange, act, and assert that the Set method of IMemoryCache is called with the correct parameters when calling the SetData method of DataService.
  • In the GetData_RetrievesData_FromMemoryCache() test, we arrange, act, and assert that the Get method of IMemoryCache is called with the correct key when calling the GetData method of DataService. We also set up the mock to return an expected value.
One more thing , IMemoryCache.Set is an extension method, it cannot be directly mocked using Moq. However, we can mock the IMemoryCache interface and set up its behavior to achieve our testing goals.


How we can modify the unit test to accommodate this:
using Xunit;
using Moq;
using Microsoft.Extensions.Caching.Memory;
using System;

public class DataServiceTests
{
    [Fact]
    public void SetData_CachesData_UsingMemoryCache()
    {
        // Arrange
        var mockMemoryCache = new Mock<IMemoryCache>();
        var service = new DataService(mockMemoryCache.Object);
        var key = "testKey";
        var value = "testValue";

        // Act
        service.SetData(key, value);

        // Assert
        mockMemoryCache.Verify(cache => cache.CreateEntry(key), Times.Once);
        mockMemoryCache.Verify(cache => cache.Set(key, value), Times.Once);
    }
}




To achieve caching in your data access layer where the MemoryCache returns a value and, if not found, retrieves data from the database, sets it to the MemoryCache, and returns the required value, you can follow these steps:

  • Inject IMemoryCache and your database context into your data access layer class:
  • Ensure your data access layer class constructor accepts both IMemoryCache and database context as dependencies.
  • Implement a method to get data from cache or database: Create a function that first tries to retrieve the data from the MemoryCache. If the data is found, return it. Otherwise, fetch the data from the database, set it to the MemoryCache, and return it.  

Example of how your data access layer code
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Threading.Tasks;

public class DataAccessLayer
{
    private readonly IMemoryCache _memoryCache;
    private readonly YourDbContext _dbContext;

    public DataAccessLayer(IMemoryCache memoryCache, YourDbContext dbContext)
    {
        _memoryCache = memoryCache;
        _dbContext = dbContext;
    }

    public async Task<Employee> GetEmployee(int id)
    {
        string cacheKey = $"Employee_{id}";
        if (_memoryCache.TryGetValue(cacheKey, out Employee cachedEmployee))
        {
            return cachedEmployee; // Employee found in cache
        }

        // Employee not found in cache, fetch from database
        var employeeFromDb = await _dbContext.Employees.FindAsync(id);
        if (employeeFromDb != null)
        {
            // Set employee to cache
            _memoryCache.Set(cacheKey, employeeFromDb, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) // Set cache expiration time
            });
            return employeeFromDb;
        }

        return null; // Employee not found in database
    }
}

2

When we encounter the situation where we need to mock the IMemoryCache.Set method using Moq framework, we come across the limitation that it's an extension method and cannot be directly mocked.

To address this, we utilize the following extension method:


    public static TItem Set(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options)
    {
        using (var entry = cache.CreateEntry(key))
        {
            if (options != null)
            {
                entry.SetOptions(options);
            }

            entry.Value = value;
        }

        return value;
    }
    

Above extension method enables us to set a value in the memory cache while allowing us to mock it for testing purposes.

Using this approach, we can effectively test code that interacts with the memory cache without being hindered by the inability to mock extension methods directly.