AOP proxy interception for Redis caching

Use AOP proxy interception to realize caching

The simple cache implementation above: The. Net core web API uses iationfilter to implement request caching The cache needs to be defined in the Controller layer, which increases the coupling to the Controller layer.

In addition, the cache is the result IActionResult cache at the controller level. Obviously, actions with complex logic cannot be cached for the data layer.

Explain with the example of user obtaining information:

/// <summary>
///Obtain user information according to account and password
/// </summary>
///< param name = "req" > account {account}, password {password (md5 encryption is used for password transmission)} < / param >
/// <returns></returns>
[HttpPost("get_userinfo")]
public async Task<ExecuteResult<UserInfoModel>> GetUserInfo(dynamic req)
{
	
}

If the user obtains the information of personnel for many times, the information of UserInfoModel personnel will not be changed in 10 minutes or even half an hour.
Then you need to cache the results of the first query of the database to obtain user information.

If you need to make other logical judgments about a user's login behavior.

Of course, method interception can also be used here, but it is not the main content of this chapter. Other implementations are ignored.

Then you need to cache the information of the obtained personnel UserInfoModel.

Then, this paper will decouple from the Controller layer to realize the proxy cache for business logic.

Common AOP implementations include: AutoFac, AspectCore, etc. This paper will implement AOP based on AspectCore micro framework
About AspectCore, there have been many articles about it in CSDN. I won't explain it here.

  • Project introduction AspectCore
    Right click the project to open the NuGet package, search and import aspectcore Core´╝îAspectCore.Abstractions two packages.

Note that AspectCore interception is to intercept interface methods or abstract methods, so it is necessary to mark the interception at the interface layer.

  • Cache tag class
    The new CustomCacheAttribute needs to be marked on the interface method and inherit the Attribute. It is used to mark the result return value of the logical interface and needs to be cached
 /// <summary>
 ///Custom cache
 /// </summary>
 [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
 public class CustomCacheAttribute : Attribute
 {
     /// <summary>
     ///Cache expiration time [minutes] the default cache time is 30 minutes
     /// </summary>
     public int ValidTimeMinutes { get; set; }
 }
  • Cache interception
    The new class CacheInterceptorAttribute inherits AbstractInterceptorAttribute and implements Invoke method
public override async Task Invoke(AspectContext context, AspectDelegate next)
{
    await next(context); //Execute the original method body
}
  • Configure interception
    Configure in ConfigureServices in Startup
public void ConfigureServices(IServiceCollection services)
{
	......
	// Inject redis cache component
    services.AddDistributedRedisCache(r => r.Configuration = Configuration["Redis:ConnectionString"]);
	//Injection global interceptor
	services.ConfigureDynamicProxy(config =>
	{
		// Acting on a class with a Service suffix
	    config.Interceptors.AddTyped<CacheInterceptorAttribute>(Predicates.ForService("*Service"));
	});
	......
}

In addition, the interceptor needs to be configured in CreateHostBuilder under Program

public static IHostBuilder CreateHostBuilder(string[] args) =>
	Host.CreateDefaultBuilder(args)
	   .ConfigureWebHostDefaults(webBuilder =>
	   {
	       webBuilder.UseStartup<Startup>();
	       webBuilder.UseUrls("http://*:15208");
	   })
	   .UseDynamicProxy(); // Injection interceptor

. UseDynamicProxy() this injection is very important. If it is not injected, the agent cannot start.

  • CacheInterceptorAttribute logic

Back to the class CacheInterceptorAttribute, since the method of intercepting is configured to match through the wildcard * Service. All methods under the class that match the wildcard will be represented.
Therefore, you need to judge whether the method is marked with CustomCacheAttribute. If it is not marked, there is no need to intercept.

In addition, if the method does not even return the result, the method does not need to be cached.

The complete code is as follows:

using AspectCore.DynamicProxy;
using AspectCore.DynamicProxy.Parameters;
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
public class CacheInterceptorAttribute : AbstractInterceptorAttribute
{
	public override async Task Invoke(AspectContext context, AspectDelegate next)
	{
	    // Gets whether the custom attribute of the method is marked CustomCacheAttribute
	    var cache = context.GetAttribute<CustomCacheAttribute>();
	    //First judge whether the method has a return value, and if not, no cache judgment will be performed
	    var methodReturnType = context.GetReturnParameter().Type;
	    bool isVoidReturn = methodReturnType == typeof(void) || methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask);
	    // If CustomCacheAttribute is marked and the method has a return value, perform the cache operation, set the return value and jump out of the method.
	    if (cache != null && !isVoidReturn)
	    {
	        Type returnType = context.GetReturnType();
	        var value = await ExecuteCacheLogicAsync(context, next, cache);
	        context.ReturnValue = ResultFactory(value, returnType, context.IsAsync());
	        // Please note that return is required here, unlike setting context in IActionResult ReturnValue will end the method body
	        return; 
	    }
	    // The method body that executes the original method
	    await next(context);
	}
	
	/// <summary>
	///Cache logic
	/// </summary>
	/// <param name="context"></param>
	/// <param name="next"></param>
	/// <param name="cache"></param>
	/// <returns></returns>
	private async Task<object> ExecuteCacheLogicAsync(AspectContext context, AspectDelegate next, CustomCacheAttribute cache)
	{
		// The key Md5Encrypt that splices the entire cache is a custom extension method that md5 encrypts string s
	    var cacheKeyBuilder = new StringBuilder();
	    cacheKeyBuilder.Append($"{context.ServiceMethod.DeclaringType.FullName}.{context.ServiceMethod.Name}");
	    string cacheParams = (string.Join('|', context.GetParameters()
	       .Select((p, i) => new KeyValuePair<string, object>(p.Name, p.Value))
	       .Select(p => $"#{p.Key}:{p.Value.ConvertJson()}")));
	    cacheKeyBuilder.Append($".{cacheParams.Md5Encrypt()}");
	    string cacheKey = cacheKeyBuilder.ToString();
	
	    return await CacheGetOrSetAsync(cacheKey, context.GetReturnType(), async () =>
	    {
	        await next(context);
	        return context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue;
	    }, cache.ValidTimeMinutes);
	}
	
	/// <summary>
	///Set or get cache
	/// </summary>
	/// <param name="context"></param>
	///< param name = "key" > cache key < / param >
	///< param name = "validtimeminutes" > timeout < / param >
	/// <returns></returns>
	private async Task<object> CacheGetOrSetAsync(string key, Type returnType, Func<Task<object>> getResultFunc, int ValidTimeMinutes)
	{
		// CustomDIContainer is a user-defined method to obtain DI injection objects. Please refer to the opening reference for the specific code, which will not be repeated here.
	    var cache = CustomDIContainer.GetSerivce<IDistributedCache>();
	    byte[] cacheBuffer = cache.Get(key);
	    object result;
	    // First judge whether there is a cache in the corresponding key
	    if (cacheBuffer == null || cacheBuffer.Length == 0)
	    {
	    	// The delegate method passed in the execution parameters
	        result = await getResultFunc();
	        // Serialize converts a custom object to a bute [] array
	        cacheBuffer = result.ConvertJson().Serialize();
	        // Set the expiration time of cache and other configurations
	        var options = new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(ValidTimeMinutes));
	        await cache.SetAsync(key, cacheBuffer, options);
	    }
	    else
	    {
	    	// Cache information is returned directly when there is a cache
	    	// Deserialize converts byte [] to the custom Json string of the specified object Json2Type and to the specified compound type
	    	// returnType is the return type of the original method The return type here is asynchronous for compatibility
	        result = cacheBuffer.Deserialize<string>().Json2Type(returnType);
	    }
	    return result;
	}
	
	private static readonly ConcurrentDictionary<Type, MethodInfo> TypeofTaskResultMethod = new ConcurrentDictionary<Type, MethodInfo>();
	// This method is used to serialize the returned value and is compatible with asynchronous returned value
	private object ResultFactory(object result, Type returnType, bool isAsync)
	{
	    if (isAsync)
	    {
	        return TypeofTaskResultMethod
	            .GetOrAdd(returnType, t => typeof(Task)
	            .GetMethods()
	            .First(p => p.Name == "FromResult" && p.ContainsGenericParameters)
	            .MakeGenericMethod(returnType))
	            .Invoke(null, new object[] { result });
	    }
	    else
	    {
	        return result;
	    }
	}
}

In addition, the serialization extension of the return parameter

using AspectCore.DynamicProxy;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;

public static class AspectContextExtension
{
    private static readonly ConcurrentDictionary<MethodInfo, object[]> MethodAttributes = new ConcurrentDictionary<MethodInfo, object[]>();

    public static Type GetReturnType(this AspectContext context)
    {
        return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
    }

    public static T GetAttribute<T>(this AspectContext context) where T : Attribute
    {
        MethodInfo method = context.ServiceMethod;
        var attributes = MethodAttributes.GetOrAdd(method, method.GetCustomAttributes(true));
        var attribute = attributes.FirstOrDefault(x => typeof(T).IsAssignableFrom(x.GetType()));
        if (attribute is T t) return t;
        return null;
    }
}
  • Mode of use

It is said on the Internet that the Action method can be successfully intercepted only if it is virtual. In fact, it is wrong.

public interface IOrganizationService
{
    /// <summary>
    ///Obtain personnel information according to account number and password
    /// </summary>
    /// <param name="account"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    [CustomCache(ValidTimeMinutes = 10)] // Mark here 
    Task<Tuple<bool, string, UserInfoModel>> GetUserInfoAsync(string account, string password);

    /// <summary>
    ///Get the organizational structure of the system
    /// </summary>
    /// <returns></returns>
    [CustomCache(ValidTimeMinutes = 60 * 24)]  // Mark here
    Task<Dictionary<string, Dictionary<string, List<UserInfoModel>>>> GetOrganizationAsync();
}

At this point, the corresponding interface implementation class will automatically cache the results.

Finally, I hope to introduce how to implement it with the most detailed explanation. If there are mistakes and omissions in this article, please correct them. If my article can help you, please don't hesitate to praise and collect. If you have any questions about the code in this article, please comment below. Thank you, watchers.

Keywords: ASP.NET Redis AOP

Added by tomlei on Tue, 01 Feb 2022 09:46:57 +0200