ASP.NET Core — 依赖注入

1. Ioc 和 DI

Ioc 和 DI 这两个词大家都应该比较熟悉,这两者已经在各种开发语言各种框架中普遍使用,成为框架中的一种基本设施了。

Ioc 是控制反转, Inversion of Control 的缩写,DI 是依赖注入,Inject Dependency 的缩写。

所谓控制反转,反转的是类与类之间依赖的创建。类型 A 依赖于类型 B 时,不依赖于具体的类型,而是依赖于抽象,不在类 A 中直接 new 类 B 的对象,而是通过外部传入依赖的类型对象,以实现类与类之间的解耦。所谓依赖注入,就是由一个外部容器对各种依赖类型对象进行管理,包括对象的创建、销毁、状态保持等,并在某一个类型需要使用其他类型对象的时候,由容器进行传入。

下图是一张网图,是关于 Ioc 解耦比较经典的图示过程了。至于依赖解耦的好处,就不在这里细讲了,如果有对依赖注入基本概念不理解的,可以稍微搜索一下相关的文章,也可以参考 ASP.NET Core 依赖注入 | Microsoft Learn 官方文档中的讲解。
在这里插入图片描述
Ioc 是一种设计思想,而 DI 是这种思想的具体实现。依赖注入是一种设计模式,是对面向对象编程五大基本原则中的依赖倒置原则的实践,其中很重要的一个点就是 Ioc 容器的实现。

2. .NET Core 依赖注入的基本用法

在 .NET Core 平台下,有一套自带的轻量级 Ioc 框架,如果是 ASP.NET Core 项目,更是在使用主机的时候自动集成了进去,我们在 startup 类中的 ConfigureServices 方法中的代码就是往容器中配置依赖注入关系,如果是控制台项目的话,还需要自己去集成。除此之外,.NET 平台下还有一些第三方依赖注入框架,如 Autofac、Unity、Castle Windsor 等。

这里先不讨论第三方框架的内容,先简单介绍一下 .Net Core 平台自带的 Ioc 框架的使用。

2.1 服务

依赖项注入术语中,服务通常是指向其他对象提供服务的对象,既可以作为其他类的依赖项,也可能依赖于其他服务。服务是 Ioc 容器管理的对象。

2.2 服务生命周期

使用了依赖注入框架之后,所有我们注入到容器中的类型的创建、销毁工作都由容器来完成,那么容器什么时候创建一个类型实例,什么时候销毁一个类型实例呢?这就涉及到注入服务的生命周期了。根据我们的需要,我们可以向容器中注册服务的时候,对服务的生命周期进行设置。服务的生命周期有以下三种:

(1) 单例 Singleton

注册成单例模式的服务,整个应用程序生命周期以内只创建一个实例。在应用内第一个使用到该服务时创建,在应用程序停止时销毁。

在某些情况下,对于某些特殊的类,我们需要注册成单例模式,这可以减少实例初始化的消耗,还能实现跨 Service 事务的功能。

(2)范围(或者作用域) Scoped

在同一个范围内只初始化一个实例 。在 web 应用中,可以理解为每一个 request 级别只创建一个实例,同一个 http request 会在一个 scope 内。

(3)多例 Tranisent

每一次使用到服务时都会创建一个新的实例,每一次对该依赖的获取都是一个新实例。

2.3 服务注册

在 ASP.NET Core 这样的 web 应用框架中,在使用主机的时候就自动集成了依赖注入框架,之后我们可以通过IServiceCollection 对象来注册依赖注入关系。前面入口文件一篇讲过,.NET 6 之前可以在 Startup 类中的 ConfigureServices 方法中进行注册,该方法传入 IServiceCollection 参数,.NET 6之后,可以通过 WebApplicationBuilder 对象的 Services 属性进行注册。

