了解依赖项注入
ASP.NET Core 应用通常需要跨多个组件访问相同的服务。 例如,多个组件可能需要访问从数据库提取数据的服务。 ASP.NET Core 使用内置依赖项注入 (DI) 容器来管理应用使用的服务。
依赖关系注入和控制反转 (IoC)
依赖项注入模式是控制反转 (IoC) 的一种形式。 在依赖项注入模式中,组件从外部源接收其依赖项,而不是自己创建。 这种模式将代码与依赖项分离,从而使代码更易于测试和维护。
请考虑以下 Program.cs 文件:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<PersonService>();
var app = builder.Build();
app.MapGet("/",
(PersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
}
);
app.Run();
以及以下 PersonService.cs 文件:
namespace MyApp.Services;
public class PersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
若要了解代码,请从突出显示的 app.MapGet
代码开始。 此代码将对根 URL (/
) 的 HTTP GET 请求映射到返回问候消息的委托。 委托的签名定义名为 PersonService
的 personService
参数。 当应用运行并且客户端请求根 URL 时,委托中的代码依赖于 服务来获取一些要添加到问候消息中的文本PersonService
。
委托在何处获取 PersonService
服务? 它由服务容器隐式提供。 突出显示的 builder.Services.AddSingleton<PersonService>()
行告知服务容器在应用启动时创建 PersonService
类的新实例,并将该实例提供给任何需要它的组件。
任何需要 PersonService
服务的组件都可以在其委托签名中声明 PersonService
类型的参数。 创建组件时,服务容器会自动提供 PersonService
类的实例。 委托本身不会创建 PersonService
实例,它只使用服务容器提供的实例。
接口和依赖项注入
为了避免依赖特定服务实现,可以改成为特定接口配置服务,然后只依赖于该接口。 通过此方法可以灵活地交换服务实现,从而使代码更易于测试且更易于维护。
请考虑 PersonService
类的接口:
public interface IPersonService
{
string GetPersonName();
}
此接口定义返回 GetPersonName
的单个方法 string
。 此 PersonService
类实现 IPersonService
接口:
internal sealed class PersonService : IPersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
可以将 PersonService
类注册为 IPersonService
接口的实现,而不是直接注册该类:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/",
(IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
}
);
app.Run();
此示例 Program.cs 在两个方面与前面的示例不同:
-
PersonService
实例注册为 接口的实现(而不是直接注册IPersonService
类)PersonService
。 - 委托签名现在需要
IPersonService
参数而不是PersonService
参数。
当应用运行并且客户端请求根 URL 时,服务容器会提供 PersonService
类的实例,因为它注册为 IPersonService
接口的实现。
提示
将 IPersonService
视为协定。 它定义实现必须具有的方法和属性。 委托需要 IPersonService
的实例。 它根本不关心基础实现,只是实例具有协定中定义的方法和属性。
使用依赖项注入进行测试
使用接口可以更轻松地独立测试组件。 可以创建 IPersonService
接口的模拟实现以进行测试。 在测试中注册模拟实现时,服务容器向要测试的组件提供模拟实现。
例如,假设 GetPersonName
类中的 PersonService
方法从数据库提取名称,而不是返回硬编码字符串。 若要测试依赖于 IPersonService
接口的组件,可以创建返回硬编码字符串的 IPersonService
接口的模拟实现。 要测试的组件不知道实际实现与模拟实现之间的差异。
另外,假设应用映射返回问候消息的 API 终结点。 终结点依赖于 IPersonService
接口来获取要问候的人员的姓名。 注册 IPersonService
服务和映射 API 终结点的代码可能如下所示:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/", (IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
});
app.Run();
这与前面使用 IPersonService
的示例类似。 委托需要服务容器提供的 IPersonService
参数。 如前所述,假定实现接口的 PersonService
从数据库中提取要问候的人员的姓名。
现在,请考虑以下用于测试同一 API 终结点的 XUnit 测试:
提示
如果你不熟悉 XUnit 或 Moq,请不要担心。 编写单元测试超出了本模块的范围。 此示例只是用于说明如何在测试中使用依赖项注入。
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using MyWebApp;
using System.Net;
public class GreetingApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public GreetingApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task GetGreeting_ReturnsExpectedGreeting()
{
//Arrange
var mockPersonService = new Mock<IPersonService>();
mockPersonService.Setup(service => service.GetPersonName()).Returns("Jane Doe");
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton(mockPersonService.Object);
});
}).CreateClient();
// Act
var response = await client.GetAsync("/");
var responseString = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello, Jane Doe!", responseString);
}
}
前面的测试:
- 创建返回硬编码字符串的
IPersonService
接口的模拟实现。 - 向服务容器注册模拟实现。
- 创建 HTTP 客户端以向 API 终结点发出请求。
- 断言来自 API 终结点的响应按预期进行。
测试并不关心 PersonService
类如何获取要问候的人员的姓名。 它只关心该姓名包含在问候消息中。 测试使用 IPersonService
接口的模拟实现来隔离要测试的组件与服务的实际实现。