65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 依赖注入入门教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.29/5 (9投票s)

2018年10月4日

CPOL

8分钟阅读

viewsIcon

17996

downloadIcon

226

在本文中,我们将探讨依赖注入在 ASP.NET Core 中的工作原理。

引言

在本文中,我们将探讨依赖注入在 ASP.NET Core 中的工作原理。我们将回顾 DI 的基本概念,并了解 DI 如何在 ASP.NET Core 中被视为一等公民。

本文的主要重点是讨论 ASP.NET Core 中的 DI。关于 DI、DIP、IoC 的基本概念,请参阅此处的前置文章

背景

在我们开始讨论依赖注入(DI)之前,让我们先尝试理解依赖倒置原则(DIP)和控制反转(IoC)。一旦我们理解了这些概念,我们就会看到依赖注入如何让我们创建符合这些原则的类。

重要提示:如上所述,以下关于 DIP、IOC 和 DI 的解释仅为定义部分。强烈建议在对这些概念的理解不清晰的情况下阅读以下文章

快速回顾基础知识

依赖倒置原则

依赖倒置原则是一项软件设计原则,它为我们提供了编写松耦合类的指导。根据依赖倒置原则的定义

  1. 高级模块不应依赖于低级模块。两者都应依赖于抽象。
  2. 抽象不应依赖于细节。细节应依赖于抽象。

控制反转

依赖倒置是一项软件设计原则,它只是说明了两个模块应如何相互依赖。现在的问题来了,我们到底要如何实现呢?

答案是控制反转。控制反转是实际的机制,通过该机制,我们可以让较高层模块依赖于抽象,而不是依赖于较低层模块的具体实现。

依赖注入

依赖注入主要用于将具体实现注入到使用抽象(即内部接口)的类中。依赖注入的主要思想是减少类之间的耦合,并将抽象和具体实现的绑定移出依赖类。

依赖注入的好处

拥有 DI 的主要好处是我们拥有松耦合的代码。由于没有一个类依赖于另一个类,它们只知道契约,因此可以在运行时注入实际的类。这在代码的可扩展性、可测试性和可维护性方面给我们带来了巨大的好处。

我们已经看到了依赖注入如何帮助我们实现依赖倒置原则,但同样重要的是要注意,拥有 DI 还可以使我们的代码符合开闭原则(OCP),因为类现在只依赖于接口,而具体实现会注入到它们之中。这意味着所有类都以接口的形式提供了钩子,并且可以轻松添加新的实现(新的具体类)来实现新功能,而无需修改现有代码。

IoC 容器

如果我们只有一级依赖,那么我们讨论过的所有三种依赖注入方法都是可以的。但如果具体类也依赖于其他抽象呢?那么如果我们有链式和嵌套的依赖,实现依赖注入就会变得相当复杂。

这时我们就可以使用 IoC 容器了。当我们在处理链式或嵌套的依赖关系时,IoC 容器将帮助我们轻松地映射依赖关系。

理解 ASP.NET Core 中的依赖注入

ASP.NET Core 的优点在于,框架本身就将依赖注入视为一等公民。ASP.NET Core 开箱即用地提供了 IoC 容器。

事实上,DI 在 ASP.NET Core 框架中非常普遍,即使是框架内部的配置、路由、日志记录等依赖关系,也预先配置为使用内置的依赖注入机制。

ASP.NET Core 支持构造函数注入和方法注入。对于构造函数注入,我们只需定义接受契约/接口作为参数的构造函数,如果依赖关系已正确注册,实际的具体对象将自动注入。为了更好地理解这一点,让我们先来看一下在 ASP.NET Core 中配置依赖注入的步骤,然后创建一个小型应用程序来实际演示。

配置依赖注入所需的步骤

  1. 定义契约/接口 - 定义将作为消费类契约的接口
  2. 实现契约/接口 - 实现一个遵循上述契约的具体类。它将被注入到消费类中。
  3. 注册服务 - 注册契约/接口到具体类的映射,以便框架知道应该为哪个接口注入哪个具体类。
  4. 使用契约/接口实现服务 - 在控制器构造函数中传递接口作为参数,让框架在运行时注入实际的具体类。

现在,在我们开始实际实现之前,我们需要理解关于上面第三点(注册服务)的一个重要概念,即服务生命周期。

理解服务生命周期

当我们注册一个服务,即接口到具体类的映射时,框架将负责实例化具体类并将其注入到消费类中。听到这个之后,脑海中会浮现一些问题

实例化类的生命周期是什么?我们能控制这些被实例化类的生命周期吗?

这些问题的答案是肯定的。ASP.NET Core 提供了各种注册服务依赖关系的方式,这些方式最终决定了被实例化对象的生命周期。它们有三种类型

  • 单例 (Singleton):顾名思义,此选项只会创建一个实例,并且该实例将由所有消费对象共享。
  • 瞬态 (Transient):组件在每次请求时都会被创建。实例永远不会与其他对象共享。
  • 作用域 (Scoped):在这里,对象实例将为每个 HTTP 请求创建。因此,从应用程序的角度来看,每个请求都有自己的对象,但从消费类的角度来看,我们可以将其视为请求单例。

