ASP.NET Core依赖注册自动化实现

概要

在ASP.NET CORE开发项目过程中,我们在封装了用户的业务逻辑之后,要按照ASP.NET 自带的DI框架的要求,将我们封装好的业务逻辑类注册到ServiceCollection容器中,这样做避免了我们手工实例化对象,为开发带来了便利。

但是我们也应该看到,在带来便利的同时,我们也不得不手工维护注册的代码。对于大型项目,用户的业务逻辑可能非常复杂,需要封装大量的业务类。同样,在项目迭代周期内,也会产生大量新的业务需求,需要反复修改注册的代码。

解决方案

基于当前的问题,本文提出一种服务自动发现,自动注册的解决方案。让用户不再需要频繁修改服务注册代码。

基本需求和思路

  1. 如果需要自动发现用户需要依赖注入的类,就需要这些类具有一定的特征,以便于程序可以自动发现他们。
  2. ASP.NET 自带的DI框架有三中依赖注入方式Transient,Singleton和Scopped,所以自动化依赖注册也需要支持这三种方式。
  3. 考虑到用户业务逻辑类直接的可能存在一定成都的耦合,所以需要设置依赖注册的顺序。

代码和实现

使用自定义Attribute的子类来标识需要自动注册到ServiceCollection容器中类,使其作为自动注册的开关。该类型只修饰普通类。

在Attribute的子类的构造器中增加一个Priority参数,并增加一个Priority属性,记录当前类的注册顺序。Priority值越高,注册顺序越靠前。

Attribute子类代码如下:

 using System;
    [AttributeUsage(AttributeTargets.Class)]
    public class AutoInjectAttribute: Attribute
    {
        public int Priority { get; set; } = 0;
        public AutoInjectAttribute(int Priority = 0)
        {
            this.Priority = Priority;
        }
    }

ASP.NET 自带的DI框架有三中依赖注入方式Transient,Singleton和Scopped,所以定义三个对应接口ITransientService,ISingletonService和ITransientService。这些接口只作为标识使用,没有具体的代码。

在使用时候,任何需要自动注册的类,需要同步定义接口,接口和类的命名规则是InterfaceName = $“I{ClassName}”。每个接口在定义时候根据注册方式不同,可以选择ITransientService,ISingletonService和ITransientService中的一个进行继承。

基于上述代码,在Startup.cs中的ConfigureServices方法中增加自动发现和自动注册的代码如下:

public void ConfigureServices(IServiceCollection services)
{
	List<Assembly> customerAssemblies = new List<Assembly>(){
                 typeof(BranchService).Assembly,
                 typeof(IdentityService).Assembly
            };
    var customerServices =  customerAssemblies
                .SelectMany(x => x.GetExportedTypes())           
                .Where(t => t.IsClass && ! t.IsAbstract  && t.IsDefined(typeof(AutoInjectAttribute)))
                .Select(s => new {
                    InstanceType = s,
                    InterfaceType = s.GetInterface($"I{s.Name}"),
                    Priority = s.GetCustomAttributes<AutoInjectAttribute>().FirstOrDefault()?.Priority ?? 0
                })
                .Where(s => s.InterfaceType != null)
                .OrderByDescending(x => x.Priority);
      
      foreach(var _service in customerServices){             
	      if ( typeof(ITransientService).IsAssignableFrom(_service.InterfaceType)){
	          services.AddTransient(_service.InstanceType, _service.InstanceType);
	      }else if (typeof(ISingletonService).IsAssignableFrom(_service.InterfaceType)){
	          services.AddSingleton(_service.InterfaceType, _service.InstanceType);
	      }else if (typeof(IScopedService).IsAssignableFrom(_service.InstanceType)){
	          services.AddScoped(_service.InterfaceType, _service.InstanceType);
	      } 
  	} 
}
  1. 获取包含需要自动注册的类的子项目程序集
  2. 获取每个程序集中的public的非抽象类,并且该类被AutoInjectAttribute修饰;
  3. 获取每个类的Type;
  4. 按照InterfaceName = $"I{ClassName}"找到类对应的接口;
  5. 获取AutoInjectAttribute中的优先级;
  6. 对于无法找到对应接口的类,无法进行依赖自动注册;
  7. 将所有找到的用于依赖自动注册的类按照优先级大小,降序拍列。
  8. 按照每个类型继承的接口ITransientService,ISingletonService和ITransientService的不同,来进行自动注册。

FAQ

新增一个业务服务相关的子项目,如何实现该项目中业务服务相关类的自动注册 ?

  1. 修该如下代码,增加新的程序集的引入:
List<Assembly> customerAssemblies = new List<Assembly>(){
     typeof(BranchService).Assembly,
     typeof(IdentityService).Assembly,
     typeof(AnyClassInNewProject).Assembly
};
  1. 按照InterfaceName = $"I{ClassName}"命名规则定义业务相关类和接口;
  2. 用AutoInjectAttribute修饰业务相关类;
  3. 服务相关的接口请继承自ITransientService,ISingletonService和ITransientService中的一个。

我们封装好的业务逻辑类并自动注册到ServiceCollection容器中,在此过程中,是否可以使用下面的代码来遍历所有的子项目?

		AppDomain
             .CurrentDomain
             .GetAssemblies()

笔者发现如果在当前的Startup.cs中没有对对应子项目中的某一个类的引用,就无法找到子项目的Assembly,这样写存在漏洞。

考虑到此处在项目开发迭代过程中,变化不会很大,所以笔者将其变为一个手动维护的过程,只有添加新的项目,并且包含用于自动注册的类,才需要在customerAssemblies添加新项。

如果一定需要从所有子项目中自动查找用于自动注册的类,请使用下面的代码,我们通过读取项目中所有的DLL,进行查找,项目名称可以根据具体项目进行设置。

 var projectPrefix = "BranchMngt.";
 var customerServices = Directory
                .GetFiles(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory, "*.dll")
                .Select(x => Assembly.LoadFrom(x))
                .Where(x => x.FullName.StartsWith(projectPrefix ))
                .SelectMany(x => x.GetExportedTypes())           
                .Where(t => t.IsClass && ! t.IsAbstract  && t.IsDefined(typeof(AutoInjectAttribute)))
                .Select(s => new {
                    InstanceType = s,
                    InterfaceType = s.GetInterface($"I{s.Name}"),
                    Priority = s.GetCustomAttributes<AutoInjectAttribute>().FirstOrDefault()?.Priority ?? 0
                })
                .Where(s => s.InterfaceType != null)
                .OrderByDescending(x => x.Priority);

版权声明:本文为weixin_43263355原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。