概要
在ASP.NET CORE开发项目过程中,我们在封装了用户的业务逻辑之后,要按照ASP.NET 自带的DI框架的要求,将我们封装好的业务逻辑类注册到ServiceCollection容器中,这样做避免了我们手工实例化对象,为开发带来了便利。
但是我们也应该看到,在带来便利的同时,我们也不得不手工维护注册的代码。对于大型项目,用户的业务逻辑可能非常复杂,需要封装大量的业务类。同样,在项目迭代周期内,也会产生大量新的业务需求,需要反复修改注册的代码。
解决方案
基于当前的问题,本文提出一种服务自动发现,自动注册的解决方案。让用户不再需要频繁修改服务注册代码。
基本需求和思路
- 如果需要自动发现用户需要依赖注入的类,就需要这些类具有一定的特征,以便于程序可以自动发现他们。
- ASP.NET 自带的DI框架有三中依赖注入方式Transient,Singleton和Scopped,所以自动化依赖注册也需要支持这三种方式。
- 考虑到用户业务逻辑类直接的可能存在一定成都的耦合,所以需要设置依赖注册的顺序。
代码和实现
使用自定义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);
}
}
}
- 获取包含需要自动注册的类的子项目程序集
- 获取每个程序集中的public的非抽象类,并且该类被AutoInjectAttribute修饰;
- 获取每个类的Type;
- 按照InterfaceName = $"I{ClassName}"找到类对应的接口;
- 获取AutoInjectAttribute中的优先级;
- 对于无法找到对应接口的类,无法进行依赖自动注册;
- 将所有找到的用于依赖自动注册的类按照优先级大小,降序拍列。
- 按照每个类型继承的接口ITransientService,ISingletonService和ITransientService的不同,来进行自动注册。
FAQ
新增一个业务服务相关的子项目,如何实现该项目中业务服务相关类的自动注册 ?
- 修该如下代码,增加新的程序集的引入:
List<Assembly> customerAssemblies = new List<Assembly>(){
typeof(BranchService).Assembly,
typeof(IdentityService).Assembly,
typeof(AnyClassInNewProject).Assembly
};
- 按照InterfaceName = $"I{ClassName}"命名规则定义业务相关类和接口;
- 用AutoInjectAttribute修饰业务相关类;
- 服务相关的接口请继承自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);