第5课:依赖注入:良好架构的起点

学习分享 丨作者 / 郑 子 铭 丨公众号 / DotNet NB / CloudNative NB

为什么要使用依赖注入框架

  • 借助依赖注入框架,我们可以轻松管理类之间的依赖,帮助我们在构建应用时遵循设计原则,确保代码的可维护性和可扩展性

  • ASP.NET Core 的整个架构中,依赖注入框架提供了对象创建和生命周期管理的核心能力,各个组件相互协作,也是由依赖注入框架的能力来实现的

组件包

  • Microsoft.Extensions.DependencyInjection.Abstractions

  • Microsoft.Extensions.DependencyInjection

依赖注入的核心是以上两个组件包,一个是抽象包,一个是具体的实现

这里用到了一个经典的设计模式,接口实现分离模式

组件只需要依赖抽象接口,而不需要依赖具体实现,当使用的时候注入它的具体实现即可

这样做的好处是可以在使用时决定具体的实现,也就意味着未来可以做任意的扩展,替换依赖注入框架的具体实现

默认情况下,使用 .NET Core 提供的内置依赖注入框架,也可以使用第三方的依赖注入框架来替换默认实现

核心类型

  • IServiceCollection:服务的注册

  • ServiceDescriptor:每一个服务注册时的信息

  • IServiceProvider:具体的容器,由 ServiceCollection Build 产生

  • IServiceScope:一个容器的子容器的生命周期

生命周期

  • 单例 Singleton:在整个根容器的生命周期内,都是单例,不管是子容器还是根容器,与作用域的区别是:一个是全局的,一个是范围的单例

  • 作用域 Scoped:在 Scope 的生存周期内,也就是容器的生存周期内,或者子容器的生存周期内,如果我的容器释放掉,我的对象也会释放掉

  • 瞬时(暂时)Transient:每一次从容器里面获取对象时,都可以得到一个全新的对象

新建一个 ASP.NET Core Web 应用程序 DependencyInjectionDemo,选择API

添加一个 Services 文件夹,新建三个服务代表三个生命周期的服务

namespace DependencyInjectionDemo.Services
{
    public interface IMyScopedService { }
    public class MyScopedService : IMyScopedService
    {
    }
}
namespace DependencyInjectionDemo.Services
{
    public interface IMySingletonService { }
    public class MySingletonService : IMySingletonService
    {
    }
}
namespace DependencyInjectionDemo.Services
{
    public interface IMyTransientService { }
    public class MyTransientService : IMyTransientService
    {
    }
}

在 Startup 中注册服务

public void ConfigureServices(IServiceCollection services)
{
    #region 注册服务不同生命周期的服务

    // 将单例的服务注册为单例的模式
    services.AddSingleton<IMySingletonService, MySingletonService>();

    // Scoped 的服务注册为 Scoped 的生命周期
    services.AddScoped<IMyScopedService, MyScopedService>();

    // 瞬时的服务注册为瞬时的生命周期
    services.AddTransient<IMyTransientService, MyTransientService>();

    #endregion

    services.AddControllers();
}

在 Controller 里面获取我们的服务

// FromServices 标注的作用是从容器里面获取我们的对象
// 每个对象获取两遍,用于对比每个生命周期获取的对象是什么样子的
// HashCode 代表对象的唯一性
[HttpGet]
public int GetService(
    [FromServices]IMySingletonService singleton1,
    [FromServices]IMySingletonService singleton2,
    [FromServices]IMyTransientService transient1,
    [FromServices]IMyTransientService transient2,
    [FromServices]IMyScopedService scoped1,
    [FromServices]IMyScopedService scoped2)
{
    Console.WriteLine($"singleton1:{singleton1.GetHashCode()}");
    Console.WriteLine($"singleton2:{singleton2.GetHashCode()}");
    Console.WriteLine($"transient1:{transient1.GetHashCode()}");
    Console.WriteLine($"transient2:{transient2.GetHashCode()}");
    Console.WriteLine($"scoped1:{scoped1.GetHashCode()}");
    Console.WriteLine($"scoped2:{scoped2.GetHashCode()}");
    Console.WriteLine($"========请求结束========");
    return 1;
}

注释 Get 方法

//[HttpGet]
//public IEnumerable<WeatherForecast> Get()
//{
//    var rng = new Random();
//    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
//    {
//        Date = DateTime.Now.AddDays(index),
//        TemperatureC = rng.Next(-20, 55),
//        Summary = Summaries[rng.Next(Summaries.Length)]
//    })
//    .ToArray();
//}

启动程序,刷新浏览器再次访问接口,输出如下:

单例模式两次的 HashCode 没有变化

两个瞬时服务两次的 HashCode 完全不同,意味着瞬时服务每次请求都会得到一个新对象

范围服务每个请求内是相同的,不同的请求之间得到的对象实例是不同的

除了使用泛型的方式注册服务之外,还有其他的方式

添加一个 OrderService

public interface IOrderService
{

}

public class OrderService1 : IOrderService
{

}


public class OrderService2 : IOrderService
{

}

在 Startup 中注册服务

