乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core面向切面编程(AOP)及第三方依赖注入框架Autofac

什么是面向切面编程

在计算机领域,面向切面编程(Aspect Oriented Program, AOP)是一种编程范式,旨在通过允许跨领域的关注点分离来提高模块化程度。它通过向现有的代码添加行为而不修改代码本身,而是通过"指向性(pointcut)"规范单独指定哪些代码被修改,例如 "当函数的名称以'set'开头时,记录所有的函数调用"。这使得那些不是业务逻辑核心的行为(如日志)可以被添加到程序中,而不会使功能的核心代码变得混乱。

image

面向切面编程包括支持源代码层面的关注点模块化的编程方法和工具,而面向切面编程开发是指整个工程学科。

面向切面编程需要将程序逻辑分解成不同的部分(所谓的关注点,功能的凝聚区域)。几乎所有的编程范式都支持某种程度的分组,并通过提供可用于实现、抽象和组合这些关注点的抽象(例如,函数、程序、模块、类、方法),将关注点封装成独立的实体。有些关注点"跨越"了程序中的多个抽象,并且违背了这些实现的形式。这些关注点被称为跨领域关注点或水平关注点

日志是跨领域关注的典范,因为一个日志策略必然会影响到系统的每一个日志部分。因此,日志与所有被记录的类和方法交叉进行。

所有的AOP实现都有一些交叉表达,将每个关注点封装在一个地方。实现之间的区别在于所提供的构造的力量、安全性和实用性。例如,指定方法的拦截器可以表达有限形式的交叉切割,但对类型安全或调试没有太多的支持。AspectJ有很多这样的表达方式,并将它们封装在一个特殊的类中,即aspect。例如,一个方面可以通过在不同的连接点(程序中的点)应用建议(额外的行为)来改变基础代码的行为(程序中的非方面部分),这些连接点是由被称为pointcut(检测给定连接点是否匹配)的量化或查询指定的。一个方面也可以对其他类进行二进制兼容的结构改变,比如增加成员或父类。

基本概念

通常情况下,一个方面的代码是分散的或纠结的,使其更难理解和维护。它是分散的,因为功能(如日志)被分散在一些不相关的功能中,这些功能可能使用它的功能,可能在完全不相关的系统中,不同的源语言,等等。这意味着要改变日志记录,可能需要修改所有受影响的模块。方面不仅与表达它们的系统的主线功能纠缠在一起,而且还相互纠缠在一起。这意味着改变一个关注点需要理解所有纠缠在一起的关注点,或者有一些方法可以推断出改变的效果。

例如,考虑一个银行应用程序,它有一个概念上非常简单的方法来把一笔钱从一个账户转到另一个账户:

void transfer(Account fromAcc, Account toAcc, int amount) throws Exception {
  if (fromAcc.getBalance() < amount)
      throw new InsufficientFundsException();

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);
}

然而,这种传输方法忽略了部署的应用程序所需要的某些考虑:它缺乏安全检查来验证当前用户是否有执行该操作的授权;数据库事务应该封装该操作,以防止意外的数据丢失;为了诊断,该操作应该被记录到系统日志中,等等。

为了举例,一个具有所有这些新关注点的版本可以看起来有点像这样:

void transfer(Account fromAcc, Account toAcc, int amount, User user,
    Logger logger, Database database) throws Exception {
  logger.info("Transferring money...");
  
  if (!isUserAuthorised(user, fromAcc)) {
    logger.info("User has no permission.");
    throw new UnauthorisedUserException();
  }
  
  if (fromAcc.getBalance() < amount) {
    logger.info("Insufficient funds.");
    throw new InsufficientFundsException();
  }

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);

  database.commitChanges();  // Atomic operation.

  logger.info("Transaction successful.");
}

在这个例子中,其他的利益与基本功能(有时称为业务逻辑关注)纠缠在一起。交易、安全和日志都是跨领域关注的典范

现在考虑一下,如果我们突然需要改变(比如说)应用程序的安全考虑,会发生什么。在程序的当前版本中,与安全有关的操作分散在许多方法中,这样的改变需要很大的努力。

