依赖注入
我们希望从Student对象数据源中查询特定的学生详细信息并将其显示在网页上。我们已经知道MVC中的Model包含了一组数据的类和管理该数据的逻辑信息。因此,为了表示想要显示的学生数据,可以使用以下Student类。
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
/// <summary>
/// 主修科目
/// </summary>
public string Major { get; set; }
public string Email{ get; set; }
}
我们在项目的根目录中创建一个Models文件夹。请注意,模型类并不需要强行放置在Models文件夹中,但将它们统一保存在Models文件夹中是一种很好的做法,便于以后找到它们。
除了呈现数据的Student类,模型还包含一个管理模型数据的类。为了管理数据,即查询和保存学生数据,我们将使用以下IStudentRepository服务。目前,我们只有一个方法**GetStudent()**通过学生ID查询学生。
public interface IStudentRepository
{
Student GetStudent(int ID);
}
以下StudentRepository类提供了IStudentRepository接口的实现。目前,StudentRepository类中的Student类数据都是采用的硬编码。在以后,我们将为IStudentRepository接口提供另一种实现——从SQL Server数据库中查询和保存数据。
public class StudentRepository : IStudentRepository
{
private List<Student> _studentList;
public StudentRepository()
{
_studentList = new List<Student>() {
new Student(){ID=1,Name="张三",Major="计科",Email="zhangsan@qq.com"},
new Student(){ID=2,Name="李四",Major="信安",Email="lisi@outlook.com"},
new Student(){ID=3,Name="王五",Major="网工",Email="wangwu@foxmail.com"}
};
}
public Student GetStudent(int ID)
{
return _studentList.FirstOrDefault(a => a.ID == ID);
}
}
为了便于项目后期的管理和规范,我们需要在项目根目录中创建一个DataRepositories文件用于存放Student的接口IStudentRepository以及接口的实现服务StudentRepository。如图所示:
现在我们将针对IStudentRepository接口进行编程,而不是具体实现StudentRepository。这种接口抽象化允许我们使用依赖注入,反过来也使应用程序灵活且易于单元测试,因此我们先来了解一下依赖注入。
了解ASP.NET Core中的依赖注入
在维基百科中对依赖注入是这样解释的:“依赖注入是一种软件设计模式,指一个或多个依赖(或服务)被注入,通过引用传递,传入一个依赖对象(或客户端)并成为客户状态的一部分。该模式通过自身的行为分离了客户依赖的创建,这允许程序设计是松耦合的,同时遵循依赖倒置和单一职责原则。与服务定位器模式直接进行对比,它允许用户了解他们用来查找依赖的机制。”
由于依赖注入的概念难免有点晦涩,因此在这里,我们将通过一个简单的例子了解依赖注入。
以前的做法是直接实例化类,代码如下。这种方式不推荐,因为它无法将系统解耦。
public string Index()
{
//不推荐的做法
var _studentRepository = new StudentRepository();
return _studentRepository.GetStudent(1).Name;
}
以下是推荐做法:
public class HomeController:Controller
{
private readonly IStudentRepository _studentRepository;
//使用构造函数注入的方式注入IStudentRepository
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
//返回学生的名字
public string Index()
{
return _studentRepository.GetStudent(1).Name;
}
}
代码说明如下:
- Homecontroller通过IStudentRepository接口来查询Student数据。
- 我们使用构造函数将IStudentRepository实例注入HomeController中,而不是HomeController对IStudentRepository接口创建新的实例化。
- 这称为构造函数依赖注入,因为我们使用构造函数来注入依赖项。
- 请注意,我们将注入的_studentRepository依赖项分配了只读字段readonly。这是一个很好的做法,因为它可以防止在方法中误操作地为其分配另一个值,比如null。
- 此时,如果我们运行项目,则会收到以下错误。
- 这是因为如果有人请求实现IStudentRepository的对象,ASP.NET Core依赖注入容器不知道要提供哪个对象实例,原因如下:
- IStudentRepository可能有多个实现。在我们的项目中只有一个实现,那就是StudentRepository。
- 顾名思义,StudentRepository使用内存中的学生模型数据。
- 以后,我们将为IStudentRepository提供另一个实现,该实现是从SQL Server数据库中查询学生数据。
- 现在,让我们先继续使用StudentRepository。
- 要修复InvalidOperationException错误,我们需要在ASP.NET Core中使用依赖注入组件,然后把StudentRepository类注册进去。
- 我们在Startup类的ConfigureServices()方法中执行注册。
使用依赖注入注册服务
ASP.NET Core提供以下3种方法来使用依赖注入注册服务。我们使用的方法决定了注册服务的声明周期。
-
📎AddSingleton()方法
AddSingleton()方法创建一个Singleton服务。首次请求时会创建Singleton服务,然后所有后续所有请求都使用相同的实例。因此,通常每个应用程序只创建一次Singleton服务,并且在整个应用程序生命周期中使用该单个实例。
-
📎AddTransient()方法
AddTransient()方法可以称作暂时性模式,它会创建一个Transient服务。每次请求时,都会创建一个新的Transient服务实例。
-
📎AddScoped()方法
AddScoped()方法创建一个Scoped服务。在范围内的每个请求中创建一个新的Scoped服务实例。比如,在Web应用程序中,它为每个HTTP请求创建一个实例,但在同一HTTP请求的其他调用中使用相同的实例;在一个客户端请求中是相同的,而在多个客户端请求中是不同的。
现在,要修复InvalidOperationException
错误,让我们使用AddSingleton()向ASP.NET Core依赖注入容器注册StudentRepository类方法。
在如下的代码中,如果调用IStudentRepository,则将调用StudentRepository的实例服务。
public void ConfigureServices(IServiceCollection services)
{
//添加MVC服务
services.AddControllersWithViews(a => a.EnableEndpointRouting = false);
//添加依赖注入
services.AddSingleton<IStudentRepository, StudentRepository>();
}
也可以使用new关键字在HomeController
中简单地创建StudentRepository类的实例,如下所示:
public class HomeController
{
private readonly IStudentRepository _studentRepository;
//使用构造函数注入的方式注入IStudentRepository
public HomeController(IStudentRepository studentRepository)
{
//在构造函数中直接实例化服务而不是接口注入
_studentRepository = new StudentRepository();
}
//返回学生的名字
public string Index()
{
return _studentRepository.GetStudent(1).Name;
}
}
这使HomeController与StudentRepository紧密耦合。以后我们会为IStudentRepository提供新的实现,如DatabaseStudentRepository。如果想要使用新的SqlStudentRepository,而不是DatabaseStudentRepository,我们必须更改HomeController中的代码。
读者可能会想,这只是更改一行代码,因此并不难。
但是如果我们在应用程序中的其它50个Controller中使用了这个StudentRepository,那么这50个Controller中的代码都得改,这不仅无聊而且容易出错。
简而言之,在代码中使用new关键字创建依赖关系的实例会产生紧密耦合,使应用程序很难更改,最后导致项目无法重构和优化。而通过依赖注入的方式,而不会有这种问题的出现。通过这种方法,即使在应用程序的其它50个其它Controller中使用了StudentRepository,如果我们想用不同的实现替换它,则只需要在Startup.cs文件中更改一下一行代码。请注意,我们现在使用DatabaseStudentRepository而不是StudentRepository。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(a => a.EnableEndpointRouting = false);
services.AddSingleton<IStudentRepository, DatabaseStudentRepository>();
}
这样带来的效果使单元测试也变得更加容易,因为我们可以通过依赖注入轻松地交换依赖项。
🐱💻总结
MVC中的Model是包含了一组数据的类和管理该数据的逻辑信息。
对于依赖注入,总结如下:
- ASP.NET Core中依赖注入提供的容器服务有以下3种:
- AddSingleton()。
- AddTransient()。
- AddScoped()。
- 依赖注入的优点如下:
- 降低耦合度。
- 让代码易于测试。
- 提供了面向对象编程的机制。