了解相依性插入
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 介面的模擬實作來隔離要測試的元件與服務的實際實作。