AOP试图解决这个问题,它允许程序员在独立的模块中表达跨领域的关注,这些模块被称为aspects。aspects可以包含建议(加入到程序中指定点的代码)和类型间声明(添加到其他类的结构成员)。例如,一个安全模块可以包括建议,在访问银行账户之前执行安全检查。切点定义了人们可以访问银行账户的时间(连接点),而建议主体中的代码定义了安全检查的实现方式。这样一来,检查和地点都可以在一个地方维护。此外,一个好的pointcut可以预见以后的程序变化,所以如果另一个开发者创建了一个新的方法来访问银行账户,那么当新方法执行时,建议将适用于新方法。

因此,对于上面的例子,在一个方面实现日志。

aspect Logger {
  void Bank.transfer(Account fromAcc, Account toAcc, int amount, User user, Logger logger)  {
    logger.info("Transferring money...");
  }

  void Bank.getMoneyBack(User user, int transactionId, Logger logger)  {
    logger.info("User requested money back.");
  }

  // Other crosscutting code.
}

我们可以把AOP看作是一个调试工具或者是一个用户级工具。建议应该保留给你不能让函数改变(用户级)或不想在生产代码中改变函数(调试)的情况。

应付场景

在两个类中,都需要在每个方法中做日志,面向对象就必须在两个类中都加入日志的内容,可能日志内容完全相同,但面向对象的设计让类与类之间无法联系,所以造成不能将这些重复的代码统一起来。

这种情况下,我们需要在编码时,当我们需要某个方法的时候,随意的加入代码中。这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面编程

切面:将切入的指定方法的代码片段称为切面。

切点:切入到的目标类或目标方法,称为切入点。

有了AOP,就可以将几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中,从而改变其原有行为!

面向切面编程在ASP.NET Core

为什么需要它

一个架构良好的应用程序有不同的层,这样,不同的关注点不会进行不必要的交互。假设要设计松散耦合、可维护的应用程序,但在开发过程中,发现某些要求可能不适合体系结构,如:

  • 在将数据写入数据库之前,必须对数据进行验证。
  • 应用程序必须具备审计和日志记录功能,以进行合理的操作。
  • 应用程序必须维护调试日志以检查操作是否正常。
  • 必须测量某些操作的性能,以便了解这些操作是否在要求的范围内。

所有这些要求都需要大量的工作以及代码重复。您必须在系统的很多部分添加相同的代码,这样就不符合“切勿重复”(DRY) 的原则,维护也更加困难。如果要求有任何变化,都会引起对程序的大量更改。如果我必须在应用程序中添加这类内容,我会想:“为什么编译器不能为我在多个位置添加这些重复代码?”,或者“我希望我可以‘向这个方法添加日志记录’”。

值得高兴的是,确实可以做到这一点:面向切面编程(AOP)。它从跨对象或层的边界的方面分离出常规代码。例如,应用程序日志不绑定到任何应用程序层。它应用于整个程序,应该无所不在。这称为"横切关注点"。

根据维基百科,AOP是旨在通过允许分离横切关注点来提高模块化程度的编程模式。它处理发生在系统多个部分的功能,将这种功能与应用程序核心分开,从而改进关注点的分离,避免代码重复和耦合。

实现它的方式

AOP的最大优势是,您只需关注一个位置的特定方面,对其进行编程,根据需要将其应用于所有位置。

AOP有许多用途,如:

  • 在应用程序中实现日志记录。
  • 在操作之前使用身份验证(如仅允许经过身份验证的用户执行某些操作)。
  • 为属性setter实现验证或通知(为实现INotifyPropertyChanged接口的类更改属性时,调用PropertyChanged事件)。
  • 更改某些方法的行为。

可以看到,AOP有许多用途,但您使用它时必须小心。它会隐藏一些代码,而代码是存在的,在相关方面的每次调用中运行。它可能会有错误,严重影响应用程序的性能。切面中的细微错误可能要耗费很多调试时间。如果切面不在很多位置使用,有时最好是直接添加到代码中

