Basic usage of Castle DynamicProxy (AOP)

Basic usage of Castle DynamicProxy (AOP)

This article introduces the basic concepts of AOP programming, the basic usage of Castle DynamicProxy (DP), the use of third-party extensions to implement asynchronous (async) support, and Autofac demonstrates how to implement AOP programming.

AOP

The explanation of AOP in the encyclopedia:

AOP is the abbreviation of Aspect Oriented Programming, which means: aspect-oriented programming, a technology that achieves unified maintenance of program functions through pre-compilation and runtime dynamic agents. AOP is a continuation of OOP and a hot spot in software development... It is a derivative paradigm of functional programming. The use of AOP can isolate various parts of the business logic, thereby reducing the coupling between the various parts of the business logic, improving the reusability of the program, and improving the efficiency of development at the same time.

In AOP, we focus on cross-cutting points and extract the general processing flow. We will provide general system functions and use them in various business layers, such as log modules and exception handling modules. A more flexible and efficient development experience is achieved through AOP programming.

Basic usage of DynamicProxy

Dynamic proxy is a way to achieve AOP, that is, we do not need to deal with aspects (logs, etc.) during the development process, but are automatically completed by dynamic proxy at runtime. Castle DynamicProxy is a framework for implementing dynamic proxy, which is used by many excellent projects to implement AOP programming, EF Core, Autofac, etc.

Let's look at two pieces of code to demonstrate the benefits of AOP. Before using AOP:

public class ProductRepository: IProductRepository
{
    private readonly ILogger logger;
    
    public ProductRepository(ILogger logger)
    {
        this.logger = logger;
    }
        
    public void Update(Product product)
    {
       //Perform the update operation
       //...

       //Record log
        logger.WriteLog($"Product {product} has been updated");
    }
}

After using AOP:

public class ProductRepository: IProductRepository
{
    public void Update(Product product)
    {
       //Perform the update operation
       //...
    }
}

It can be clearly seen that before using our ProductRepository depends on ILogger, and after executing the Update operation, we must write the code to record the log; and after use, the log record is handed over to the dynamic agent for processing, which reduces a lot The amount of development, even if you meet a slightly sloppy programmer, it will not delay the record of our log.

How to achieve such an operation?

  • 1. quoteCastle.Core
  • Then, define the interceptor and implement the IInterceptorinterface
public class LoggerInterceptor: IInterceptor
{
    private readonly ILogger logger;

    public LoggerInterceptor(ILogger logger)
    {
        this.logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
       //Get execution information
        var methodName = invocation.Method.Name;

       //Call the business method
        invocation.Proceed();

       //Record log
        this.logger.WriteLog($"{methodName} has been executed");
    }
}
  • Finally, add the calling code
static void Main(string[] args)
{
    ILogger logger = new ConsoleLogger();
    Product product = new Product() {Name = "Book" };
    IProductRepository target = new ProductRepository();

    ProxyGenerator generator = new ProxyGenerator();

    IInterceptor loggerIntercept = new LoggerInterceptor(logger);
    IProductRepository proxy = generator.CreateInterfaceProxyWithTarget(target, loggerIntercept);
    
    proxy.Update(product);
}

So far, we have completed a log interceptor. When logging is needed for other businesses, AOP programming can also be done by creating a dynamic proxy.

However, it is still more complicated to call, what needs to be improved? Of course, dependency injection (DI) is used.

Autofac integration

Autofac integrates support for DynamicProxy, we need to reference Autofac.Extras.DynamicProxy, then create a container, register a service, generate an instance, and call a method. Let's look at the following code:

ContainerBuilder builder = new ContainerBuilder();
//Register the interceptor
builder.RegisterType<LoggerInterceptor>().AsSelf();

//Register basic services
builder.RegisterType<ConsoleLogger>().AsImplementedInterfaces();

//Register the service to be intercepted
builder.RegisterType<ProductRepository>().AsImplementedInterfaces()
    .EnableInterfaceInterceptors()//Enable interface interception
    .InterceptedBy(typeof(LoggerInterceptor));//Specify the interceptor

var container = builder.Build();

//Analysis service
var productRepository = container.Resolve<IProductRepository>();

Product product = new Product() {Name = "Book" };
productRepository.Update(product);

