Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

An AOP Example that Makes MemoryCache Easier to Use

5.00/5 (1 vote)
27 Dec 2020CPOL2 min read 4.9K  
Easier to use MemoryCache with AOP example

0. Preface

I wrote a few articles before introducing some AOP knowledge, but the posture of AOP has not been revealed yet.
Maybe the posture is more beautiful. Everyone will be interested in AOP.

The content will be roughly divided into the following articles (after all, I is a lazy man, too tired to finish writing at once, and there is no motivation):

  1. AOP posture to simplify the use of MemoryCache
  2. The posture of AOP to simplify the mixed use of MemoryCache and DistributedCache
  3. The posture of AOP how to turn HttpClient into declarative

As for the AOP framework, the example here will still use my own dynamic proxy AOP framework based on emit: https://github.com/fs7744/Norns.Urd
After all, I wrote it myself, so it is very convenient to modify/add functions.

In case you have any questions (although I probably won't have any), I can answer it too. (Of course, if everyone agrees, giving a star on github will be really fun).

1. Review How to Use MemoryCache

C#
var cache = ServiceProvider.GetRequiredService<IMemoryCache>();
var r = await cache.GetOrCreateAsync(cacheKey, async e =>
            {
                var rr = await do();
                e.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
                return rr;
            });

`MemoryCache` itself has been encapsulated so simple that it can be used. But, every time we use it, we still have to rewrite similar code like this. Of course, we all have super strong Ctrl+c and Ctrl+v capabilities. This little repetitive code is drizzle, the above w lines of code are all small scenes.

However, this kind of code is written in the same way as students in school. How can it reflect the fact that we have been working overtime for decades? We want to make these students/interns not understand our code, so that they can’t see `GetOrCreateAsync`.
Let them not find how to run the breakpoint in `do()` when debugging. Only in this way can we show the strength of the Sweeper: Come, kid, let me teach you a new skill.

2. Write a Cache AOP Interceptor

In Norns.Urd, Interceptor is the core where users can insert their own logic in methods.
The standard structure is `IInterceptor`.

C#
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);
}

Here, we will use the simplest way for everyone to understand it simply: Use `AbstractInterceptorAttribute`. A very simple example is as follows:

C#
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. Do a Test

Just use simple test code like this:

C#
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 result:

Hello World!
Hello Two!
Hello Two!

Add cache settings:

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

Console result:

Hello World!
Hello World!
Hello Two!

The example codes are all at https://github.com/fs7744/AopDemoList/tree/master/MakeMemoryChacheSimple.
A more comprehensive example of handling the situation is at https://github.com/fs7744/Norns.Urd/tree/main/src.

History

  • 27th December, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)