.NET Core开发实战--02.依赖注入

本文深入解析依赖注入(DI)的概念,展示如何通过DI实现对象与依赖间的松散耦合,提高代码的可维护性和可扩展性。以ASP.NET Core为例,介绍依赖注入框架的核心组件和服务生命周期管理,包括单例、瞬时和作用域服务的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是依赖注入
依赖注入,英文是Dependency Injection一般简称DI,是实现对象与其协作者或依赖关系之间松散耦合的技术。为了执行其操作,类所需要的对象不是直接实例化协作者或使用静态引用,而是以某种方式提供给类。大多数情况下,类将通过他们的构造函数来声明他们的依赖关系,允许他们遵循显示依赖原则,这种方法被称为“构造注入”。

在设计时考虑到DI,它们更加松散耦合,因为他们没有直接的,硬编码的依赖于他们的合作者。 这遵循依赖性反转原则,其中指出“高级模块不应该依赖于低级模块;两者都应该取决于抽象”。 除了引用特定的实现之外,类请求构造类时提供给它们的抽象(通常是接口)。 将依赖关系提取到接口中并将这些接口的实现提供为参数也是策略设计模式的一个示例。

当系统被设计为使用DI时,有许多类通过它们的构造方法(或属性)请求它们的依赖关系,有一个专门用于创建这些类及其关联的依赖关系的类是有帮助的。 这些类被称为容器,或更具体地称为控制反转(IoC)容器或依赖注入(DI)容器。 容器本质上是一个工厂,负责提供从它请求的类型的实例。 如果给定类型已声明它具有依赖关系,并且容器已配置为提供依赖关系类型,那么它将创建依赖关系作为创建请求的实例的一部分。 以这种方式,可以将复杂的依赖关系图提供给类,而不需要任何硬编码的对象构造。 除了创建具有依赖关系的对象之外,容器通常会在应用程序中管理对象生命周期。

为什么要使用依赖注入

  • 借助依赖注入框架,我们可以轻松管理类之间的依赖,帮助我们在构建应用时遵循设计原则,确保代码的可维护性和可扩展性
  • ASP.NET Core 在整个架构中,依赖注入框架提供了对象创建和生命周期管理的核心能力,各个组件相互协作,也是由依赖注入框架的能力来实现的

组件包

  • Microsoft.Extensions.DependencyInjection.Abstractions
  • Microsoft.Extensions.DependencyInjection

依赖注入的核心是以上两个组件包,一个是抽象包,一个是具体的实现
这里用到了接口实现分离的设计模式
组件只需要依赖抽象接口,而不需要依赖具体实现,当使用的时候注入它的具体实现即可。这样做的好处是可以在使用时决定具体的实现,也就意味着未来可以做任意的扩展,替换依赖注入框架的具体实现。
默认情况下,使用.NET Core 提供的内置依赖注入框架,也可以使用第三方的依赖注入框架来替换默认实现

依赖注入的核心类型

// ServiceCollection  是负责服务的注册
IServiceCollection

// ServiceDescriptor 就是每一个服务注时的信息
ServiceDescriptor

// ServiceProvider 是具体的容器,是由ServiceCollection Build 出来的
IServiceProvider

// ServiceScope 表示一个容器的子容器的生命周期
IServiceScope

新建一个ASP.NET Core API 项目
添加Services文件夹,在Services文件夹下新建三个代表不同生命周期的服务

// 单例
public interface IMySingletonService { }
public class MySingletonService : IMySingletonService
 {
  }
// 作用域
public interface IMyScopedService{ }
public class MyScopedService: IMyScopedService
 {
  }
// 瞬时
public interface IMyTransientService{ }
public class MyTransientService: IMyTransientService
 {
  }

在Startup.ConfigureServices 中注册服务

public void ConfigureServices(IServiceCollection services)
{
	#region 注册不同生命周期的服务
	// 单例模式
	services.AddSingleton<IMySingletonService, MySingletonService>();
	// 作用域 的生命周期
    services.AddScoped<IMyScopedService, MyScopedService>();
    // 瞬时生命周期
    services.AddTransient<IMyTransientService, MyTransientService>();
    #endregion
    services.AddControllers();
}

在Controller中获取注册的服务

[HttpGet]
public int GetService([FromServices] IMySingletonService singleton1,
                      [FromServices] IMySingletonService singleton2,
                      [FromServices] IMyTransientService transient1,
                      [FromServices] IMyTransientService transient2,
                      [FromServices] IMyScopedService scope1,
                      [FromServices] IMyScopedService scope2)
{
    Console.WriteLine($"singleton1:{singleton1.GetHashCode()}");
    Console.WriteLine($"singleton2:{singleton2.GetHashCode()}");

    Console.WriteLine($"transient1:{transient1.GetHashCode()}");
    Console.WriteLine($"transient2:{transient2.GetHashCode()}");

    Console.WriteLine($"scope1:{scope1.GetHashCode()}");
    Console.WriteLine($"scope2:{scope2.GetHashCode()}");

    return 1;
}