当我们查看代码时,将详细介绍如何使用此选项。

Using the Code

让我们创建一个示例应用程序来实际演示这一切。让我们采用逐步创建虚拟应用程序的方法,以便我们可以看到依赖注入在 ASP.NET Core 中的工作原理。

定义契约/接口

让我们从定义应用程序中将要使用的契约开始。让我们以创建一个用于从数据库检索书籍列表的存储库为例。此存储库的示例接口如下所示

public interface IBooksRepository
{
	List<Book> GetAll();
}

此接口中使用的模型是Book模型。

public class Book
{
	public int ID { get; set; }
	public string Name { get; set; }
	public string ISBN { get; set; }
}

有了基本的契约/接口后,让我们继续实际实现存储库。

实现契约/接口

为了实现IBooksRepository,让我们创建一个名为InMemoryBooksRepository的具体类。这个存储库将简单地将几本书添加到books 集合中,并将此集合返回给调用者。

public class InMemoryBooksRepository : IBooksRepository
{
	public List<Book> GetAll()
	{
		List<Book> books = new List<book>();

		books.Add(new Book { ID = 1, ISBN = "Test ISBN 1", Name = "Test Book 1" });
		books.Add(new Book { ID = 1, ISBN = "Test ISBN 1", Name = "Test Book 1" });
		books.Add(new Book { ID = 1, ISBN = "Test ISBN 1", Name = "Test Book 1" });

		return books;
	}
}

现在我们已经准备好了一个实现我们契约的具体类。现在让我们看看如何注册这个依赖项。

注册服务

要注册依赖项,我们需要在Startup类中查找ConfigureServices(IServiceCollection)。我们需要使用IServiceCollectionAdd()方法将我们的依赖项与内置的 ASP.NET Core IoC 容器进行注册。

首先,让我们检查一下从对象生命周期的角度来看,有哪些选项可以注册依赖项。

services.Add(new ServiceDescriptor(typeof(IBooksRepository), typeof(InMemoryBooksRepository)));

当我们使用此Add方法时,默认的服务生命周期是单例。如果我们想覆盖此行为,我们需要将该行为也作为参数传递。

services.Add(new ServiceDescriptor(typeof(IBooksRepository), 
      typeof(InMemoryBooksRepository), ServiceLifetime.Singleton));
services.Add(new ServiceDescriptor(typeof(IBooksRepository), 
      typeof(InMemoryBooksRepository), ServiceLifetime.Scoped));
services.Add(new ServiceDescriptor(typeof(IBooksRepository), 
      typeof(InMemoryBooksRepository), ServiceLifetime.Transient));

除了使用add方法并传递生命周期选项外,我们还可以使用框架提供的扩展方法,以以下方式配置具有所需生命周期的依赖项

services.AddScoped<IBooksRepository, InMemoryBooksRepository>();
services.AddSingleton<IBooksRepository, InMemoryBooksRepository>();
services.AddTransient<IBooksRepository, InMemoryBooksRepository>();

现在我们已经看到了如何使用可能的生命周期选项注册依赖项,让我们看看对于我们当前的示例,什么才是合理的。由于我们希望我们的repository类在每次被请求时都被实例化,因此我们将仅将其配置为Transient

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<IBooksRepository, InMemoryBooksRepository>();

	services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

现在我们已经将依赖项注册到了内置的 IoC 容器中。现在让我们看看消费类应该如何编写,以便对象能够被注入到它们之中。

使用契约/接口实现服务

我们知道内置容器支持构造函数注入。所以我们需要创建一个控制器,其构造函数接受此接口参数。然后,我们可以使用此接口句柄来调用实际的存储库功能。

[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
	IBooksRepository m_booksRepository = null;

	public BooksController(IBooksRepository booksRepo)
	{
		m_booksRepository = booksRepo;
	}

	// GET: api/Books
	[HttpGet]
	public List<Book> Get()
	{
		return m_booksRepository.GetAll();
	}       
}

有了这些代码,我们就拥有了声明、定义、配置和使用我们的依赖项所需的所有操作。让我们运行应用程序,看看实际效果。

关于方法注入的说明

如果我们想要方法注入,方法参数应使用FromServices属性进行装饰,以便框架能够识别该参数需要注入依赖项。以下是这方面的示例代码

public List<Book> GetAllBooks([FromServices]IBooksRepository booksRepo)
{
	return booksRepo.GetAll();
}

关注点

在本文中,我们探讨了依赖注入如何成为 ASP.NET Core 框架中的一等公民。我们还探讨了如何在 ASP.NET Core 中使用 DI,以及所有可能的生命周期选项。本文是从初学者的角度编写的。希望它能有所启发。

另外,如果有人想了解 IoC 容器的内部工作原理,这里有一个我出于好玩而编写的简陋的 IoC 容器。它可以帮助您了解 IoC 容器的内部工作原理

历史

  • 2018年10月4日:第一个版本
© . All rights reserved.