服务注册常用的方法如下:

  • Add 方法

    通过参数 ServiceDescriptor 将服务类型、实现类型、生命周期等信息传入进去,是服务注册最基本的方法。其中 ServiceDescriptor 参数又有多种变形。

    // 最基本的服务注册方法,除此之外还有其他各种变形
    builder.Services.Add(new ServiceDescriptor(typeof(IRabbit), typeof(Rabbit), ServiceLifetime.Transient));
    builder.Services.Add(ServiceDescriptor.Scoped<IRabbit, Rabbit>());
    builder.Services.Add(ServiceDescriptor.Singleton(typeof(IRabbit), (services) => new Rabbit()));
    
  • Add{lifetime} 扩展方法

    基于 Add 方法的扩展方法,包括以下几种,每种都有多个重载:

    // 基于生命周期的扩展方法,以下为实例,正式开发中不可能将一个类型注册为多个生命周期,会抛出异常
    builder.Services.AddTransient<IRabbit, Rabbit>();
    builder.Services.AddTransient(typeof(IRabbit), typeof(Rabbit));
    builder.Services.AddScoped<IRabbit, Rabbit>();
    builder.Services.AddSingleton<IRabbit, Rabbit>();
    
  • TryAdd{lifetime} 扩展方法

    对于 Add{lifetime} 方法的扩展,位于命名空间 Microsoft.Extensions.DependencyInjection.Extensions 下。

    与 Add{lifetime} 方法相比,差别在于当使用 Add{lifetime} 方法将同样的服务注册了多次时,在使用 IEnumerable<{Service}> 解析服务时,就会产生多个实例的副本,这可能会导致一些意料之外的 bug,特别是单例生命周期的服务。

    // 同一个服务同一个实现注入多次
    builder.Services.AddSingleton<IRabbit, Rabbit>();
    builder.Services.AddSingleton<IRabbit, Rabbit>();
    
    [ApiController]
    [Route("[controller]")]
    public class InjectTestController : ControllerBase
    {
        private readonly IEnumerable<IRabbit> _rabbits;
    
        public InjectTestController(IEnumerable<IRabbit> rabbits)
        {
            _rabbits = rabbits;
        }
    
        [HttpGet("")]
        public Task InjectTest()
        {
            // 2个IRabbit实例
            Console.WriteLine(_rabbits.Count());
            var rabbit1 = _rabbits.First();
            var rabbit2 = _rabbits.ElementAt(1);
            // 都是 Rabbit 类型
            Console.WriteLine(rabbit1 is Rabbit);
            Console.WriteLine(rabbit2 is Rabbit);
            // 两个实例不是同一个
            Console.WriteLine(rabbit1 == rabbit2);
            return Task.CompletedTask;
        }
    }
    

    调用接口后,打印输出结果如下:
    在这里插入图片描述
    而使用 TryAdd{lifetime} 方法,当 DI 容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到 DI 容器中。

    • TryAdd:对于 Add 方法
    • TryAddTransient:对应AddTransient 方法
    • TryAddScoped:对应AddScoped 方法
    • TryAddSingleton:对应AddSingleton 方法

    将服务注册改成以下代码:

    builder.Services.AddTransient<IRabbit, Rabbit>();
    // 由于上面已经注册了服务类型 IRabbit,所以下面的代码不不会执行任何操作(与生命周期无关)
    builder.Services.TryAddTransient<IRabbit, Rabbit>();
    builder.Services.TryAddTransient<IRabbit, Rabbit1>();
    

    在上面的控制器中新增以下方法:

    [HttpGet(nameof(InjectTest1))]
    public Task InjectTest1()
     {
         // 只有1个IRabbit实例
         Console.WriteLine(_rabbits.Count());
         var rabbit1 = _rabbits.First();
         // 都是 Rabbit 类型
         Console.WriteLine(rabbit1 is Rabbit);
         return Task.CompletedTask;
     }
    

    调用接口后,打印输出结果如下:
    在这里插入图片描述

  • TryAddEnumerable 方法

    与 TryAdd 对应,区别在于 TryAdd 仅根据服务类型来判断是否要进行注册,而 TryAddEnumerable 则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。

    将服务注册改成以下代码:

    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit>());
    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit1>());
    // 未进行任何操作,因为 IRabbit 服务的 Rabbit实现在上面已经注册了
    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit>());
    

    在上面的控制器新增一个方法:

    [HttpGet(nameof(InjectTest2))]
    public Task InjectTest2()
    {
         // 2个IRabbit实例
         Console.WriteLine(_rabbits.Count());
         var rabbit1 = _rabbits.First();
         var rabbit2 = _rabbits.ElementAt(1);
         // 第一个是 Rabbit 类型,第二个是 Rabbit1类型
         Console.WriteLine(rabbit1 is Rabbit);
         Console.WriteLine(rabbit2 is Rabbit1);
         return Task.CompletedTask;
     }
    

    调用接口后,控制台打印如下:
    在这里插入图片描述

  • Repalce 与 Remove 方法

    当我们想要从Ioc容器中替换或是移除某些已经注册的服务时,可以使用 Replace 和 Remove 。

    // 将容器中注册的IRabbit实现替换为 Rabbit1
    builder.Services.Replace(ServiceDescriptor.Transient<IRabbit, Rabbit1>());
    // 从容器中 IRabbit 注册的实现 Rabbit1
    builder.Services.Remove(ServiceDescriptor.Transient<IRabbit, Rabbit1>());
    // 移除 IRabbit服务的所有注册
    builder.Services.RemoveAll<IRabbit>();
    // 清空容器中的所有服务注册
    builder.Services.Clear();
    

