Omówienie wstrzykiwania zależności

Ukończone

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 PersonService jest rejestrowane jako implementacja interfejsu IPersonService (w przeciwieństwie do bezpośredniego rejestrowania PersonService klasy).
  • Sygnatura delegata IPersonService oczekuje teraz parametru zamiast parametru PersonService .

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.