Inzicht in afhankelijkheidsinjectie

Voltooid

ASP.NET Core-apps hebben vaak toegang nodig tot dezelfde services in meerdere onderdelen. Verschillende onderdelen moeten bijvoorbeeld toegang hebben tot een service waarmee gegevens uit een database worden opgehaald. ASP.NET Core maakt gebruik van een ingebouwde afhankelijkheidsinjectiecontainer (DI) om de services te beheren die een app gebruikt.

Afhankelijkheidsinjectie en Inversion of Control (IoC)

Het patroon voor afhankelijkheidsinjectie is een vorm van Inversion of Control (IoC). In het patroon voor afhankelijkheidsinjectie ontvangt een onderdeel de afhankelijkheden van externe bronnen in plaats van deze zelf te maken. Met dit patroon wordt de code losgekoppeld van de afhankelijkheid, waardoor code gemakkelijker te testen en te onderhouden is.

Houd rekening met het volgende Program.cs bestand:

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

En het volgende PersonService.cs bestand:

namespace MyApp.Services;

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

Als u de code wilt begrijpen, begint u met de gemarkeerde app.MapGet code. Met deze code worden HTTP GET-aanvragen voor de hoofd-URL (/) toegewezen aan een gemachtigde die een begroetingsbericht retourneert. De handtekening van de gedelegeerde definieert een PersonService parameter met de naam personService. Wanneer de app wordt uitgevoerd en een client de basis-URL aanvraagt, is de codestructuur in de gedelegeerde afhankelijk van de PersonService service voor het verkrijgen van tekst die aan het begroetingsbericht wordt toegevoegd.

Waar haalt de gedelegeerde de PersonService service op? Deze wordt impliciet geleverd door de servicecontainer. De gemarkeerde builder.Services.AddSingleton<PersonService>() regel vertelt de servicecontainer dat er een nieuw exemplaar van de PersonService klasse moet worden gemaakt wanneer de app wordt gestart en dat exemplaar moet worden verstrekt aan elk onderdeel dat deze nodig heeft.

Elk onderdeel dat de PersonService service nodig heeft, kan een parameter van het type PersonService declareren in de gedelegeerde handtekening. De servicecontainer levert automatisch een exemplaar van de PersonService klasse wanneer het onderdeel wordt gemaakt. De gedelegeerde maakt PersonService het exemplaar zelf niet, maar maakt alleen gebruik van het exemplaar dat de servicecontainer biedt.

Interfaces en afhankelijkheidsinjectie

Als u afhankelijkheden van een specifieke service-implementatie wilt voorkomen, kunt u in plaats daarvan een service configureren voor een specifieke interface en vervolgens alleen afhankelijk zijn van de interface. Deze aanpak biedt u de flexibiliteit om de service-implementatie uit te wisselen, waardoor de code testbaar en eenvoudiger te onderhouden is.

Overweeg een interface voor de PersonService klasse:

public interface IPersonService
{
    string GetPersonName();
}

Deze interface definieert de ene methode, GetPersonNamedie een string. Met deze PersonService klasse wordt de IPersonService interface geïmplementeerd:

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

In plaats van de PersonService klasse rechtstreeks te registreren, kunt u deze registreren als een implementatie van de IPersonService interface:

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

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

Dit voorbeeld Program.cs verschilt van het vorige voorbeeld op twee manieren:

  • Het PersonService exemplaar wordt geregistreerd als een implementatie van de IPersonService interface (in tegenstelling tot het rechtstreeks registreren van de PersonService klasse).
  • De handtekening voor gedelegeerden verwacht nu een IPersonService parameter in plaats van een PersonService parameter.

Wanneer de app wordt uitgevoerd en een client de hoofd-URL aanvraagt, biedt de servicecontainer een exemplaar van de klasse omdat deze PersonService is geregistreerd als de implementatie van de IPersonService interface.

Aanbeveling

IPersonService Denk aan een contract. Hiermee worden de methoden en eigenschappen gedefinieerd die een implementatie moet hebben. De gedelegeerde wil een instantie van IPersonService. Het maakt helemaal niets uit over de onderliggende implementatie, alleen dat het exemplaar de methoden en eigenschappen bevat die in het contract zijn gedefinieerd.

Testen met afhankelijkheidsinjectie

Het gebruik van interfaces maakt het eenvoudiger om onderdelen geïsoleerd te testen. U kunt een mock-implementatie van de IPersonService interface maken voor testdoeleinden. Wanneer u de mock-implementatie in de test registreert, biedt de servicecontainer de mock-implementatie aan het onderdeel dat wordt getest.

Stel dat in plaats van een vastgelegde tekenreeks een vastgelegde tekenreeks te retourneren, de GetPersonName methode in de PersonService klasse de naam ophaalt uit een database. Als u het onderdeel wilt testen dat afhankelijk is van de IPersonService interface, kunt u een mock-implementatie maken van de IPersonService interface die een vastgelegde tekenreeks retourneert. Het onderdeel dat wordt getest, kent het verschil tussen de echte implementatie en de mock-implementatie niet.

Stel ook dat uw app een API-eindpunt toe wijst dat een begroetingsbericht retourneert. Het eindpunt is afhankelijk van de IPersonService interface om de naam op te halen van de persoon die u wilt begroeten. De code waarmee de IPersonService service wordt geregistreerd en het API-eindpunt wordt toegewezen, kan er als volgt uitzien:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

Dit is vergelijkbaar met het vorige voorbeeld met IPersonService. De gemachtigde verwacht een IPersonService parameter, die door de servicecontainer wordt geleverd. Zoals eerder vermeld, wordt ervan uitgegaan dat de PersonService interface waarmee de interface wordt geïmplementeerd, de naam van de persoon ophaalt om te begroeten vanuit een database.

Overweeg nu de volgende XUnit-test waarmee hetzelfde API-eindpunt wordt getest:

Aanbeveling

Maak u geen zorgen als u niet bekend bent met XUnit of Moq. Het schrijven van eenheidstests valt buiten het bereik van deze module. In dit voorbeeld ziet u hoe afhankelijkheidsinjectie kan worden gebruikt bij het testen.

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

De voorgaande test:

  • Hiermee maakt u een mock-implementatie van de IPersonService interface die een vastgelegde tekenreeks retourneert.
  • Registreert de mock-implementatie met de servicecontainer.
  • Hiermee maakt u een HTTP-client om een aanvraag naar het API-eindpunt te maken.
  • Bevestigt dat het antwoord van het API-eindpunt naar verwachting is.

Het maakt de test niet uit hoe de PersonService klas de naam krijgt van de persoon die moet begroeten. Het geeft alleen aan dat de naam is opgenomen in het begroetingsbericht. De test maakt gebruik van een mock-implementatie van de IPersonService interface om het onderdeel dat wordt getest te isoleren van de echte implementatie van de service.