AOP的姿势之 简化 MemoryCache 使用方式

0. 前言

之前写了几篇文章介绍了一些AOP的知识,
但是还没有亮出来AOP的姿势,
也许姿势漂亮一点,
大家会对AOP有点兴趣
内容大致会分为如下几篇:(毕竟人懒,一下子写完太累了,没有动力)

  1. AOP的姿势之 简化 MemoryCache 使用方式
  2. AOP的姿势之 简化混用 MemoryCache 和 DistributedCache 使用方式
  3. AOP的姿势之 如何把 HttpClient 变为声明式

至于AOP框架在这儿示例依然会使用我自己基于emit实现的动态代理AOP框架: //github.com/fs7744/Norns.Urd
毕竟是自己写的,魔改/加功能都很方便,
万一万一大家如果有疑问,(虽然大概不会有),我也好回答, (当然如果大家认可,在github给个star,就实在是太让人开心了)

1. 正文

1.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() 里面的断点跑不到
这样我们才能展示出扫地僧的实力:来,小朋友,我来教你新姿势

1.2 逼格启航

1.2.1 逼格核心 – 拦截器

在Norns.Urd中,Interceptor 拦截器是用户可以在方法插入自己的逻辑的核心。
标准结构为IInterceptor

public interface IInterceptor
{
    // 用户可以通过Order自定义拦截器顺序,排序方式为ASC,全局拦截器和显示拦截器都会列入排序中
    int Order { get; }

    // 同步拦截方法
    void Invoke(AspectContext context, AspectDelegate next);

    // 异步拦截方法
    Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);

    // 可以设置拦截器如何选择过滤是否拦截方法,除了这里还有NonAspectAttribute 和全局的NonPredicates可以影响过滤
    bool CanAspect(MethodInfo method);
}

这里我们为了大家理解简单,就使用最简单的方式来做 : 使用 AbstractInterceptorAttribute
一个非常简单的例子就如下了:

    public class CacheAttribute : AbstractInterceptorAttribute
    {
        private readonly TimeSpan absoluteExpirationRelativeToNow;
        private readonly string cacheKey;

        // 为了简单,缓存策略我们就先只支持TTL 存活固定时间
        public CacheAttribute(string cacheKey, string absoluteExpirationRelativeToNow)
        {
            this.cacheKey = cacheKey;
            this.absoluteExpirationRelativeToNow = TimeSpan.Parse(absoluteExpirationRelativeToNow);
        }

        public override async Task InvokeAsync(AspectContext context, AsyncAspectDelegate next)
        {
            // 整个代码基本和我们直接使用 MemoryCache 一样
            var cache = context.ServiceProvider.GetRequiredService<IMemoryCache>();
            var r = await cache.GetOrCreateAsync(cacheKey, async e =>
            {
                await next(context); // 所以真正实现的方法逻辑都在 next 中,所以调用它就好了
                e.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
                return context.ReturnValue;  // 结果都在ReturnValue , 这里为了简单,就不写 void / Task<T> / ValueTask<T> 等等 各种返回值的兼容代码了
            });
            context.ReturnValue = r; // 设置 ReturnValue, 由于缓存有效期内, next不会被调用, 所以ReturnValue不会有值,我们需要将缓存结果设置到 ReturnValue
        }
    }

1.2.2 测试一下

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

Console 结果

Hello World!
Hello Two!
Hello Two!

加上缓存设置:

    public class TestCacheClient : ITestCacheClient
    {
        [Cache(nameof(Say), "00:00:03")]
        public string Say(string v) => v;
    }

再次测试的 Console 结果

Hello World!
Hello World!
Hello Two!

例子代码都在 //github.com/fs7744/AopDemoList/tree/master/MakeMemoryChacheSimple
处理情况更全面的例子在 //github.com/fs7744/Norns.Urd/tree/main/src

祝大家都能愉快被叫 大神 nb。

Tags: