SoFunction
Updated on 2024-11-21

Core 3.1 AspectCore-based AOP implementation implements transaction, cache interceptor functionality

Recently I wanted to add a functionality to my framework, that is, for example, add a transactional feature Attribute to a method, then the method will enable transaction processing. Add a caching feature to a method, then the method will cache.

This one is also known as AOP for Cutting Oriented Programming as stated on the web.

The concept of AOP is also well understood, similar to middleware, to put it bluntly, is that I can arbitrarily add code in front of or behind the method, which is very suitable for caching, logging and other processing.

Back in net core 2.2, I tried implementing aop with autofac at that time, but this time I didn't want to use autofac, I used a more lightweight framework, AspectCore.

It's very, very easy to use, but at first it was a bit of a detour, mainly because the internet is full of tutorials for net core 3 or less, and 3 or less is a bit different to use than before.

Install the NuGet package first, package name:

Then add a line of code to the class, which is different for net core 3. This added code, means replace the built-in with AspectCore's IOC container. Since AOP relies on IOC implementation, the built-in IOC has to be replaced.

public class Program
  {
    public static void Main(string[] args)
    {
      CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
      (args)
      .ConfigureWebHostDefaults(webBuilder =>
      {
        <Startup>();
      })
      // Replacing the default IOC container with AspectCore
      .UseServiceProviderFactory(new DynamicProxyServiceProviderFactory());
}

Then add code to ConfigureServices in the class. (Actually, this can be added or not, if you need to configure it, add it, for example, global interceptor, only intercept which matches the service, because I only use the characteristics for intercepting, so I just don't configure anything)

(o=> { 
   // Add AOP configuration
});

This way AOP is configured, isn't it simple.

Of course, you need to pay attention to the use of interfaces, interface methods, classes, class virtual methods can be intercepted. And if you want to intercept the action of the controller, then you need to add ControllerAsServices in ConfigureService.

()
// Treat the controller as a service
.AddControllersAsServices()

Below I list my transaction interceptor code, inherit AbstractInterceptorAttribute if it's a feature interceptor, AbstractInterceptor if you want to write a global interceptor, and then configure it in ConfigureDynamicProxy, which I'm not going to describe

If your interceptor is placed in another project, then remember to add the package, not just add it, I just added it at the beginning and never found IsAsync, UnwrapAsyncReturnValue and some other extension methods.

public class TransactionInterceptorAttribute : AbstractInterceptorAttribute
  {
    public async override Task Invoke(AspectContext context, AspectDelegate next)
    {
      var dbContext = <AppDbContext>();
      // Determine if transactions are enabled first
      if ( == null)
      {
        await ();
        try
        {
          await next(context);
          ();
        }
        catch (Exception ex)
        {
          ();
          throw ex;
        }
      }
      else
      {
        await next(context);
      }
    }
  }

And then I can use transactions so elegantly.

I'll list my cache interceptor again, (thanks to the webmaster, I made a little modification for the handling of asynchronous method return values), and by the way, the following ICacheHelper is a cache helper interface I defined, using redis, I'll write a blog about it later on

public class CacheInterceptorAttribute : AbstractInterceptorAttribute
  {
    /// <summary>
    /// Cache seconds
    /// </summary>
    public int ExpireSeconds { get; set; }

    public async override Task Invoke(AspectContext context, AspectDelegate next)
    {
      // Determine if the method is asynchronous
      bool isAsync = ();
      //if ((typeof(AsyncStateMachineAttribute)) != null)
      //{
      //  isAsync = true;
      //}
      // first determine whether the method has a return value, without no cache judgment
      var methodReturnType = ().Type;
      if (methodReturnType == typeof(void) || methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask))
      {
        await next(context);
        return;
      }
      var returnType = methodReturnType;
      if (isAsync)
      {
        // Get the type of asynchronous return
        returnType = ();
      }
      //Get the method parameter name
      string param = ();
      // Get the method name, which is the cache key value
      string key = "Methods:" +  + "." + ;
      var cache = <ICacheHelper>();
      // If the cache has a value, then return the cached value directly
      if ((key, param))
      {
        // Reflection to get the cached value, equivalent to <>(key,param)
        var value = typeof(ICacheHelper).GetMethod(nameof()).MakeGenericMethod(returnType).Invoke(cache, new[] { key, param });
        if (isAsync)
        {
          // Determine if it's a Task or a ValueTask.
          if (methodReturnType == typeof(Task<>).MakeGenericType(returnType))
          {
            // Reflection to get the return value of type Task<>, equivalent to (value)
             = typeof(Task).GetMethod(nameof()).MakeGenericMethod(returnType).Invoke(null, new[] { value });
          }
          else if (methodReturnType == typeof(ValueTask<>).MakeGenericType(returnType))
          {
            // Reflection constructs a return value of type ValueTask<>, equivalent to new ValueTask(value)
             = (typeof(ValueTask<>).MakeGenericType(returnType), value);
          }
        }
        else
        {
           = value;
        }
        return;
      }
      await next(context);
      object returnValue;
      if (isAsync)
      {
        returnValue = await ();
        // Reflection gets the value of the asynchronous result, equivalent to ( as Task<>).Result
        //returnValue = typeof(Task<>).MakeGenericType(returnType).GetProperty(nameof(Task<object>.Result)).GetValue();

      }
      else
      {
        returnValue = ;
      }
      (key, param, returnValue);
      if (ExpireSeconds > 0)
      {
        (key, (ExpireSeconds));
      }

    }
  }

I've also got a cache deletion interceptor, which works by deleting the relevant cached values when a method with this feature is executed

Why this design? For example, if I add a cache to a method GetUserList, what if my data changes, and I want to delete the cache when the User data changes, then I can add my cache deletion interceptor to the SaveUser method, which will delete the relevant cache after the execution of this method

public class CacheDeleteInterceptorAttribute : AbstractInterceptorAttribute
{
  private readonly Type[] _types;
  private readonly string[] _methods;

  /// <summary>
  /// You need to pass the same number of Types and Methods, and the Types and Methods in the same location will be combined to form a cache key for deletion.
  /// </summary>
  /// <param name="Types"> Pass in the class to delete the cache </param>
  /// <param name="Methods"> Pass in the name of the method to remove from the cache, which must correspond to the Types array </param>.
  public CacheDeleteInterceptorAttribute(Type[] Types, string[] Methods)
  {
    if ( != )
    {
      throw new ApiFailException(ApiFailCode.OPERATION_FAIL, "Types must match the number of Methods.");
    }
    _types = Types;
    _methods = Methods;
  }

  public async override Task Invoke(AspectContext context, AspectDelegate next)
  {
    var cache = <ICacheHelper>();
    await next(context);
    for (int i = 0; i < _types.Length; i++)
    {
      var type = _types[i];
      var method = _methods[i];
      string key = "Methods:" +  + "." + method;
      (key);
    }
  }
}

The principle of AOP implementation I also imagined:

To implement AOP, you need to rely on the IOC container, because it is the steward of our classes, then the classes that can be intercepted must be injected by the IOC, and the ones that are newly created by yourself are not subject to interception. If I want to add some code in front of the A method, then I tell the IOC, give it the code, then the IOC in the injection of the class where the A method, it will inherit it to generate a derived class, and then rewrite the A method, so the intercept method must be virtual, and then the A method to write the code I want to add, and then () so.

To this article on Core 3.1 based on AspectCore to achieve AOP transactions, cache interceptor function of the article is introduced to this , more related Core 3.1 to achieve transactions, cache interceptor content, please search for my previous articles or continue to browse the following related articles I hope you will support me in the future !