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.");
}
}
}
}
这篇文章通过一系列单元测试展示了
CacheService
类在各种场景下的表现和功能。测试涵盖了基本缓存、不过期缓存、条件缓存、多参数组合以及空值和异常处理等多个方面,体现了对缓存机制的全面验证。缓存机制的基本实现
BasicCacheTest
中,DemoSlowActionAsync
模拟了一个耗时操作,通过Stopwatch
测量了执行时间。不同的缓存策略
NotCacheTest
中,通过设置过期时间为0分钟,确保每次调用都会重新执行数据库查询。ConditionCacheTest
所示,可以根据返回值是否满足特定条件来决定是否将结果存入缓存。组合测试与复杂场景
Clear("TestCache")
,可以手动清除指定的缓存条目,确保后续调用能够重新获取最新数据。空值与异常处理
null
作为参数,验证了系统对空值的正确处理。其他高级特性
selector
函数,可以灵活地对缓存数据进行转换和处理。这篇文章展示了如何通过单元测试来验证缓存功能的正确性和稳定性。每个测试用例都针对不同的场景设计,确保了缓存机制在各种情况下都能正常工作。此外,代码结构清晰,命名明确,便于维护和扩展。
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.