Explain this code:

  • When registering the interceptor, you need to register as AsSelf, because the interceptor instance is used when the service intercepts. This registration method can ensure that the container can resolve the interceptor.
  • Enable interception function: When registering the service to be intercepted, you need to call the EnableInterfaceInterceptorsmethod, which means that the interface interception is enabled;
  • Associated services and interceptors: The InterceptedBymethod is passed to the interceptor. There are two ways to specify the interceptor. One is the writing in our code, which is non-invasive to the service, so this usage is recommended. The other is Interceptto associate through features, for example, the code above can be written as ProductRepositoryadding features to the class[Intercept(typeof(LoggerInterceptor))]
  • The registration of the interceptor can be registered as a type interceptor or as a named interceptor. There will be some differences in use, mainly in the association of interceptors. For this part, please refer to the official Autofac documentation. Our example uses type registration.
  • Interceptors are only effective for public interface methods and virtual methods in classes, and special attention should be paid when using them.

The basic principle of DynamicProxy

We mentioned above that dynamic proxy is only effective for public interface methods and virtual methods in classes. Have you ever wondered why?

In fact, the dynamic proxy dynamically generates a proxy class for us at runtime. The Generatorproxy class instance is returned to us when generated, and only the methods in the interface and the virtual methods in the class can be used in the subclass. Rewrite.

If we don't use dynamic proxy, what should our proxy service look like? Look at the following code, let us create a proxy class manually:

The following is my understanding of the agency category, please treat it dialectically. If there is something wrong, I hope to point it out.

Use a proxy for the interface:

public class ProductRepositoryProxy: IProductRepository
{
    private readonly ILogger logger;
    private readonly IProductRepository target;

    public ProductRepositoryProxy(ILogger logger, IProductRepository target)
    {
        this.logger = logger;
        this.target = target;
    }

    public void Update(Product product)
    {
       //Call the Update operation of IProductRepository
        target.Update(product);

       //Record log
        this.logger.WriteLog($"{nameof(Update)} has been executed");
    }
}

//Use proxy class
IProductRepository target = new ProductRepository();
ILogger logger = new ConsoleLogger();
IProductRepository productRepository = new ProductRepositoryProxy(logger, target);

Use a proxy for the class:

public class ProductRepository: IProductRepository
{
   //Rewritten as a virtual method
    public virtual void Update(Product product)
    {
       //Perform the update operation
       //...
    }
}

public class ProductRepositoryProxy: ProductRepository
{
    private readonly ILogger logger;

    public ProductRepositoryProxy(ILogger logger)
    {
        this.logger = logger;
    }

    public override void Update(Product product)
    {
       //Call the Update operation of the parent class
        base.Update(product);
       //Record log
        this.logger.WriteLog($"{nameof(Update)} has been executed");
    }
}

//Use proxy class
ILogger logger = new ConsoleLogger();
ProductRepository productRepository = new ProductRepositoryProxy(logger);

Asynchronous (async/await) support

If you are from the perspective of the application, asynchrony is just a syntactic sugar of Microsoft. Using asynchronous methods to return the result as a Task or Task object is no different from an int type for DP, but if we want To get the real return result during interception, some additional processing is needed.

Castle.Core.AsyncInterceptorIt is a framework that helps us deal with asynchronous interception, and the difficulty of asynchronous processing can be reduced by using this framework.

We still combine Autofac for processing in this section. 1. we modify the code and change the ProductRepository.Updatemethod to asynchronous.

public class ProductRepository: IProductRepository
{
    public virtual Task<int> Update(Product product)
    {
        Console.WriteLine($"{nameof(Update)} Entry");

       //Perform the update operation
        var task = Task.Run(() =>
        {
           //...
            Thread.Sleep(1000);

            Console.WriteLine($"{nameof(Update)} update operation has been completed");
           //Return the execution result
            return 1;
        });

       //return
        return task;
    }
}

Next, define our asynchronous interceptor:

public class LoggerAsyncInterceptor: IAsyncInterceptor
{
    private readonly ILogger logger;

    public LoggerAsyncInterceptor(ILogger logger)
    {
        this.logger = logger;
    }

   ///<summary>
   ///Used when the synchronization method is intercepted
   ///</summary>
   ///<param name="invocation"></param>
    public void InterceptSynchronous(IInvocation invocation)
    {
        throw new NotImplementedException(); 
    }

   ///<summary>
   ///Used when the asynchronous method returns to Task
   ///</summary>
   ///<param name="invocation"></param>
    public void InterceptAsynchronous(IInvocation invocation)
    {
        throw new NotImplementedException();
    }

   ///<summary>
   ///Used when the asynchronous method returns Task<T>
   ///</summary>
   ///<typeparam name="TResult"></typeparam>
   ///<param name="invocation"></param>
    public void InterceptAsynchronous<TResult>(IInvocation invocation)
    {
       //Call the business method
        invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
    }