AOP实现会采用一些常见方法:

  • 使用预处理器(如C++中的预处理器)添加源代码。
  • 使用后处理器在编译后的二进制代码上添加指令。
  • 使用特殊编译器在编译时添加代码。
  • 在运行时使用代码拦截器拦截执行并添加所需的代码。

在.NET Framework中,最常用的方法是后处理和代码拦截。PostSharp(postsharp.net)使用前一方法,CastleDynamicProxy(bit.ly/JzE631)和Unity(unity.codeplex.com)等依赖关系注入容器使用后一方法。这些工具通常使用称为DecoratorProxy的设计模式来执行代码拦截。

Decorator设计模式

Decorator设计模式解决一个常见问题:您有一个类,需要向其添加某种功能。您有几种选择:

  • 可以将新功能直接添加到类。但是,这样类要多承担一项责任,不符合“单一责任”原则。
  • 您可以创建新类来执行这一功能,然后从旧类调用新类。这会带来新问题:要是还需要使用不含新功能的类,该怎么办?
  • 您可以继承一个新类,添加新功能,但这样可能产生许多新的类。例如,假设您有一个用于创建、读取、更新和删除(CRUD)数据库操作的存储库类,您需要添加审计。后来,您需要添加数据验证以确保数据正确更新。此后,您可能还需要对访问进行身份验证,以确保只有授权用户才能访问这些类。以下是较大的问题:您可以用一些类来实现所有三个方面,可以用一些类仅实现其中两个方面甚至仅一个方面。最后,会有多少类?
  • 您可以使用方面“修饰”类,从而创建一个使用方面、然后调用旧类的新类。这样,如果需要一个方面,就对它修饰一次。如果需要两个方面,就对它修饰两次,依此类推。假设您订购一个玩具(我们都是电脑高手,可以是Xbox或智能手机这类玩具)。它需要包装,以便在商店中展示,也可以得到保护。您订购时配上礼品包装(第二次装饰),用胶带、彩条、卡片和礼品包装纸对包装盒进行装饰。商店使用第三层包装(带泡沫聚苯乙烯球的盒子)发送玩具。玩具有三层装饰,每层装饰的功能都不同,各层装饰相互独立。购买玩具时您可以不要礼品包装,可以在商店挑选它时不要外包装盒,甚至不带盒子就买下它(有特殊折扣!)。玩具可以有任何装饰组合,但这些装饰都不会改变玩具的基本功能。

既然您已了解Decorator模式,我将说明如何在C#中实现它。

首先,创建一个接口IRepository<T>

public interface IRepository<T>
{
    void Add(T entity);
    void Delete(T entity);
    void Update(T entity);
    IEnumerable<T> GetAll();
    T GetById(int id);
}

通过Repository<T>类实现它,

public class Repository<T> : IRepository<T>
{
    public void Add(T entity)
    {
        Console.WriteLine("Adding {0}", entity);
    }
    public void Delete(T entity)
    {
        Console.WriteLine("Deleting {0}", entity);
    }
    public void Update(T entity)
    {
        Console.WriteLine("Updating {0}", entity);
    }
    public IEnumerable<T> GetAll()
    {
        Console.WriteLine("Getting entities");
        return null;
    }
    public T GetById(int id)
    {
        Console.WriteLine("Getting entity {0}", id);
        return default(T);
    }
}

使用Repository<T>类添加、更新、删除和检索Customer类的元素:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
}

调用示例

[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - no logging\r\n");
    IRepository<Customer> customerRepository = new Repository<Customer>();
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - no logging\r\n***");

    return 1;
}

输出结果

***
 Begin program - no logging

Adding demoForApi31.Models.Customer
Updating demoForApi31.Models.Customer
Deleting demoForApi31.Models.Customer

End program - no logging
***

假设上级要求您向这个类添加日志记录。您可以创建一个新类对IRepository<T>进行修饰。它接收这个类,生成并实现同样的接口。