public void ConfigureServices(IServiceCollection services)
{
    #region 注册服务不同生命周期的服务

    // 将单例的服务注册为单例的模式
    services.AddSingleton<IMySingletonService, MySingletonService>();

    // Scoped 的服务注册为 Scoped 的生命周期
    services.AddScoped<IMyScopedService, MyScopedService>();

    // 瞬时的服务注册为瞬时的生命周期
    services.AddTransient<IMyTransientService, MyTransientService>();

    #endregion

    #region 花式注册

    services.AddSingleton<IOrderService>(new OrderService1());  //直接注入实例

    #endregion

    #region 尝试注册(如果服务已经注册过,则不再注册)

    services.TryAddSingleton<IOrderService, OrderService2>();

    #endregion

    services.AddControllers();
}

在服务端 WeatherForecastController 定义另外一个接口

// IEnumerable<IOrderService>:获取曾经注册过的所有 IOrderService
public int GetServiceList([FromServices]IEnumerable<IOrderService> services)
{
    foreach (var item in services)
    {
        Console.WriteLine($"获取到服务实例:{item.ToString()}:{item.GetHashCode()}");
    }
    return 1;
}

调整一下程序的启动页面,Properties 下的 launchSetting.json 的这一行代码

"launchUrl": "weatherforecast/getservicelist",

修改路由

[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase

启动程序,输出如下:

获取到服务实例:DependencyInjectionDemo.Services.OrderService1:25560520

只有一个实例,说明 TryAddSingleton 没有生效

接着,注册两个服务

services.AddSingleton<IOrderService>(new OrderService1());
services.AddSingleton<IOrderService, OrderService2>();

启动程序,输出如下:

获取到服务实例:DependencyInjectionDemo.Services.OrderService1:16991442
获取到服务实例:DependencyInjectionDemo.Services.OrderService2:25560520

结果获取到了两个实例

接下来,了解一下 TryAddEnumerable 与 TryAddSingleton 的区别

#region 尝试注册(如果服务已经注册过,则不再注册)

services.TryAddSingleton<IOrderService, OrderService2>();// 接口类型重复,则不注册
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService1>());// 相同类型的接口,实现类相同,则不注册

#endregion

注册服务

services.AddSingleton<IOrderService>(new OrderService1());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService1>());

启动程序,输出如下:

获取到服务实例:DependencyInjectionDemo.Services.OrderService1:53046438

因为已经注册过 OrderService,所以第二句代码不生效

以不同的实现注册服务

services.AddSingleton<IOrderService>(new OrderService1());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService2>());

启动程序,输出如下:

获取到服务实例:DependencyInjectionDemo.Services.OrderService1:24219861
获取到服务实例:DependencyInjectionDemo.Services.OrderService2:38855053

这样就可以获取到两个服务实例

刷新浏览器,再执行一遍

获取到服务实例:DependencyInjectionDemo.Services.OrderService1:24219861
获取到服务实例:DependencyInjectionDemo.Services.OrderService2:38855053
获取到服务实例:DependencyInjectionDemo.Services.OrderService1:24219861
获取到服务实例:DependencyInjectionDemo.Services.OrderService2:38855053

因为注册的是单例,所以两次请求获取到的实例都是相同的

这样做的好处是:一方面避免一个服务重复注册,也可以控制一个服务需要注册不同的实现

注册完毕之后,想替换某些组件的某些部分时,可以使用 Replace 和 RemoveAll

services.AddSingleton<IOrderService>(new OrderService1());
services.Replace(ServiceDescriptor.Singleton<IOrderService, OrderService2>());// 替换掉注册的第一个实现

启动程序,输出如下:

获取到服务实例:DependencyInjectionDemo.Services.OrderService2:25560520

从结果看出,注册的 OrderService1 被替换为 OrderService2

下面介绍 RemoveAll

services.AddSingleton<IOrderService>(new OrderService1());
services.AddSingleton<IOrderService, OrderService2>();
services.RemoveAll<IOrderService>();// 移除所有 IOrderService 的注册

这种情况下程序会报错,因为所有 IOrderService 的注册被移除

Unable to resolve service for type 'DependencyInjectionDemo.Services.IOrderService'

下面介绍如何注册泛型模板

当需要注册一组泛型实现的时候

实际上注册的时候并不知道泛型类的具体类型入参

依赖注入框架为我们提供了泛型模板的注册方式

通过一行代码来注册所有此泛型的具体实现

定义一个泛型接口

namespace DependencyInjectionDemo.Services
{

    public interface IGenericService<T>
    {

    }

    public class GenericService<T> : IGenericService<T>
    {
        public T Data { get; private set; }
        public GenericService(T data)
        {
            this.Data = data;
        }

    }
}

泛型模板注册方法

services.AddSingleton(typeof(IGenericService<>), typeof(GenericService<>));

它的生命周期与之前的注册方式是一致的

不过它无法通过泛型 API 注册

需要注册两个 service 的 type

第一个入参是服务的类型

第二个入参是服务实现的类型

接下来,看看如何在 controller 中使用

// 在构造函数中添加两个入参,IOrderService 和 IGenericService
// 通过断点调试查看 genericService 的类型可得知,泛型的具体实现可以用容器里面的任意类型来替代
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOrderService orderService, IGenericService<IOrderService> genericService)
{
    _orderService = orderService;
    _logger = logger;
}

在 controller 中有两种依赖注入的实例的获取方式:

  • 通过 controller 构造函数注入

  • 通过 [FromServices] 注入

当定义一个 controller 的时候

它的服务是大部分接口都需要使用的情况下

推荐的做法是用构造函数注入的方式

如果这个服务仅仅在某一个接口使用的情况下

推荐使用 [FromServices] 注入

GitHub源码链接:https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/DependencyInjectionDemo

Last updated