2.4 服务解析

通过 IServiceCollection 注册了服务之后,可以通过以下方式解析相应服务的实例:

  • IServiceProvider

    IServiceProiver 实例由 IServiceCollection 通过 BuildServiceProvider() 方法创建,在 ASP.NET Core 中,主机启动的时候会创建一个全局的 IServiceProvider,并且此实例也在容器当中。所有在容器注册过的服务都可以通过 IServiceProiver 进行解析,当然该服务的依赖项必须也在容器中注册。

  • ActivatorUtilities

    用于手动创建未在 Ioc 容器中注册的服务实例

2.4.1 服务注入方式

当我们通过容器解析一个服务实例的时候,容器根据当前服务的链式依赖关系图解析其依赖项,根据依赖项的生命周期或创建、或从已有的实例获取,然后注入到我们解析的服务当中。在一个服务中获取另一个服务实例的方式由以下几种:

(1) 构造函数注入

构造函数注入是非常常见的服务注入方式,也是微软最推荐的方式,这种方式可以明确地声明当前类所依赖的东西,一目了然。如同上面的示例代码中,使用的就是构造函数注入方式。构造函数注入,对于类的构造函数有以下要求:

  • 构造函数可以接收非依赖注入的参数,但必须提供默认值

  • 当服务通过 IServiceProvider 解析时,要求构造函数必须是 public

  • 当服务由 ActivatorUtilities 解析时,构造函数注入要求只存在一个适用的构造函数。 支持构造函数重载,但其参数可以全部通过依赖注入来实现的重载只能存在一个。

  • 如果发现构造函数时存在歧义,将引发异常,例如以下情况:

    public class ExampleService
    {
        public ExampleService()
        {
        }
    
        public ExampleService(ILogger<ExampleService> logger)
        {
            // omitted for brevity
        }
    
        public ExampleService(IOptions<ExampleOptions> options)
        {
            // omitted for brevity
        }
    }
    

(2) 属性注入

这里有一点需要说明,.NET Core 内置的依赖注入框架并不支持属性注入,如果需要使用属性注入需要结合第三方依赖注入框架进行使用,如 autofac。

顾名思义,属性注入就是通过类中的属性注入需要的服务,要求属性必须是 public ,并且具备 get、set 访问器。如下:

在这里插入图片描述
属性注入一般用于注入一些即使缺失了也不会导致当前类无法工作的依赖项,如日志记录等。这种时候会为数据注入设置一个默认实现,防止该属性为空,导致当前类的功能受影响。

(3) 方法注入

通过 FromServicesAttribute 特性在控制器的方法参数中注入,这种方式只能用于控制器。默认情况下,控制器示例由容器来管理,在入口文件调用 builder.Services.AddControllers(); 时注册到容器中。

[HttpGet(nameof(InjectTest3))]
public Task InjectTest3([FromServices] IRabbit rabbit)
{
    Console.WriteLine(rabbit is Rabbit);
    return Task.CompletedTask;
}

这种方式用于缩小依赖注入的粒度,适用于注入的服务只在当前方法使用的时候,是对构造函数注入的简化。

(4) 手动解析

在.NET框架中,任何可以拿得到 IServiceProvider 实例的地方都可以通过 GetRequiredService() 或者 GetService() 解析我们需要的服务。直接使用IServiceProvider是服务定位器模式的一个示例。这通常被认为是反模式,因为它隐藏了类的依赖关系。这种方式在某些情况下是有用的,但是应该尽量避免。
在这里插入图片描述
GetService() 与 GetRequiredService() 的区别在于解析服务时,如果该服务没有在容器中注册,前者会返回 Null,而后者会抛出异常。两者的区别可参考以下文件:ASP.NET Core中GetService()和GetRequiredService()之间的区别

除了通过注入 IServiceProvider 来解析服务之外,其他的方式,例如 HttpContext 中也包含 IServiceProvider 实例,如:

var rabbit1 = HttpContext.RequestServices.GetRequiredService<IRabbit>();

2.3.2 ActivatorUtilities 使用

通过 ActivatorUtilities 解析服务比较简单,常用的由以下两个方法:

ActivatorUtilities.CreateInstance<HelloService>(provider, "test");
ActivatorUtilities.GetServiceOrCreateInstance<IHelloService>(provider);

其中 CreateInstance 方法的泛型类型需要是具体的类型,而不是接口,这个方法还可以传入构造函数需要的,但没有在容器中注册的参数。GetServiceOrCreateInstance 方法会先尝试从容器获取实例,获取不到再创建,不支持不在容器中注册的构造函数参数

2.5 容器中的服务创建与释放

我们使用了 IoC 容器之后,服务实例的创建和销毁的工作就交给了容器去处理,前面也讲到了服务的生命周期,那三种生命周期中对象的创建和销毁分别在什么时候呢。以下面的例子演示以下:

首先是新增三个类,用于注册三种不同的生命周期:

public class Service1
{
    public Service1()
    {
        Console.WriteLine("Service1 Created");
    }
}
public class Service2
{
    public Service2()
    {
        Console.WriteLine("Service2 Created");
    }
}
public class Service3
{
    public Service3()
    {
        Console.WriteLine("Service3 Created");
    }
}

接下来是演示场景,为了简单起见,就用后台服务程序吧

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
        services.AddSingleton<Service1>();
        services.AddScoped<Service2>();
        services.AddTransient<Service3>();
    })
    .Build();

await host.RunAsync();
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IServiceProvider _serviceProvider;

    public Worker(ILogger<Worker> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        #region 生命周期实例创建
        Console.WriteLine("Service1 第一次调用");
        var service11 = _serviceProvider.GetService<Service1>();
        Console.WriteLine("Service1 第二次调用");
        var service12 = _serviceProvider.GetService<Service1>();

        // 创建作用域,与 Web 应用中的一次请求一样
        using (var scope = _serviceProvider.CreateScope())
        {
            Console.WriteLine("Service2 第一次调用");
            var service31 = scope.ServiceProvider.GetService<Service2>();
            Console.WriteLine("Service2 第二次调用");
            var service32 = scope.ServiceProvider.GetService<Service2>();

            using (var scope1 = _serviceProvider.CreateScope())
            {
                Console.WriteLine("Service2 第三次调用");
                var service33 = scope1.ServiceProvider.GetService<Service2>();
            }
        }

        {
            Console.WriteLine("Service3 第一次调用");
            var service41 = _serviceProvider.GetService<Service3>();

            Console.WriteLine("Service3 第二次调用");
            var service42 = _serviceProvider.GetService<Service3>();
        }
        #endregion
    }
}

最终的输出如下:
在这里插入图片描述
通过输出,我们可以单例生命周期服务在第一次使用的时候创建,之后一直是一个实例,作用域生命周期服务在一个作用域中第一次使用的时候创建实例,之后在同一个实例中只保持一个,但在其他作用域中则会重新创建,而瞬时生命周期服务每次都会创建一个新实例。

看完创建,我们再看实例销毁的时机。

若服务实现了 IDisposable 接口,并且该服务是由 DI 容器创建的,则我们不应该手动去 Dispose,DI 容器会对服务自动进行释放。这里由两个关键点,一个是要实现 IDisposable 接口,一个是由容器创建。这里再增加多两个类,用于演示,并且为了避免干扰将之前演示创建过程的代码注释。

public class Service1 : IDisposable
{
    public Service1()
    {
        Console.WriteLine("Service1 Created");
    }

    public void Dispose()
    {
        Console.WriteLine("Service1 Dispose");
    }
}

public class Service2 : IDisposable
{
    public Service2()
    {
        Console.WriteLine("Service2 Created");
    }

    public void Dispose()
    {
        Console.WriteLine("Service2 Dispose");
    }
}

public class Service3 : IDisposable
{
    public Service3()
    {
        Console.WriteLine("Service3 Created");
    }

    public void Dispose()
    {
        Console.WriteLine("Service3 Dispose");
    }
}

public class Service4 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service4 Dispose");
    }
}

public class Service5 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service5 Dispose");
    }
}

