让 MemoryCache 更易于使用的 AOP 示例
使用 AOP 示例使 MemoryCache 更易于使用
0. 前言
我之前写过几篇文章介绍了一些 AOP 知识,但 AOP 的姿势还没有展现出来。
也许更漂亮的姿势,大家会对 AOP 更感兴趣。
内容大致会分成以下几篇文章(毕竟我是一个懒人,写完一次太累了,也没有动力)
- AOP 姿势简化
MemoryCache
的使用 - AOP 姿势简化
MemoryCache
和DistributedCache
的混合使用 - AOP 姿势如何将
HttpClient
变成声明式
至于 AOP 框架,这里的示例仍然会使用我自己的基于 emit 的动态代理 AOP 框架:https://github.com/fs7744/Norns.Urd
毕竟是我自己写的,所以修改/添加功能非常方便。
万一你有什么问题(虽然我可能没有),我也可以回答。 (当然,如果大家同意,在 github 上给个 star 会真的很有趣)。
1. 回顾 MemoryCache 的使用方法
var cache = ServiceProvider.GetRequiredService<IMemoryCache>();
var r = await cache.GetOrCreateAsync(cacheKey, async e =>
{
var rr = await do();
e.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
return rr;
});
MemoryCache
` 本身已经封装得非常简单,可以很容易地使用。 但是,每次使用它时,我们仍然需要重写类似的代码。 当然,我们都有超强的 Ctrl+c 和 Ctrl+v 能力。 这点重复代码是小菜一碟,上面的 w 行代码都是小场景。
但是,这种代码的编写方式就像在学校里的学生一样。 如何体现我们加班了几十年? 我们想让这些学生/实习生看不懂我们的代码,让他们看不到 `GetOrCreateAsync
`。
让他们找不到在 `do()
` 中如何运行断点。 只有这样才能展现出扫地工的实力:来,孩子,让我教你一项新技能。
2. 编写 Cache AOP 拦截器
在 Norns.Urd
中,Interceptor
是核心,用户可以在方法中插入自己的逻辑。
标准结构是 `IInterceptor
`。
public interface IInterceptor
{
// Users can customize the interceptor Order with Order, sorted by ASC,
// in which both the global interceptor and the display interceptor are included
int Order { get; }
// Synchronous interception method
void Invoke(AspectContext context, AspectDelegate next);
// Asynchronous interception method
Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);
// You can set how the interceptor chooses whether to filter or
// not to intercept a method, in addition to the NonAspectAttribute
// and global NonPredicates that can influence filtering
bool CanAspect(MethodInfo method);
}
在这里,我们将使用最简单的方法让大家简单理解:使用 `AbstractInterceptorAttribute
`。 一个非常简单的例子如下
public class CacheAttribute : AbstractInterceptorAttribute
{
private readonly TimeSpan absoluteExpirationRelativeToNow;
private readonly string cacheKey;
// For the sake of simplicity, we will only support TTL
// for a fixed time for the caching strategy.
public CacheAttribute(string cacheKey, string absoluteExpirationRelativeToNow)
{
this.cacheKey = cacheKey;
this.absoluteExpirationRelativeToNow =
TimeSpan.Parse(absoluteExpirationRelativeToNow);
}
public override async Task InvokeAsync
(AspectContext context, AsyncAspectDelegate next)
{
// The whole code is basically the same as we directly use MemoryCache
var cache = context.ServiceProvider.GetRequiredService<IMemoryCache>();
var r = await cache.GetOrCreateAsync(cacheKey, async e =>
{
await next(context); // So the real method logic is in next,
// so just call it
e.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
return context.ReturnValue; // The results are all in ReturnValue.
// For simplicity, I won’t write
// void / Task<T> / ValueTask<T> and so on.
// Compatible codes for various return values.
});
context.ReturnValue = r; // Set ReturnValue, because next will not be
// called within the validity period of the
// cache, so ReturnValue will not have a value,
// we need to set the cached result
// to ReturnValue
}
}
3. 进行测试
只需使用像这样的简单测试代码
public interface ITestCacheClient
{
string Say(string v);
}
public class TestCacheClient : ITestCacheClient
{
public string Say(string v) => v;
}
static class Program
{
static void Main(string[] args)
{
var client = new ServiceCollection()
.AddMemoryCache()
.AddSingleton<ITestCacheClient, TestCacheClient>()
.ConfigureAop()
.BuildServiceProvider()
.GetRequiredService<ITestCacheClient>();
Console.WriteLine(client.Say("Hello World!"));
Console.WriteLine(client.Say("Hello Two!"));
Thread.Sleep(3000);
Console.WriteLine(client.Say("Hello Two!"));
}
}
控制台结果
Hello World!
Hello Two!
Hello Two!
添加缓存设置
public class TestCacheClient : ITestCacheClient
{
[Cache(nameof(Say), "00:00:03")]
public string Say(string v) => v;
}
控制台结果
Hello World!
Hello World!
Hello Two!
示例代码都在 https://github.com/fs7744/AopDemoList/tree/master/MakeMemoryChacheSimple。
一个更全面的处理情况的示例在 https://github.com/fs7744/Norns.Urd/tree/main/src。
历史
- 2020年12月27日:初始版本