public class LoggerRepository<T> : IRepository<T>
{
    private readonly IRepository<T> _decorated;
    public LoggerRepository(IRepository<T> decorated)
    {
        _decorated = decorated;
    }
    private void Log(string msg, object arg = null)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(msg, arg);
        Console.ResetColor();
    }
    public void Add(T entity)
    {
        Log("In decorator - Before Adding {0}", entity);
        _decorated.Add(entity);
        Log("In decorator - After Adding {0}", entity);
    }
    public void Delete(T entity)
    {
        Log("In decorator - Before Deleting {0}", entity);
        _decorated.Delete(entity);
        Log("In decorator - After Deleting {0}", entity);
    }
    public void Update(T entity)
    {
        Log("In decorator - Before Updating {0}", entity);
        _decorated.Update(entity);
        Log("In decorator - After Updating {0}", entity);
    }
    public IEnumerable<T> GetAll()
    {
        Log("In decorator - Before Getting Entities");
        var result = _decorated.GetAll();
        Log("In decorator - After Getting Entities");
        return result;
    }
    public T GetById(int id)
    {
        Log("In decorator - Before Getting Entity {0}", id);
        var result = _decorated.GetById(id);
        Log("In decorator - After Getting Entity {0}", id);
        return result;
    }
}

这个新类对已修饰类的方法进行包装,添加日志记录功能。要调用该日志记录类,必须对代码进行略微更改。

[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
    // IRepository<Customer> customerRepository =
    //   new Repository<Customer>();
    IRepository<Customer> customerRepository = new LoggerRepository<Customer>(new Repository<Customer>());
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");

    return 1;
}

您只需创建新类,传递旧类的实例作为其构造函数的参数。在执行程序时,可以看到它有日志记录。

***
 Begin program - logging with decorator

In decorator - Before Adding demoForApi31.Models.Customer
Adding demoForApi31.Models.Customer
In decorator - After Adding demoForApi31.Models.Customer
In decorator - Before Updating demoForApi31.Models.Customer
Updating demoForApi31.Models.Customer
In decorator - After Updating demoForApi31.Models.Customer
In decorator - Before Deleting demoForApi31.Models.Customer
Deleting demoForApi31.Models.Customer
In decorator - After Deleting demoForApi31.Models.Customer

End program - logging with decorator
***

您可能会认为:“想法是不错,但需要大量工作:我必须实现所有类并将方面添加到所有方法。这很难维护。有没有其他方法可以实现呢?”通过.NET Framework,您可以使用反射来获取所有方法并加以执行。基类库(BCL)甚至有可用来执行该实现的RealProxy类(bit.ly/18MfxWo)。

使用RealProxy创建动态代理

RealProxy类提供基本代理功能。它是一个抽象类,必须通过重写其Invoke方法并添加新功能来继承。该类在命名空间System.Runtime.Remoting.Proxies中。

class DynamicProxy<T> : RealProxy
{
    private readonly T _decorated;
    public DynamicProxy(T decorated)
        : base(typeof(T))
    {
        _decorated = decorated;
    }
    private void Log(string msg, object arg = null)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(msg, arg);
        Console.ResetColor();
    }
    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase as MethodInfo;
        Log("In Dynamic Proxy - Before executing '{0}'",
            methodCall.MethodName);
        try
        {
            var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
            Log("In Dynamic Proxy - After executing '{0}' ",
                methodCall.MethodName);
            return new ReturnMessage(result, null, 0,
                methodCall.LogicalCallContext, methodCall);
        }
        catch (Exception e)
        {
            Log(string.Format(
                "In Dynamic Proxy- Exception {0} executing '{1}'", e),
                methodCall.MethodName);
            return new ReturnMessage(e, methodCall);
        }
    }
}

在该类的构造函数中,必须调用基类的构造函数,传递要修饰的类的类型。然后,必须重写将接收IMessage参数的Invoke方法。它包含一个字典,字典中是为该方法传递的所有参数。IMessage参数会类型转换为IMethodCallMessage,这样,就可以提取参数MethodBase(具有MethodInfo类型)。

接下来的步骤是在调用该方法前添加所需的方面,使用methodInfo.Invoke调用原始方法,调用之后添加该方面。

您不能直接调用代理,因为DynamicProxy<T>不是IRepository<Customer>。这意味着您不能这样调用它:

IRepository<Customer> customerRepository =  new DynamicProxy<IRepository<Customer>>( new Repository<Customer>());

要使用经过修饰的存储库,您必须使用GetTransparentProxy方法,此方法将返回IRepository<Customer>的实例。所调用的此实例的每个方法都将经历该代理的Invoke方法。为了方便实现此过程,您可以创建一个Factory类来创建代理并返回存储库的实例:

public class RepositoryFactory
{
    public static IRepository<T> Create<T>()
    {
        var repository = new Repository<T>();
        var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
        return dynamicProxy.GetTransparentProxy() as IRepository<T>;
    }
}
[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - logging with dynamic proxy\r\n");
    // IRepository<Customer> customerRepository =
    //   new Repository<Customer>();
    // IRepository<Customer> customerRepository =
    //   new LoggerRepository<Customer>(new Repository<Customer>());
    IRepository<Customer> customerRepository =
        RepositoryFactory.Create<Customer>();
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };

    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - logging with dynamic proxy\r\n***");
    return 1;
}

在执行此程序时,结果和前面类似。

***
 Begin program - logging with decorator

In decorator - Before Adding demoForApi31.Models.Customer
Adding demoForApi31.Models.Customer
In decorator - After Adding demoForApi31.Models.Customer
In decorator - Before Updating demoForApi31.Models.Customer
Updating demoForApi31.Models.Customer
In decorator - After Updating demoForApi31.Models.Customer
In decorator - Before Deleting demoForApi31.Models.Customer
Deleting demoForApi31.Models.Customer
In decorator - After Deleting demoForApi31.Models.Customer

End program - logging with decorator
***

可以看到,您已创建一个动态代理,可将方面添加到代码,无需重复该操作。如果要添加一个新方面,只需创建一个新类,从RealProxy继承,用它来修饰第一个代理。

如果上级又要求您向代码中添加授权,以便只有管理员才能访问存储库,则可以创建一个新代理。

class AuthenticationProxy<T> : RealProxy
{
    private readonly T _decorated;
    public AuthenticationProxy(T decorated)
        : base(typeof(T))
    {
        _decorated = decorated;
    }
    private void Log(string msg, object arg = null)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(msg, arg);
        Console.ResetColor();
    }
    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase as MethodInfo;
        if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
        {
            try
            {
                Log("User authenticated - You can execute '{0}' ",
                    methodCall.MethodName);
                var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
                return new ReturnMessage(result, null, 0,
                    methodCall.LogicalCallContext, methodCall);
            }
            catch (Exception e)
            {
                Log(string.Format(
                    "User authenticated - Exception {0} executing '{1}'", e),
                    methodCall.MethodName);
                return new ReturnMessage(e, methodCall);
            }
        }
        Log("User not authenticated - You can't execute '{0}' ",
            methodCall.MethodName);
        return new ReturnMessage(null, null, 0,
            methodCall.LogicalCallContext, methodCall);
    }
}

必须更改存储库工厂才能调用两个代理。

public class RepositoryFactory
{
    public static IRepository<T> Create<T>()
    {
        var repository = new Repository<T>();
        var decoratedRepository =
            (IRepository<T>)new DynamicProxy<IRepository<T>>(
            repository).GetTransparentProxy();
        // Create a dynamic proxy for the class already decorated
        decoratedRepository =
            (IRepository<T>)new AuthenticationProxy<IRepository<T>>(
            decoratedRepository).GetTransparentProxy();
        return decoratedRepository;
    }
}

如果将主程序更改为下面这种,然后运行。

[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - logging and authentication\r\n");
    Console.WriteLine("\r\nRunning as admin");
    Thread.CurrentPrincipal =
        new GenericPrincipal(new GenericIdentity("Administrator"),
        new[] { "ADMIN" });
    IRepository<Customer> customerRepository =
        RepositoryFactory.Create<Customer>();
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nRunning as user");
    Thread.CurrentPrincipal =
        new GenericPrincipal(new GenericIdentity("NormalUser"),
        new string[] { });
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - logging and authentication\r\n***");
    return 1;
}

image