之后后台服务程序也做一些修改

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
        services.AddSingleton<Service1>();
        services.AddScoped<Service2>();
        services.AddTransient<Service3>();
        // 这种方式依旧由容器创建实例,只不过提供了工厂方法
        services.AddSingleton<Service4>(provider => new Service4());
        // 这种方式是用外部创建实例,只有单例生命周期可用
        services.AddSingleton<Service5>(new Service5());
    })
    .Build();

await host.RunAsync();
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IServiceProvider _serviceProvider;

    public Worker(ILogger<Worker> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        #region 生命周期实例销毁

        Console.WriteLine("Service1 调用");
        var service1 = _serviceProvider.GetService<Service1>();

        // 创建作用域,与 Web 应用中的一次请求一样
        using (var scope = _serviceProvider.CreateScope())
        {
            Console.WriteLine("Service2 调用");
            var service2 = scope.ServiceProvider.GetService<Service2>();
            Console.WriteLine("即将结束作用域");

            Console.WriteLine("Service3 调用");
            var service3 = scope.ServiceProvider.GetService<Service3>();
        }

        Console.WriteLine("Service4 调用");
        var service4 = _serviceProvider.GetService<Service4>();
        Console.WriteLine("Service5 调用");
        var service5 = _serviceProvider.GetService<Service5>();

        #endregion
    }
}

这样要直接用命令启动应用,不能够通过 vs 调试,之后 Ctrl+C 停止应用的时候,输出如下:

在这里插入图片描述
通过输出可以看得到,瞬时生命周期服务和作用域生命周期服务在超出作用范围就会被释放,而单例生命周期服务则在应用关闭时才释放,同为单例生命周期的 Service5 没有被释放。

这里要提一下的是,在解析瞬时生命周期服务 Service3 的时候,示例代码中是放到一个单独的作用域中的,这是因为在通过 services.AddHostedService<Worker>(); 注入 Worker 的时候是注入为单例生命周期的,而在单例生命周期对象中解析其他生命周期的对象是会有问题的,这也是服务注入、解析需要注意的一个关键点。

一定要注意服务解析范围,不要在 Singleton 中解析 Transient 或 Scoped 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例,因为单例生命周期的服务对象只会在应用停止的时候释放,而单例对象都没释放,它的依赖项肯定不会释放)。允许的方式有:

  • 在 Scoped 或 Transient 服务中解析 Singleton 服务
  • 在 Scoped 或 Transient 服务中解析 Scoped 服务(不能和前面的Scoped服务相同)

如果要在单例生命周期示例中临时解析作用域、瞬时生命周期的服务,可以通过创建一个子作用域的方式。对子作用域 IServiceScope 的工作方式感兴趣的,可阅读一下这篇文章:细聊.Net Core中IServiceScope的工作方式。

3. ASP.NET Core默认服务

上一篇文章讲完了中间件,实际上一个中间件要正常进行工作,通常需要许多的服务配合进行,而中间件中的服务自然也是通过 Ioc 容器进行注册和注入的。前面也讲到,按照约定中间件的封装一般会提供一个 User{Middleware} 的扩展方法给用户使用,而服务注册中也有一个类似的约定,一般会有一个 Add{Services} 的扩展方法。

例如一个WebApi项目中,对于控制器路由终结点中间件的配置使用:

builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();

这也是我们在日常开发中可以学习的方式,随着业务增长,需要依赖注入的服务也越来越多,我们可以根据业务模块,通过扩展方法讲相应模块的服务注入注册进行封装,命名为 Add{Services},更加清晰明了地对我们的业务进行封装。

.NET Core 框架下默认提供250个以上的的服务,包括ASP.NET Core MVC、EF Core 等等,当然这些服务很多不会默认就注入到容器中,我们在新建一个项目的时候,不同项目框架的模板会帮我们默认配置好一些最基本的必须的服务,其他的服务我们可以根据自己的需要进行使用。

在这里插入图片描述

4. 依赖注入配置变形

随着业务的增长,我们项目工作中的类型、服务越来越多,而每一个服务的依赖注入关系都需要在入口文件通过 Service.Add{} 方法去进行注册,这将是非常麻烦的,入口文件需要频繁改动,而且代码组织管理也会变得麻烦,非常不优雅。

在许多框架中会对这种通过 Service.Add{} 的方式在代码中显式注册依赖注入关系的方式进行变形,有的可以通过配置文件进行注册,例如 Java Spring 框架就有这样大量的配置文件,有的可以通过接口进行默认注册,有的通过特性进行默认注册。

这里稍微简单介绍一下依赖注入默认注册的原理,其实也就是通过放射的一些手段,再加上一些约定好的规则而已。

