Recently, I found that almost every time I creates a new .NET app, I gonna need cache service.

While Microsoft officially provides the IMemoryCache, I found that it is pretty complicated for you to use it. For it requires a lot of code.

So I wrapped it to a more common one.

Before starting, make sure the project references Microsoft.Extensions.Caching.Memory and Microsoft.Extensions.Logging.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
 
namespace MyApp
{
    /// <summary>
    /// A cache service.
    /// </summary>
    public class CacheService
    {
        private readonly IMemoryCache cache;
        private readonly ILogger<CacheService> logger;
 
        /// <summary>
        /// Creates a new cache service.
        /// </summary>
        /// <param name="cache">Cache base layer.</param>
        /// <param name="logger">logger.</param>
        public CacheService(
            IMemoryCache cache,
            ILogger<CacheService> logger)
        {
            this.cache = cache;
            this.logger = logger;
        }
 
        /// <summary>
        /// Call a method with cache.
        /// </summary>
        /// <typeparam name="T">Response type</typeparam>
        /// <param name="cacheKey">Key</param>
        /// <param name="fallback">Fallback method</param>
        /// <param name="cacheCondition">In which condition shall we use cache.</param>
        /// <param name="cachedMinutes">Cached minutes.</param>
        /// <returns>Response</returns>
        public async Task<T> RunWithCache<T>(
            string cacheKey, 
            Func<Task<T>> fallback,
            Predicate<T> cacheCondition = null,
            int cachedMinutes = 20)
        {
            if (cacheCondition == null)
            {
                cacheCondition = (t) => true;
            }
 
            if (!this.cache.TryGetValue(cacheKey, out T resultValue) || resultValue == null || cachedMinutes <= 0 || cacheCondition(resultValue) == false)
            {
                resultValue = await fallback();
                if (resultValue == null)
                {
                    return default;
                }
                else if (cachedMinutes > 0 && cacheCondition(resultValue))
                {
                    var cacheEntryOptions = new MemoryCacheEntryOptions()
                        .SetSlidingExpiration(TimeSpan.FromMinutes(cachedMinutes));
 
                    this.cache.Set(cacheKey, resultValue, cacheEntryOptions);
                    this.logger.LogInformation($"Cache set For {cachedMinutes} minutes! Cached key: {cacheKey}");
                }
            }
            else
            {
                this.logger.LogInformation($"Cache hit! Cached key: {cacheKey}");
            }
 
            return resultValue;
        }
  
        /// <summary>
        /// Clear a cached key.
        /// </summary>
        /// <param name="key">Key</param>
        public void Clear(string key)
        {
            this.cache.Remove(key);
        }
    }
}

To use it, you can simply inject that service to service collection.

services.AddLogging()
    .AddMemoryCache()
    .AddScoped<CacheService>();

And inject the cache service from dependency injection.

private readonly CacheService cacheService;
 
public AzureDevOpsClient(CacheService cacheService)
{
    this.cacheService = cacheService;
}

Finally, using the service is pretty simple.

Exmaple:

/// <summary>
/// Get the pull request for pull request ID.
/// </summary>
/// <param name="pullRequestId">Pull request ID.</param>
/// <returns>Pull request</returns>
public virtual async Task<GitPullRequest> GetPullRequestAsync(int pullRequestId)
{
    return await this.cacheService.RunWithCache($"devops-pr-id-{pullRequestId}", async () =>
    {
        var pr = await this.gitClient.GetPullRequestByIdAsync(
            project: this.config.ProjectName,
            pullRequestId: pullRequestId);
        return pr;
    }, cachedMinutes: 200);
}

If you need to temporarily disable cache for one item, you can pass the cached minutes with 0.

public Task<IEnumerable<GitPullRequest>> GetPullRequests(int skip = 0, int take, bool allowCache = true)
{
    var allPrs = this.cacheService.RunWithCache($"prs-skip-{skip}-take-{take}", () => this.gitClient.GetPullRequestsAsync(skip, take), cachedMinutes: allowCache ? 20 : 0);
 
    return allPrs;
}

