Vysvětlení injektáže závislostí

Dokončeno

ASP.NET aplikace Core často potřebují přístup ke stejným službám napříč několika komponentami. Například několik komponent může potřebovat přístup ke službě, která načítá data z databáze. ASP.NET Core ke správě služeb, které aplikace používá, používá integrovaný kontejner injektáže závislostí (DI).

Injektáž závislostí a inverze řízení (IoC)

Model injektáže závislostí je forma inverze řízení (IoC). V modelu injektáže závislostí přijímá komponenta své závislosti z externích zdrojů, nikoli z jejich samotného vytváření. Tento vzor odděluje kód od závislosti, což usnadňuje testování a údržbu kódu.

Zvažte následující Program.cs soubor:

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 následující PersonService.cs soubor:

namespace MyApp.Services;

public class PersonService
{
    public string GetPersonName()
    {
        return "John Doe";
    }
}

Abyste pochopili kód, začněte zvýrazněným app.MapGet kódem. Tento kód mapuje požadavky HTTP GET pro kořenovou adresu URL (/) na delegáta, který vrací zprávu s pozdravem. Podpis delegáta PersonService definuje parametr s názvem personService. Když se aplikace spustí a klient požádá o kořenovou adresu URL, kód uvnitř delegáta závisí na PersonService službu, aby získal určitý text, který se má zahrnout do zprávy s pozdravem.

Kde delegát službu získá PersonService ? Kontejner služby ho implicitně poskytuje. Zvýrazněný builder.Services.AddSingleton<PersonService>() řádek říká kontejneru služby, aby při spuštění aplikace vytvořil novou instanci PersonService třídy a poskytl ji libovolné komponentě, která ji potřebuje.

Každá komponenta, která službu potřebuje PersonService , může deklarovat parametr typu PersonService v podpisu delegáta. Kontejner služby automaticky poskytne instanci PersonService třídy při vytvoření komponenty. Delegát nevytvoří PersonService samotnou instanci, pouze používá instanci, kterou poskytuje kontejner služby.

Injektáž rozhraní a závislostí

Abyste se vyhnuli závislostem na konkrétní implementaci služby, můžete místo toho nakonfigurovat službu pro konkrétní rozhraní a pak záviset jen na rozhraní. Tento přístup vám dává flexibilitu prohodit implementaci služby, což usnadňuje testování kódu a usnadňuje údržbu.

Zvažte rozhraní pro PersonService třídu:

public interface IPersonService
{
    string GetPersonName();
}

Toto rozhraní definuje jednu metodu, GetPersonNamekterá vrací string. Tato PersonService třída implementuje IPersonService rozhraní:

internal sealed class PersonService : IPersonService
{
    public string GetPersonName()
    {
        return "John Doe";
    }
}

Místo přímé registrace PersonService třídy ji můžete zaregistrovat jako implementaci IPersonService rozhraní:

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();

app.MapGet("/", 
    (IPersonService personService) => 
    {
        return $"Hello, {personService.GetPersonName()}!";
    }
);
    
app.Run();

Tento příklad Program.cs se liší od předchozího příkladu dvěma způsoby:

  • Instance PersonService je registrována jako implementaceIPersonService rozhraní (na rozdíl od registrace PersonService třídy přímo).
  • Podpis delegáta IPersonService teď místo parametru PersonService očekává parametr.

Když aplikace spustí a klient požádá o kořenovou adresu URL, kontejner služby poskytne instanci PersonService třídy, protože je zaregistrovaná jako implementace IPersonService rozhraní.

Návod

IPersonService Představte si smlouvu. Definuje metody a vlastnosti, které musí mít implementace. Delegát chce instanci IPersonService. Nezáleží vůbec na základní implementaci, pouze to, že instance má metody a vlastnosti definované v kontraktu.

Testování pomocí injektáže závislostí

Použití rozhraní usnadňuje testování komponent v izolaci. Pro účely testování můžete vytvořit napodobenou implementaci IPersonService rozhraní. Když v testu zaregistrujete implementaci napodobení, kontejner služby poskytne napodobenou implementaci komponenty, která se testuje.

Řekněme například, že místo vrácení pevně zakódovaného řetězce GetPersonName metoda ve PersonService třídě načte název z databáze. Pokud chcete otestovat komponentu, která závisí na IPersonService rozhraní, můžete vytvořit napodobenou implementaci IPersonService rozhraní, která vrátí pevně zakódovaný řetězec. Testovaná komponenta nezná rozdíl mezi skutečnou implementací a implementací napodobení.

Předpokládejme také, že vaše aplikace mapuje koncový bod rozhraní API, který vrací zprávu s pozdravem. Koncový bod závisí na rozhraní pro IPersonService získání jména osoby, která se má pozdravit. Kód, který zaregistruje IPersonService službu a mapuje koncový bod rozhraní API, může vypadat takto:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IPersonService, PersonService>();

var app = builder.Build();

app.MapGet("/", (IPersonService personService) =>
{
    return $"Hello, {personService.GetPersonName()}!";
});

app.Run();

Toto je podobné předchozímu příkladu s IPersonService. Delegát očekává IPersonService parametr, který kontejner služby poskytuje. Jak už jsme zmínili dříve, předpokládejme, že PersonService implementuje rozhraní, načte jméno osoby, která má pozdravit z databáze.

Teď zvažte následující test XUnit, který testuje stejný koncový bod rozhraní API:

Návod

Nemějte obavy, pokud neznáte XUnit nebo Moq. Psaní testů jednotek je mimo rozsah tohoto modulu. Tento příklad ukazuje, jak lze injektáž závislostí použít při testování.

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);
    }
}

Předchozí test:

  • Vytvoří napodobenou implementaci IPersonService rozhraní, která vrátí pevně zakódovaný řetězec.
  • Zaregistruje implementaci napodobení v kontejneru služby.
  • Vytvoří klienta HTTP, který vytvoří požadavek na koncový bod rozhraní API.
  • Tvrdí, že odpověď z koncového bodu rozhraní API je podle očekávání.

Test nezajímá, jak PersonService třída získá jméno osoby, která se má pozdravit. Záleží jenom na tom, že jméno je součástí zprávy s pozdravem. Test používá napodobení implementace IPersonService rozhraní k izolaci komponenty testované od skutečné implementace služby.