Förstå beroendeinmatning
ASP.NET Core-appar behöver ofta komma åt samma tjänster i flera komponenter. Flera komponenter kan till exempel behöva komma åt en tjänst som hämtar data från en databas. ASP.NET Core använder en inbyggd di-container (dependency injection) för att hantera de tjänster som en app använder.
Beroendeinmatning och inversion av kontroll (IoC)
Beroendeinmatningsmönstret är en form av Inversion av Kontroll (IoC). I beroendeinmatningsmönstret tar en komponent emot sina beroenden från externa källor i stället för att skapa dem själva. Det här mönstret frikopplar koden från beroendet, vilket gör koden enklare att testa och underhålla.
Överväg följande Program.cs fil:
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();
Och följande PersonService.cs fil:
namespace MyApp.Services;
public class PersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
Börja med den markerade app.MapGet koden för att förstå koden. Den här koden mappar HTTP GET-begäranden för rot-URL:en (/) till ett ombud som returnerar ett hälsningsmeddelande. Ombudets signatur definierar en PersonService parameter med namnet personService. När appen körs och en klient begär rot-URL:en beror koden i delegeringen på PersonService tjänsten för att få någon text att inkludera i hälsningsmeddelandet.
Var hämtar ombudet PersonService tjänsten? Det tillhandahålls implicit av tjänstcontainern. Den markerade builder.Services.AddSingleton<PersonService>() raden instruerar tjänstcontainern att skapa en ny instans av PersonService klassen när appen startar och att tillhandahålla den instansen till alla komponenter som behöver den.
Alla komponenter som behöver PersonService tjänsten kan deklarera en parameter av typen PersonService i sin ombudssignatur. Tjänstcontainern tillhandahåller automatiskt en instans av PersonService klassen när komponenten skapas. Ombudet skapar inte själva instansen PersonService , utan använder bara den instans som tjänstcontainern tillhandahåller.
Gränssnitt och beroendeinmatning
För att undvika beroenden för en specifik tjänstimplementering kan du i stället konfigurera en tjänst för ett specifikt gränssnitt och sedan bara vara beroende av gränssnittet. Den här metoden ger dig flexibiliteten att byta ut tjänstimplementeringen, vilket gör koden mer testbar och enklare att underhålla.
Överväg ett gränssnitt för PersonService klassen:
public interface IPersonService
{
string GetPersonName();
}
Det här gränssnittet definierar den enda metoden, GetPersonName, som returnerar en string. Den här PersonService klassen implementerar IPersonService gränssnittet:
internal sealed class PersonService : IPersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
I stället för att PersonService registrera klassen direkt kan du registrera den som en implementering av IPersonService gränssnittet:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/",
(IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
}
);
app.Run();
Det här exemplet Program.cs skiljer sig från föregående exempel på två sätt:
- Instansen
PersonServiceregistreras som en implementering avIPersonServicegränssnittet (i stället för att registreraPersonServiceklassen direkt). - Ombudssignaturen förväntar sig nu en
IPersonServiceparameter i stället för enPersonServiceparameter.
När appen körs och en klient begär rot-URL:en tillhandahåller tjänstcontainern en instans av PersonService klassen eftersom den är registrerad som implementering av IPersonService gränssnittet.
Dricks
Tänk på IPersonService som ett kontrakt. Den definierar de metoder och egenskaper som en implementering måste ha. Ombudet vill ha en instans av IPersonService. Det bryr sig inte alls om den underliggande implementeringen, bara att instansen har de metoder och egenskaper som definierats i kontraktet.
Testa med beroendeinmatning
Med hjälp av gränssnitt blir det enklare att testa komponenter isolerat. Du kan skapa en simulerad implementering av IPersonService gränssnittet i testsyfte. När du registrerar den falska implementeringen i testet tillhandahåller tjänstcontainern den falska implementeringen till komponenten som testas.
Anta till exempel att i stället för att returnera en hårdkodad sträng GetPersonName hämtar metoden i PersonService klassen namnet från en databas. Om du vill testa komponenten som är IPersonService beroende av gränssnittet kan du skapa en simulerad implementering av IPersonService gränssnittet som returnerar en hårdkodad sträng. Komponenten som testas vet inte skillnaden mellan den verkliga implementeringen och den falska implementeringen.
Anta också att din app mappar en API-slutpunkt som returnerar ett hälsningsmeddelande. Slutpunkten är IPersonService beroende av gränssnittet för att få namnet på den person som ska hälsas. Koden som registrerar IPersonService tjänsten och mappar API-slutpunkten kan se ut så här:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/", (IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
});
app.Run();
Detta liknar föregående exempel med IPersonService. Ombudet förväntar sig en IPersonService parameter som tjänstcontainern tillhandahåller. Anta som tidigare att PersonService det som implementerar gränssnittet hämtar namnet på personen som ska hälsas från en databas.
Överväg nu följande XUnit-test som testar samma API-slutpunkt:
Dricks
Oroa dig inte om du inte är bekant med XUnit eller Moq. Att skriva enhetstester ligger utanför omfånget för den här modulen. Det här exemplet är bara för att illustrera hur beroendeinmatning kan användas vid testning.
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);
}
}
Föregående test:
- Skapar en simulerad implementering av
IPersonServicegränssnittet som returnerar en hårdkodad sträng. - Registrerar den falska implementeringen med tjänstcontainern.
- Skapar en HTTP-klient för att göra en begäran till API-slutpunkten.
- Hävdar att svaret från API-slutpunkten är som förväntat.
Testet bryr sig inte om hur PersonService klassen får namnet på personen att hälsa på. Det bryr sig bara om att namnet ingår i hälsningsmeddelandet. Testet använder en falsk implementering av IPersonService gränssnittet för att isolera komponenten som testas från den verkliga implementeringen av tjänsten.