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.");
            }
        }
    }
}