启动并刷新接口,得到两次输出

-- 第一次请求
singleton1:23737571
singleton2:23737571
transient1:66394946
transient2:20706689
scope1:32187286
scope2:32187286
-- 第二次请求
singleton1:23737571
singleton2:23737571
transient1:36181605
transient2:28068188
scope1:33163964
scope2:33163964

从输出结果可以看出

  • 单例模式两次HashCode一致,说明得到的是同一个对象
  • 瞬时模式下每次的HashCode都不一样,说明瞬时服务每次获取对象都是一个新的对象
  • 范围服务每个请求的HashCode一致,不同请求得到的对象实例是不同的

这里我们使用的是通过泛型的方式来注册我们的服务,但是注册服务实际上我们可以支持很多其他的注册方式。
我们可以直接将实例注册进去,也就是说我们已经有了一个单例的实例然后注册到服务里面去。

public interface IOrderService
{
}
public class OrderService : IOrderService
{
}
public class OrderServiceEx : IOrderService
{
}

// Startup.cs
public void ConfigureServices(IServiceCollection services){
	// 直接注册实例
	services.AddSingleton<IOrderService>(new OrderService());
}

当然这里我们也可以通过工厂方式来注册

// Startup.cs
public void ConfigureServices(IServiceCollection services){
	// 工厂方式注册
	//public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
	services.AddSingleton<IOrderService>(serviceProvider =>
	{
    	return new OrderServiceEx();
   	});
   	services.AddScoped<IOrderService>(serviceProvider =>
	{
		//serviceProvider.GetService<T>();
    	return new OrderServiceEx();
   	});
}

我们注入了一个Factory,他的入参是IServiceProvider。工厂模式是适用于单例模式,范围模式和瞬时模式的。
但是这里我们可以注意到我们是可以使用serviceProvider这个参数的,也就意味着我们可以从容器获取多个对象然后进行组装,得到我们最终的实现实例。
也就是说我们可以把我们的工厂类设计的比较复杂,比如说我们的实现类依赖了我们容器里面的另外一个类的情况,就可以使用,或者说我们期望用另外一个类来包装我原有的实现的时候。
这是我们向容器添加我们的服务的时候,实际上我们还有一些扩展方法是尝试注册
尝试注册的是指什么呢?
比如说我们这里把我们的OrderService重新注册一遍

public void ConfigureServices(IServiceCollection services)
{
    #region 注册不同生命周期的服务
    // 单例模式
    services.AddSingleton<IMySingletonService, MySingletonService>();
    // Scope 的生命周期
    services.AddScoped<IMyScopedService, MyScopedService>();
    // 瞬时生命周期
    services.AddTransient<IMyTransientService, MyTransientService>();
    #endregion

    #region 
    //直接注册实例
    services.AddSingleton<IOrderService>(new OrderService());
    #endregion
	// 尝试注册,如果服务已经注册则不再继续注册
    services.TryAddSingleton<IOrderService, OrderServiceEx>();
    services.AddControllers();
}

比如我们在控制器增加方法

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

从输出结果看只能得到一个OrderService 的实例,说明后面的TryAddXXX 是没有注册的
这里还有一个扩展方法是TryAddEnumerable,这个方法的作用是对于相同接口的不同实现是可以注册进入容器的,比如说

services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());

因为OrderService这个实现之前已经注册过,所以这两个方法只能将OrderServiceEx注册进入容器。
这样的好处是可以避免服务的重复注册,也可以控制一个服务需要不同的实现时也能够注册。
还有两个比较有意思的方法,Replace 和 RemoveAll

// RemoveAll 是用来移除所有注册的指定服务类型
services.RemoveAll<IOrderService>();
// Replace 方法会替换掉我们注册的服务的第一个实现
services.Replace(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());

还有一个比较常见的场景,是泛型的注册,当我们需要注册一组泛型实现时,我们注册的时候是不知道泛型类的具体入参的。依赖注入框架为我们提供了泛型模板的注册方式,这意味着我们可以把泛型模板注册进去,通过一行代码来注册所有的此泛型的具体实现。

// 泛型模板注册
services.AddSingleton(typeof(IGenericService<>),typeof(GenericService<>));
// 泛型模板的使用
// 泛型类的具体实现是可以用容器里面的任意类型来代替的
public WeatherForecastController(ILogger<WeatherForecastController> logger,
    IOrderService orderService,
    IGenericService<IOrderService> genericService)
{
    _orderService = orderService;
    _genericService = genericService;
    _logger = logger;
}

这里有两种依赖注入实例的获取方式,一种是构造函数,一种是FromServices。这两种方式使用的场景是有所区别的

  • 如果服务是整个控制器大部分接口需要使用的,则推荐使用构造注入
  • 如果只是某个接口单独使用的,则可以使用FromServices注入
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值