.Net Core 教程 Part4 – (21)(22) 依赖注入

Part4 – (21) 依赖注入的使用

1、在ASP.NET Core项目中一般不需要自己创建ServiceCollection、IServiceProvider。在Program.cs的builder.Build()之前向builder.Services中注入。

2、在Controller中可以通过构造方法注入服务。

.Net Core 教程 Part4 – (21)(22) 依赖注入
builder Service对象

这个 builder的Service对象实现了接口IServiceCollection:

.Net Core 教程 Part4 – (21)(22) 依赖注入
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);
        }
    }
}
.Net Core 教程 Part4 – (21)(22) 依赖注入
swagger页面

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这个包为例,首先看一下使用过程:

.Net Core 教程 Part4 – (21)(22) 依赖注入
创建Web项目和类库服务并使得Web项目引用类库项目1和2
.Net Core 教程 Part4 – (21)(22) 依赖注入
三个项目都引用 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's Blog站点所有,发布者:chaoqiang,转转请注明出处:https://www.zhengchaoqiang.com/1423.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

近期个人博客正在迁移中,原博客请移步此处,抱歉!