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