    private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
    {
       //Get execution information
        var methodName = invocation.Method.Name;

        invocation.Proceed();
        var task = (Task<TResult>)invocation.ReturnValue;
        TResult result = await task;

       //Record log
        this.logger.WriteLog($"{methodName} has been executed, return result: {result}");

        return result;
    }
}

IAsyncInterceptorThe interface is an asynchronous interceptor interface, which provides three methods:

  • InterceptSynchronous: Method of intercepting synchronous execution
  • InterceptAsynchronous: Intercept the method that returns the result as Task
  • InterceptAsynchronous<TResult>: Intercept the method that returns the result as Task

In our code above, only InterceptAsynchronous<TResult>methods are implemented .

Since IAsyncInterceptorthe IInterceptorinterface is not related to the interface in the DP framework , we also need a synchronization interceptor. Here we directly modify the old synchronization interceptor:

public class LoggerInterceptor: IInterceptor
{
    private readonly LoggerAsyncInterceptor interceptor;
    public LoggerInterceptor(LoggerAsyncInterceptor interceptor)
    {
        this.interceptor = interceptor;
    }

    public void Intercept(IInvocation invocation)
    {
        this.interceptor.ToInterceptor().Intercept(invocation);
    }
}

As you can see from the code, the asynchronous interceptor LoggerAsyncInterceptorhas ToInterceptor()an extension method named , which can convert IAsyncInterceptorthe object of the IInterceptorinterface into the object of the interface.

Next we modify the service registration part of DI:

ContainerBuilder builder = new ContainerBuilder();
//Register the interceptor
builder.RegisterType<LoggerInterceptor>().AsSelf();
builder.RegisterType<LoggerAsyncInterceptor>().AsSelf();

//Register basic services
builder.RegisterType<ConsoleLogger>().AsImplementedInterfaces();

//Register the service to be intercepted
builder.RegisterType<ProductRepository>().AsImplementedInterfaces()
    .EnableInterfaceInterceptors()//Enable interface interception
    .InterceptedBy(typeof(LoggerInterceptor));//Specify the interceptor

var container = builder.Build();

The above is the way to IAsyncInterceptorimplement asynchronous interceptors. In addition to using this method, we can also judge the returned result manually in the dynamic interceptor, which will not be repeated here.

Discussion: Aspect Programming in ASP.NET MVC

Through the above introduction, we have understood the basic usage of AOP, but how to use ASP.NET Coreit?

  1. The registration of the MVC controller is done in Services, and Services itself does not support DP. This problem can be accomplished by integrating Autofac to re-register the controller, but is this operation really good?
  2. The controller in MVC is inherited from ControllerBase, and the Action method is customized by us, not the implementation of a certain interface, which is difficult to implement AOP. This problem can be solved by defining Action as a virtual method, but does this really conform to our coding habits?

We know that the original intention of AOP is to maintain a black box for users and program by extracting sections. These two problems require us to modify the users, which violates the SOLID principle.

So, if we want to use AOP in MVC, what is the method? In fact, MVC has provided us with two ways to achieve AOP:

  1. Middleware, which is a big killer in MVC, provides a series of built-in middleware such as logs, cookies, authorization, etc. It can be seen from this that MVC does not want us to implement AOP through DP, but in the pipeline Make a fuss.
  2. Filter. Filter is a product of ASP.NET MVC. It once helped us solve the logic of exceptions and authorization. In the Core era, we can still use this method.

These two methods are more in line with our coding habits and also reflect the characteristics of the MVC framework.

In summary, it is not recommended to use DP for Controller in MVC. If the NLayer architecture is adopted, DP can be used in the Application layer and the Domain layer to achieve similar data auditing, SQL tracking and other processing.

Although it is not recommended, I still give the code and give myself one more way:

  • MVC controller registered as a service
services.AddMvc()
    .AddControllersAsServices();
  • Re-register the controller and configure interception
builder.RegisterType<ProductController>()
    .EnableClassInterceptors()
    .InterceptedBy(typeof(ControllerInterceptor));
  • The Action in the controller is defined as a virtual method
[HttpPost]
public virtual Task<int> Update(Product product)
{
    return this.productRepository.Update(product);
}

to add on

  • Supplement on July 24, 2019

When creating a proxy class (no matter it is a class or an interface), there are two ways of writing: WithTarget and WithoutTarget, these two ways of writing have certain differences, withTarget needs to be passed in the target instance, and withoutTarget is not used, only the type needs to be passed in. can.

Reference documents

Reference: https://cloud.tencent.com/developer/article/1511929 Castle DynamicProxy Basic Usage (AOP)-Cloud + Community-Tencent Cloud