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






4.29/5 (9投票s)
在本文中,我们将探讨依赖注入在 ASP.NET Core 中的工作原理。
引言
在本文中,我们将探讨依赖注入在 ASP.NET Core 中的工作原理。我们将回顾 DI 的基本概念,并了解 DI 如何在 ASP.NET Core 中被视为一等公民。
本文的主要重点是讨论 ASP.NET Core 中的 DI。关于 DI、DIP、IoC 的基本概念,请参阅此处的前置文章
背景
在我们开始讨论依赖注入(DI)之前,让我们先尝试理解依赖倒置原则(DIP)和控制反转(IoC)。一旦我们理解了这些概念,我们就会看到依赖注入如何让我们创建符合这些原则的类。
重要提示:如上所述,以下关于 DIP、IOC 和 DI 的解释仅为定义部分。强烈建议在对这些概念的理解不清晰的情况下阅读以下文章
快速回顾基础知识
依赖倒置原则
依赖倒置原则是一项软件设计原则,它为我们提供了编写松耦合类的指导。根据依赖倒置原则的定义
- 高级模块不应依赖于低级模块。两者都应依赖于抽象。
- 抽象不应依赖于细节。细节应依赖于抽象。
控制反转
依赖倒置是一项软件设计原则,它只是说明了两个模块应如何相互依赖。现在的问题来了,我们到底要如何实现呢?
答案是控制反转。控制反转是实际的机制,通过该机制,我们可以让较高层模块依赖于抽象,而不是依赖于较低层模块的具体实现。
依赖注入
依赖注入主要用于将具体实现注入到使用抽象(即内部接口)的类中。依赖注入的主要思想是减少类之间的耦合,并将抽象和具体实现的绑定移出依赖类。
依赖注入的好处
拥有 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 中配置依赖注入的步骤,然后创建一个小型应用程序来实际演示。
配置依赖注入所需的步骤
- 定义契约/接口 - 定义将作为消费类契约的接口
- 实现契约/接口 - 实现一个遵循上述契约的具体类。它将被注入到消费类中。
- 注册服务 - 注册契约/接口到具体类的映射,以便框架知道应该为哪个接口注入哪个具体类。
- 使用契约/接口实现服务 - 在控制器构造函数中传递接口作为参数,让框架在运行时注入实际的具体类。
现在,在我们开始实际实现之前,我们需要理解关于上面第三点(注册服务)的一个重要概念,即服务生命周期。
理解服务生命周期
当我们注册一个服务,即接口到具体类的映射时,框架将负责实例化具体类并将其注入到消费类中。听到这个之后,脑海中会浮现一些问题
实例化类的生命周期是什么?我们能控制这些被实例化类的生命周期吗?
这些问题的答案是肯定的。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
的具体类。这个存储库将简单地将几本书添加到book
s 集合中,并将此集合返回给调用者。
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)
。我们需要使用IServiceCollection
的Add()
方法将我们的依赖项与内置的 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日:第一个版本