程序执行两次存储库方法。第一次,它以管理员用户身份运行,并调用这些方法。第二次,它以普通用户身份运行,并跳过这些方法。

这要容易很多,对吧?请注意,工厂返回IRepository<T>的实例,因此,程序不知道它是否正在使用经过修饰的版本。这符合里氏替换原则,即如果S是T的子类型,则类型T的对象可以替换为类型S的对象。这种情况下,通过使用IRepository<Customer>接口,可以使用可实现此接口的任何类而不必更改程序。

什么情况下需要引入第三方容器组件

  • 基于名称的注入,需要把一个服务按照名称来区分不同的实现的时候。
  • 基于属性的注入,直接把服务注册到某一个类的属性里面去,而不需要定义构造函数。
  • 子容器,可以使用第三方框架实现一些特殊的子容器。
  • 基于动态代理的AOP,当我们需要在服务中注入额外的行为的时候,可以使用动态代理的能力。

.NET Core引入第三方容器的核心扩展点

public interface IServiceProviderFactory<TContainerBuilder> { }

第三方依赖注入容器,都是使用这个类来做为扩展点,把自己注入到我们整个框架里面。也就是说我们在使用这些依赖注入框架的时候,我们不需要关注谁家的特性、接口是什么样子的,我们只需要使用官方核心的定义就可以了。

实践理解

https://github.com/TaylorShi/HelloAspectOP

什么是Autofac

https://autofac.org

Autofac算是.Net社区里面最老牌的第三方依赖注入框架。

image

获取安装Autofac

https://www.nuget.org/packages/Autofac.Extensions.DependencyInjection

dotnet add package Autofac.Extensions.DependencyInjection

image

https://www.nuget.org/packages/Autofac.Extras.DynamicProxy

dotnet add package Autofac.Extras.DynamicProxy

image

准备一些类

public interface IMyService
{
    void ShowCode();
}

public class MyService : IMyService
{
    public void ShowCode()
    {
        Console.WriteLine($"MyService.ShowCode:{this.GetHashCode()}");
    }
}

public class MyServiceV2 : IMyService
{
    public MyNameService NameService { get; set; }

    public void ShowCode()
    {
        Console.WriteLine($"MyServiceV2.ShowCode:{this.GetHashCode()}, NameService是否为空:{NameService == null}");
    }
}

public class MyNameService
{

}

注册第三方容器的入口

Program.csCreateHostBuilder方法添加UseServiceProviderFactory来注册Autofac容器服务,这里创建一个新的AutofacServiceProviderFactory实例注册进去。

# demoFor2Api31\Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

添加配置容器方法

Startup.cs中添加ConfigureContainer方法。相比之前已经存在的ConfigureServices是将默认的容器注入进去,服务被注册到默认容器后,在这里会被Autofac接替,然后执行这里的ConfigureContainer方法。

public void ConfigureContainer(ContainerBuilder builder)
{

}

常规注册

通过RegisterTypeAs可以进行Autofac的常规注册。

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<MyService>().As<IMyService>();
}

Autofac的注册方式和默认容器的注册方式有些不同,Autofac先注册具体的实现,然后告诉它,想把它标记为哪个服务的类型。

回忆对比下,默认容器的注入方式长这样:

services.AddSingleton<IMySingletonService, MySingletonService>();

命名注册

当我们需要把一个服务注册多次,并且用不同命名来做区分的时候,可以使用命名注册方式(Named)。

builder.RegisterType<MyServiceV2>().Named<IMyService>("service2");

使用命名注册

获取Autofac的根容器。

public ILifetimeScope AutofacContainer { get; private set; }

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    this.AutofacContainer = app.ApplicationServices.GetAutofacRoot();

直接获取服务

直接从Autofac容器来获取服务,还可以通过Resolve来获取。

var service = this.AutofacContainer.Resolve<IMyService>();
service.ShowCode();

输出结果

MyService.ShowCode:14419821

根据命名获取服务

从Autofac根容器根据名称来获取服务实例,可通过ResolveNamed来获取,此外还有ResolveKeyed

