Omówienie wstrzykiwania zależności
aplikacje ASP.NET Core często muszą uzyskiwać dostęp do tych samych usług w wielu składnikach. Na przykład kilka składników może wymagać dostępu do usługi pobierającej dane z bazy danych. ASP.NET Core używa wbudowanego kontenera wstrzykiwania zależności (DI) do zarządzania usługami używanymi przez aplikację.
Wstrzykiwanie zależności i inwersja kontroli (IoC)
Wzorzec wstrzykiwania zależności jest formą inwersji kontrolki (IoC). We wzorcu wstrzykiwania zależności składnik odbiera jego zależności ze źródeł zewnętrznych, zamiast tworzyć je same. Ten wzorzec rozdziela kod z zależności, co ułatwia testowanie i konserwację kodu.
Rozważ następujący plik 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();
A następujący plik PersonService.cs :
namespace MyApp.Services;
public class PersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
Aby zrozumieć kod, zacznij od wyróżnionego app.MapGet kodu. Ten kod mapuje żądania HTTP GET dla głównego adresu URL (/) na delegata, który zwraca komunikat powitania. Podpis delegata definiuje PersonService parametr o nazwie personService. Gdy aplikacja jest uruchamiana, a klient żąda głównego adresu URL, kod wewnątrz delegata zależyPersonService od usługi, aby uzyskać jakiś tekst do uwzględnienia w wiadomości powitania.
Gdzie delegat uzyskuje usługę PersonService ? Jest ona niejawnie dostarczana przez kontener usługi. Wyróżniony builder.Services.AddSingleton<PersonService>() wiersz informuje kontener usługi o utworzeniu nowego wystąpienia PersonService klasy podczas uruchamiania aplikacji i udostępnienia tego wystąpienia dowolnemu składnikowi, który go potrzebuje.
Każdy składnik, który wymaga PersonService usługi, może zadeklarować parametr typu PersonService w podpisie delegata. Kontener usługi automatycznie udostępni wystąpienie PersonService klasy podczas tworzenia składnika. Delegat nie tworzy PersonService samego wystąpienia, a jedynie używa wystąpienia, które zapewnia kontener usługi.
Interfejsy i wstrzykiwanie zależności
Aby uniknąć zależności od określonej implementacji usługi, możesz zamiast tego skonfigurować usługę dla określonego interfejsu, a następnie zależeć tylko od interfejsu. Takie podejście zapewnia elastyczność zamiany implementacji usługi, co sprawia, że kod jest bardziej testowalny i łatwiejszy w obsłudze.
Rozważ interfejs dla PersonService klasy:
public interface IPersonService
{
string GetPersonName();
}
Ten interfejs definiuje pojedynczą metodę , GetPersonNamektóra zwraca stringwartość . Ta PersonService klasa implementuje IPersonService interfejs:
internal sealed class PersonService : IPersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
Zamiast bezpośrednio rejestrować klasę PersonService , możesz zarejestrować ją jako implementację interfejsu 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();
Ten przykład Program.cs różni się od poprzedniego przykładu na dwa sposoby:
- Wystąpienie
PersonServicejest rejestrowane jako implementacja interfejsuIPersonService(w przeciwieństwie do bezpośredniego rejestrowaniaPersonServiceklasy). - Sygnatura delegata
IPersonServiceoczekuje teraz parametru zamiast parametruPersonService.
Gdy aplikacja jest uruchamiana, a klient żąda głównego adresu URL, kontener usługi udostępnia wystąpienie PersonService klasy, ponieważ jest on zarejestrowany jako implementacja interfejsu IPersonService .
Napiwek
Pomyśl o IPersonService umowie. Definiuje metody i właściwości, które musi mieć implementacja. Delegat chce wystąpienia IPersonServiceklasy . Nie zależy to w ogóle na podstawowej implementacji, tylko że wystąpienie ma metody i właściwości zdefiniowane w kontrakcie.
Testowanie za pomocą wstrzykiwania zależności
Korzystanie z interfejsów ułatwia testowanie składników w izolacji. Możesz utworzyć pozorną implementację interfejsu IPersonService na potrzeby testowania. Podczas rejestrowania makiety implementacji w teście kontener usługi zapewnia pozorną implementację testowanego składnika.
Załóżmy na przykład, że zamiast zwracać zakodowany ciąg, GetPersonName metoda w PersonService klasie pobiera nazwę z bazy danych. Aby przetestować składnik, który zależy od interfejsu IPersonService , możesz utworzyć pozorną implementację interfejsu IPersonService , który zwraca zakodowany ciąg. Testowany składnik nie zna różnicy między rzeczywistą implementacją a pozorną implementacją.
Załóżmy również, że aplikacja mapuje punkt końcowy interfejsu API, który zwraca komunikat powitania. Punkt końcowy zależy od interfejsu IPersonService , aby uzyskać imię i nazwisko osoby do powitania. Kod rejestrujący usługę IPersonService i mapujący punkt końcowy interfejsu API może wyglądać następująco:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/", (IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
});
app.Run();
Jest to podobny do poprzedniego przykładu z IPersonService. Delegat oczekuje parametru IPersonService , który zapewnia kontener usługi. Jak wspomniano wcześniej, załóżmy, że PersonService implementuje interfejs pobiera imię i nazwisko osoby do powitania z bazy danych.
Teraz rozważ następujący test XUnit, który testuje ten sam punkt końcowy interfejsu API:
Napiwek
Nie martw się, jeśli nie znasz narzędzia XUnit lub Moq. Pisanie testów jednostkowych wykracza poza zakres tego modułu. W tym przykładzie pokazano, jak można użyć iniekcji zależności podczas testowania.
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);
}
}
Poprzedni test:
- Tworzy pozorną implementację interfejsu
IPersonService, który zwraca zakodowany ciąg. - Rejestruje pozorną implementację w kontenerze usługi.
- Tworzy klienta HTTP w celu wykonania żądania do punktu końcowego interfejsu API.
- Potwierdza, że odpowiedź z punktu końcowego interfejsu API jest zgodnie z oczekiwaniami.
Test nie obchodzi, jak PersonService klasa pobiera imię i nazwisko osoby do powitania. Zależy tylko na tym, że nazwa jest uwzględniona w wiadomości powitania. Test używa pozornej implementacji interfejsu IPersonService , aby odizolować składnik testowany od rzeczywistej implementacji usługi.