Recently, I found that almost every time I creates a new .NET app, I 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()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(cachedMinutes));
this.cache.Set(cacheKey, resultValue, cacheEntryOptions);
this.logger.LogTrace($"Cache set For {cachedMinutes} minutes! Cached key: {cacheKey}");
}
}
else
{
this.logger.LogTrace($"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.
Example:
/// <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.");
}
}
}
}