首先需要三个生命周期接口,如下,这三个接口没有内容,仅仅只是作为标记而已。

public interface ISingleton
{
}
public interface IScoped
{
}
public interface ITransient
{
}

之后需要一个扩展方法,如下:

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionDependencyExtensions
    {
        public static IServiceCollection AddAutoInject<T>(this IServiceCollection services)
        {
            var register = new ServiceRegister();
            register.AddAssembly(services, typeof(T).Assembly);
            return services;
        }
    }
}

这个扩展方法中调用了注册器,往容器中注入服务,实现如下:

public class ServiceRegister
{
    public void AddAssembly(IServiceCollection services, Assembly assembly)
    {
        // 查找程序中的类型
        var types = assembly.GetTypes().Where(t => t != null && t.IsClass && !t.IsAbstract && !t.IsGenericType);
        // 遍历每一个类检查释放满足约定的规则
        foreach (var type in types)
        {
            AddType(services, type);
        }
    }

    /// <summary>
    /// 添加当前类型的依赖注入关系
    /// </summary>
    /// <param name="services"></param>
    /// <param name="type"></param>
    public void AddType(IServiceCollection services, Type type)
    {
        var lifetime = GetLifetimeOrNull(type);
        if (lifetime == null)
        {
            return;
        }

        var exposeServices = ExposeService(type);
        foreach (var serviceType in exposeServices)
        {
            var serviceDescriptor = new ServiceDescriptor(serviceType, type, lifetime.Value);
            services.Add(serviceDescriptor);
        }
    }

    /// <summary>
    /// 根据标记接口确定生命周期,如果没有添加标记接口的,则不会被自动注册到容器
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public ServiceLifetime? GetLifetimeOrNull(Type type)
    {
        if (typeof(ISingleton).IsAssignableFrom(type))
        {
            return ServiceLifetime.Singleton;
        }

        if(typeof(IScoped).IsAssignableFrom(type))
        {
            return ServiceLifetime.Scoped;
        }

        if(typeof(ITransient).IsAssignableFrom(type))
        {
            return ServiceLifetime.Transient;
        }

        return null;
    }

    /// <summary>
    /// 根据约定的规则查找当前类对于的服务类型
    /// 通过接口实现的方式,查找当前类实现的接口,如果一个接口名称去除了 "I" 之后与当前类的后半段一样,
    /// 则当前类应该被注册为这个接口的服务。
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public IList<Type> ExposeService(Type type)
    {
        var serviceTypes = new List<Type>();
        var interfaces = type.GetInterfaces();
        foreach (var interfacesType in interfaces)
        {
            var interfaceName = interfacesType.Name;
            if (interfaceName.StartsWith("I"))
            {
                interfaceName = interfaceName.Substring(1);
            }

            if (type.Name.EndsWith(interfaceName))
            {
                serviceTypes.Add(interfacesType);
            }
        }
        return serviceTypes;
    }
}

整体的逻辑就是查找遍历程序集中的所有类型,并通过判别类型是否实现之前定好的三个生命周期接口,从而确定类型是否需要自动注册到容器中,如果需要再根据约定好的规则获取需要注册的服务类型,并且构建服务描述器,再将其添加到容器中。

之后在入口文件中这样使用:

builder.Services.AddAutoInject<Program>();

而需要自动注入的服务只要多实现一个标记接口即可:

public class Rabbit : IRabbit, ITransient
{
}

以上主要介绍一下依赖注入自动化注册的思路和基本实现,代码只是一个基本的演示,比较简单,很多细节也没有在这里体现,但是核心的思路是和 ABP 框架中的自动注入的方式一样的,有兴趣详细了解一下 ABP 中的依赖注入的机制的童鞋,可以看一下我之前写的文章: ABP 依赖注入(1)

到这里,.NET Core 框架中默认的依赖注入框架基本的内容就讲完了,之后依赖注入的知识点还有:

  1. 第三方依赖注入框架的结合使用,
  2. .NET Core 默认依赖注入框架源码解析

不过这些得等一等时间了,下一篇还是会沿着 .NET Core 框架的一些基本知识点继续写下去。


参考文章:
ASP.NET Core 依赖注入 | Microsoft Learn
理解ASP.NET Core - 依赖注入(Dependency Injection)

ASP.NET Core 系列总结:
上一篇:ASP.NET Core — 请求管道与中间件
下一篇:ASP.NET Core — 配置系统

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值