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.
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.");
}
}
}
}
I've just finished reading your blog post about building a common cache service for a C# app. The core concept of the article is to demonstrate how to implement a cache service that can improve the performance of applications by reducing the time taken for slow operations. The implementation makes use of the
QueryCacheWithSelector
method, which allows for flexible caching strategies. Overall, the article is well-written and provides a useful solution for developers looking to optimize their C# applications.One of the highlights of the article is the detailed explanation of the caching process and how it can be customized using the
QueryCacheWithSelector
method. The inclusion of sample code snippets, along with explanations, makes it easy for readers to understand the implementation and adapt it to their own projects.However, there are a few areas where the article could be improved. Firstly, it would be helpful to provide an introduction to caching and why it is essential for optimizing application performance. This would give readers a better understanding of the problem you are addressing and the benefits of implementing a cache service.
Secondly, the article could benefit from a more structured approach. Breaking the content into sections with clear headings would make it easier to follow and understand. For example, you could have separate sections for explaining the caching process, the
QueryCacheWithSelector
method, and the test cases.Lastly, it would be useful to include a conclusion that summarizes the key points of the article and reiterates the benefits of using a cache service in a C# application. This would help to reinforce the main ideas and leave a lasting impression on the reader.
Overall, the article is informative and provides valuable information about implementing a cache service in a C# application. With a few improvements in structure and content, it could become an even more valuable resource for developers looking to optimize their applications. Keep up the good work, and I look forward to reading more of your articles in the future.