Part4 – (21) 依赖注入的使用
1、在ASP.NET Core项目中一般不需要自己创建ServiceCollection、IServiceProvider。在Program.cs的builder.Build()之前向builder.Services中注入。
2、在Controller中可以通过构造方法注入服务。
这个 builder的Service对象实现了接口IServiceCollection:
从上面的过程很容易看出,由WebApplication的CreateBuilder方法折腾来折腾去,折腾出一个 ServiceCollection对象来,我们可以直接使用这个对象来做注入。Build之后,内部可以为我们创建一个SeriviceProvide,后面就可以直接拿来使用。
下面我们创建两个要注入的服务:CalculateService 和 ScanService 。其中, CalculateService 很轻量,只完成简单的加
减乘除,初始化很快, ScanService 扫描所有exe文件,服务消耗时间较长。
CalculateService的注入——构造函数注入
首先来看第一个计算服务,我们完成注入,并在控制器中进行使用。
namespace Part4_21
{
public class CalculateService
{
public int Add(int i1, int i2)
{
return i1 + i2;
}
}
}
builder.Services.AddScoped<CalculateService>();
namespace Part4_21.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{
private readonly CalculateService calculateService;
public TestController(CalculateService calculateService)
{
this.calculateService = calculateService;
}
[HttpGet]
public int Add1()
{
return calculateService.Add(3,8);
}
}
}
ScanService的注入——请求Action注入
ScanService的构造函数,需要扫描D盘所有的exe文件,耗时很长,这里只是为了举例,如下:
public class ScanService
{
private string[] files;
public ScanService()
{
this.files= Directory.GetFiles("d:/","*.exe",SearchOption.AllDirectories);
}
public int Count
{
get
{
return this.files.Length;
}
}
}
然后,这个ScanService服务将会被注入,并且在Test控制器中使用,如下:
builder.Services.AddScoped<CalculateService>();
builder.Services.AddScoped<ScanService>();
同样,在TestController中有很多Action,但是只有一个Action会用到ScanService,那么在请求其他方法的时候,同样需要实例化ScanService,耗时很长。
public class TestController : ControllerBase
{
private readonly CalculateService calculateService;
private readonly ScanService scanService;
public TestController(CalculateService calculateService, ScanService scanService)
{
this.calculateService = calculateService;
this.scanService = scanService;
}
[HttpGet]
public int Add1()
{
return calculateService.Add(3,8);
}
[HttpGet]
public int Scan()
{
return scanService.Count;
}
}
那么在注入这个ScanService时,除了在TestControl的构造函数中注入服务,有没有其他方法来实现呢?
答案是,有的。对于少数在构造时就很消耗资源的服务,可以通过在Action的参数中进行注入,实现方法被调用时才进行实例化。
- 把Action用到的服务通过Action的参数注入,在这个参数上标注[FromServices]。和Action的其他参数不冲突。
- 一般不需要,只有调用频率不高并且资源的创建比较消耗资源的服务才[FromServices]。
- 只有Action方法才能用[FromServices] ,普通的类默认不支持。
[HttpGet]
public int Scan2([FromServices] ScanService scanService, int x)
{
return scanService.Count + x;
}
Part4 – (22) 依赖注入的案例
1、目的:在分层项目中,让各个项目负责各自的服务注册。
2、先学会使用:
1)Install-Package Zack.Commons
2)每个项目中创建一个或者多个实现了IModuleInitializer接口的类。
3)初始化DI容器
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
services.RunModuleInitializers(assemblies);
使用场景
一般来说,一个系统分为多个项目,例如某个项目管理多个服务,最后这些服务都会被注入,被使用,举个例子:
- 类库项目ClassLibrary1 包含2个服务,分别是加法服务AddServie和减法服务SubtractService;
- 类库项目ClassLibrary2 包含2个服务,分别是乘法服务MultiplyService 和除法服务DivideService;
- Web项目最终会使用所有这些加减乘除服务
那么,问题来了,如何管理这些服务对象的注册呢?注册的过程发生在Web项目,但是Web项目并不知道需要注册哪些服务,因为这些服务来自类库项目。有没有一种可能,类库项目自己决定可以注册哪些服务,web项目也不需要操心需要注册哪些服务,自动完成所有服务的注册呢?
答案是,有。下面以 Zack.Commons这个包为例,首先看一下使用过程:
为类库项目添加一个类,并且实现IModuleInitializer这个接口,如下:
using Microsoft.Extensions.DependencyInjection;
using Zack.Commons;
namespace Part4_21_ClassLibrary1
{
public class ModuleInit: IModuleInitializer
{
public void Initialize(IServiceCollection services)
{
services.AddScoped<AddServie>();
services.AddScoped<SubtractService>();
}
}
}
Web项目中初始化DI容器,只需要在服务启动过程中增加如下代码即可:
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
services.RunModuleInitializers(assemblies);
在容器中,就可以直接使用被注册的服务了:
namespace Part4_21.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class DITestController : ControllerBase
{
private readonly MultiplyService multiplyService;
private readonly DivideService divideService;
private readonly AddServie addServie;
private readonly SubtractService subtractService;
public DITestController(MultiplyService multiplyService, DivideService divideService, AddServie addServie, SubtractService subtractService)
{
this.multiplyService = multiplyService;
this.divideService = divideService;
this.addServie = addServie;
this.subtractService = subtractService;
}
}
}
实现原理
主要分析RunModuleInitializers方法和ReflectionHelper.cs。
这个RunModuleInitializers方法可以扫描所有程序集,得到实现了IModuleInitializer接口的类。
namespace Zack.Commons
{
public static class ModuleInitializerExtensions
{
/// <summary>
/// 每个项目中都可以自己写一些实现了IModuleInitializer接口的类,在其中注册自己需要的服务,这样避免所有内容到入口项目中注册
/// </summary>
/// <param name="services"></param>
/// <param name="assemblies"></param>
public static IServiceCollection RunModuleInitializers(this IServiceCollection services,
IEnumerable<Assembly> assemblies)
{
foreach (var implType in assemblies.SelectMany(asm => asm.GetTypes())
.Where(t => !t.IsAbstract && typeof(IModuleInitializer).IsAssignableFrom(t)))
{
var initializer = (IModuleInitializer?)Activator.CreateInstance(implType);
if (initializer == null)
{
throw new ApplicationException("Cannot create " + implType);
}
initializer.Initialize(services);
}
return services;
}
}
}
如何获得Assemblies?获得项目的根程序集,通过递归的方式获取这个项目引用的程序集,以及这些被引用的程序集所引用的程序集。详细的可以参考文章: https://dotnetcoretutorials.com/2020/07/03/getting-assemblies-is-harder-than-you-think-in-c/
public static IEnumerable<Assembly> GetAllReferencedAssemblies(bool skipSystemAssemblies = true)
{
/// 对于MSTest项目启动,Assembly.GetEntryAssembly()是"TestHost",而"TestHost"的GetReferencedAssemblies
/// 不包含项目的Dll,可能是因为"TestHost"都是通过Http调用被测试的程序集导致的,因此必须传入一个跟rootAssembly
/// https://dotnetcoretutorials.com/2020/07/03/getting-assemblies-is-harder-than-you-think-in-c/
Assembly? rootAssembly = Assembly.GetEntryAssembly();
if (rootAssembly == null)
{
rootAssembly = Assembly.GetCallingAssembly();
}
var returnAssemblies = new HashSet<Assembly>(new AssemblyEquality());
var loadedAssemblies = new HashSet<string>();
var assembliesToCheck = new Queue<Assembly>();
assembliesToCheck.Enqueue(rootAssembly);
if (skipSystemAssemblies && IsSystemAssembly(rootAssembly) != false)
{
returnAssemblies.Add(rootAssembly);
}
while (assembliesToCheck.Any())
{
var assemblyToCheck = assembliesToCheck.Dequeue();
foreach (var reference in assemblyToCheck.GetReferencedAssemblies())
{
if (!loadedAssemblies.Contains(reference.FullName))
{
var assembly = Assembly.Load(reference);
if (skipSystemAssemblies && IsSystemAssembly(assembly))
{
continue;
}
assembliesToCheck.Enqueue(assembly);
loadedAssemblies.Add(reference.FullName);
returnAssemblies.Add(assembly);
}
}
}
//以下代码解决两个问题:
//1、如果一个程序集被引用了,但是我们没有直接用在代码中静态使用其中的类,
//而是使用反射的形式使用了那些类,
//那么我们是无法用GetReferencedAssemblies获得这个程序集的
//2、以MsTest运行的时候GetEntryAssembly()拿到的是TestHost,从而无法进一步的获取它引用的程序集。
//"TestHost"的GetReferencedAssemblies不包含项目的Dll,可能是因为"TestHost"
//都是通过Http调用被测试的程序集导致的。
//https://dotnetcoretutorials.com/2020/07/03/getting-assemblies-is-harder-than-you-think-in-c/
//因此这里通过扫描运行目录下的dll的形式再去补偿获取一些之前可能没扫描的程序集,
//当然,在项目【发布】的时候,可能已经没有静态引用的程序集就没有被生成,
//因此,如果遇到这种情况,就需要我们在代码中显式的静态引用那个程序集中的一个类了。
var asmsInBaseDir = Directory.EnumerateFiles(AppContext.BaseDirectory,
"*.dll", new EnumerationOptions { RecurseSubdirectories = true });
foreach (var asmPath in asmsInBaseDir)
{
if(!IsManagedAssembly(asmPath))
{
continue;
}
AssemblyName asmName = AssemblyName.GetAssemblyName(asmPath);
//如果程序集已经加载过了就不再加载
if(returnAssemblies.Any(x=>AssemblyName.ReferenceMatchesDefinition(x.GetName(),asmName)))
{
continue;
}
Assembly asm = Assembly.Load(asmName);
if (skipSystemAssemblies && IsSystemAssembly(asm))
{
continue;
}
returnAssemblies.Add(asm);
}
return returnAssemblies.ToArray();
}
本文版权归个人技术分享站点所有,发布者:chaoqiang,转转请注明出处:http://www.zhengchaoqiang.com/1423.html