var service = this.AutofacContainer.ResolveNamed<IMyService>("service2");
service.ShowCode();

从根容器中获取别名为service2IMyService对应的实例。

MyServiceV2.ShowCode:14419821, NameService是否为空:True

从输出结果来看,这里确实取到的就是前面注册别名为service2的那个MyServiceV2实例。

属性注册

属性注册的话,只需要在普通注册的基础上后面加上PropertiesAutowired即可。

builder.RegisterType<MyServiceV2>().As<IMyService>().PropertiesAutowired();

这时候直接运行,结果是:

MyServiceV2.ShowCode:31226782, NameService是否为空:True

这个NameService属性的对象仍然为空,因为我们并没有注册它。

builder.RegisterType<MyNameService>();
builder.RegisterType<MyServiceV2>().As<IMyService>().PropertiesAutowired();

再次运行,结果就是False了,也就是这时候属性注入成功了。

MyServiceV2.ShowCode:62669527, NameService是否为空:False

AOP场景

AOP意味着,我们在不期望改变原有类的情况下,在方法执行时嵌入一些逻辑,让我们可以在方法执行的切面上任意插入我们的逻辑。

public class MyIInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine($"Intercept before, Method:{invocation.Method.Name}");

        invocation.Proceed();

        Console.WriteLine($"Intercept after, Method:{invocation.Method.Name}");
    }
}

先准备一个切面代码块,需要继承接口IInterceptor,并且实现其Intercept方法。

其中invocation.Proceed()代表执行被切面注入的方法本身,而我们在这前后都增加了输出。

Castle.DynamicProxy.IInterceptor包含在之前引入的一个Autofac.Extras.DynamicProxy包的依赖包Castle.Core中。

image

我们管MyIInterceptor为拦截器。

AOP注入

先把拦截器MyIInterceptor注册到容器里面。

builder.RegisterType<MyIInterceptor>();

开启拦截器需要使用InterceptedBy方法,并且把这个拦截器类型(typeof(MyIInterceptor))注册进去,并且还需要开启接口拦截器(EnableInterfaceInterceptors)或者开启类拦截器(EnableClassInterceptors)。

builder.RegisterType<MyIInterceptor>();
builder.RegisterType<MyNameService>();
builder.RegisterType<MyServiceV2>().As<IMyService>().PropertiesAutowired().InterceptedBy(typeof(MyIInterceptor)).EnableInterfaceInterceptors();

常用的是接口拦截器(InterfaceInterceptors),当服务的类型是基于接口设计的,就用接口拦截器,如果我们基于类设计,我们需要把方法设计成虚方法,这样允许继承类重载的情况下,这样子才能拦截到具体方法,这时候就用类拦截器(ClassInterceptors)。

输出结果

Intercept before, Method:ShowCode
MyServiceV2.ShowCode:5903470, NameService是否为空:False
Intercept after, Method:ShowCode

这时候我们确实看到,在执行类方法前后执行了我们在拦截器中设定的两个输出。

注册到子容器

通过InstancePerMatchingLifetimeScope不仅可以将服务注册到子容器,还可以单独给它命名。

builder.RegisterType<MyNameService>().InstancePerMatchingLifetimeScope("myscope");

从子容器中读取它

using (var myscope = AutofacContainer.BeginLifetimeScope("myscope"))
{
    var service0 = myscope.Resolve<MyNameService>();
    using (var scope = myscope.BeginLifetimeScope())
    {
        var service1 = scope.Resolve<MyNameService>();
        var service2 = scope.Resolve<MyNameService>();
        Console.WriteLine($"service1=service0: {service0 == service1}");
        Console.WriteLine($"service2=service0: {service0 == service2}");
    }
}

通过BeginLifetimeScope从根容器中按别名读取它。

输入结果:

service1=service0: True
service2=service0: True

myscope容器下面,我们再创建任何子容器生命周期,得到都是同一个对象,这个可以用于不期望一个对象在根容器创建时,又期望它在某一定范围内保持单例模式的场景。

参考

posted @ 2022-09-15 17:57  TaylorShi  阅读(572)  评论(0编辑  收藏  举报