Of course you might want to add some unit test to that class. I have also made it ready for you.

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MyApp.Tests
{
    /// <summary>
    /// Cache service tests.
    /// </summary>
    [TestClass]
    public class CacheServiceTests
    {
        private IServiceProvider serviceProvider;

        /// <summary>
        /// Init
        /// </summary>
        [TestInitialize]
        public void Init()
        {
            this.serviceProvider = new ServiceCollection()
                .AddLogging()
                .AddMemoryCache()
                .AddScoped<CacheService>()
                .AddTransient<DemoIOService>()
                .BuildServiceProvider();
        }

        /// <summary>
        /// Clean up
        /// </summary>
        [TestCleanup]
        public void CleanUp()
        {
            var aiurCache = this.serviceProvider.GetRequiredService<CacheService>();
            aiurCache.Clear("TestCache");
        }

        /// <summary>
        /// CacheConditionTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task CacheConditionTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", () => demoService.DemoSlowActionAsync(-1), arg => (int)arg > 0);
                watch.Stop();
                Assert.AreEqual(count, -1);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", () => demoService.DemoSlowActionAsync(-2), arg => (int)arg > 0);
                watch.Stop();
                Assert.AreEqual(count, -2);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }
        }

        /// <summary>
        /// CacheTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task CacheTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", demoService.GetSomeCountSlowAsync);
                watch.Stop();
                Assert.AreEqual(count, 0);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", demoService.GetSomeCountSlowAsync);
                watch.Stop();
                Assert.AreEqual(count, 0);
                Assert.IsTrue(watch.Elapsed < TimeSpan.FromMilliseconds(190), "Demo action should finish very fast.");
            }

            cache.Clear("TestCache");
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", demoService.GetSomeCountSlowAsync);
                watch.Stop();
                Assert.AreEqual(count, 1);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }
        }

        /// <summary>
        /// NotCacheTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task NotCacheTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", () => demoService.DemoSlowActionAsync(-1), cachedMinutes: 0);
                watch.Stop();
                Assert.AreEqual(count, -1);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", () => demoService.DemoSlowActionAsync(-2));
                watch.Stop();
                Assert.AreEqual(count, -2);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }
        }

        /// <summary>
        /// NullCacheTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task NullCacheTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", () => demoService.DemoSlowActionAsync(null));
                watch.Stop();
                Assert.AreEqual(count, null);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", () => demoService.DemoSlowActionAsync(5), cacheCondition: arg => (int)arg > 0);
                watch.Stop();
                Assert.AreEqual(count, 5);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.RunWithCache("TestCache", demoService.GetSomeCountSlowAsync);
                watch.Stop();
                Assert.AreEqual(count, 5);
                Assert.IsTrue(watch.Elapsed < TimeSpan.FromMilliseconds(190), "Demo action should finish very fast.");
            }
        }

        /// <summary>
        /// SelectorCacheConditionTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task SelectorCacheConditionTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", () => demoService.DemoSlowActionAsync(-1), result => (int)result + 100, arg => (int)arg > 0, 20);
                watch.Stop();
                Assert.AreEqual(count, 99);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", () => demoService.DemoSlowActionAsync(-2), result => (int)result + 100, arg => (int)arg > 0);
                watch.Stop();
                Assert.AreEqual(count, 98);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }
        }

        /// <summary>
        /// SelectorCacheTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task SelectorCacheTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", demoService.GetSomeCountSlowAsync, result => result + 100);
                watch.Stop();
                Assert.AreEqual(count, 100);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", demoService.GetSomeCountSlowAsync, result => result + 100);
                watch.Stop();
                Assert.AreEqual(count, 100);
                Assert.IsTrue(watch.Elapsed < TimeSpan.FromMilliseconds(190), "Demo action should finish very fast.");
            }

            cache.Clear("TestCache");
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", demoService.GetSomeCountSlowAsync, result => result + 100);
                watch.Stop();
                Assert.AreEqual(count, 101);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }
        }

        /// <summary>
        /// SelectorNotCacheTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task SelectorNotCacheTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", () => demoService.DemoSlowActionAsync(-1), result => (int)result + 100, cachedMinutes: 0);
                watch.Stop();
                Assert.AreEqual(count, 99);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", () => demoService.DemoSlowActionAsync(-2), result => (int)result + 100);
                watch.Stop();
                Assert.AreEqual(count, 98);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }
        }

        /// <summary>
        /// SelectorNullCacheTest
        /// </summary>
        /// <returns>Task</returns>
        [TestMethod]
        public async Task SelectorNullCacheTest()
        {
            var cache = this.serviceProvider.GetRequiredService<CacheService>();
            var demoService = this.serviceProvider.GetRequiredService<DemoIOService>();
            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", () => demoService.DemoSlowActionAsync(null), (obj) => obj);
                watch.Stop();
                Assert.AreEqual(count, null);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", () => demoService.DemoSlowActionAsync(5), (result) => (int)result + 100, cacheCondition: arg => (int)arg > 0);
                watch.Stop();
                Assert.AreEqual(count, 105);
                Assert.IsTrue(watch.Elapsed > TimeSpan.FromMilliseconds(190), "Demo action should finish very slow.");
            }

            {
                var watch = new Stopwatch();
                watch.Start();
                var count = await cache.QueryCacheWithSelector("TestCache", demoService.GetSomeCountSlowAsync, (result) => (int)result + 200);
                watch.Stop();
                Assert.AreEqual(count, 205);
                Assert.IsTrue(watch.Elapsed < TimeSpan.FromMilliseconds(190), "Demo action should finish very fast.");
            }
        }
    }
}