Informazioni sull'inserimento delle dipendenze
Le app ASP.NET Core spesso devono accedere agli stessi servizi tra più componenti. Ad esempio, potrebbero essere necessari diversi componenti per accedere a un servizio che recupera i dati da un database. ASP.NET Core usa un contenitore di inserimento delle dipendenze (DI) predefinito per gestire i servizi usati da un'app.
Inserimento delle dipendenze e inversione del controllo (IoC)
Il modello di inserimento delle dipendenze è una forma di inversione del controllo (IoC). Nel modello di inserimento delle dipendenze, un componente riceve le relative dipendenze da origini esterne, anziché crearle da solo. Questo modello separa il codice dalla dipendenza, il che semplifica il test e la manutenzione del codice.
Considerare il seguente file di 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();
E il seguente file di PersonService.cs:
namespace MyApp.Services;
public class PersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
Per comprendere il codice, iniziare con il codice evidenziato app.MapGet. Questo codice esegue il mapping delle richieste HTTP GET per l'URL radice (/) a un delegato che restituisce un messaggio di saluto. La firma del delegato definisce un parametro PersonService denominato personService. Quando l'app viene eseguita e un client richiede l'URL radice, il codice all'interno del delegato dipende dal servizio PersonService per ottenere testo da includere nel messaggio di saluto.
Dove ottiene il servizio PersonService il delegato? Viene fornito in modo implicito dal contenitore del servizio. La riga evidenziata builder.Services.AddSingleton<PersonService>() indica al contenitore del servizio di creare una nuova istanza della classe PersonService all'avvio dell'app e di fornire tale istanza a qualsiasi componente che ne abbia bisogno.
Qualsiasi componente che necessita del servizio PersonService può dichiarare un parametro di tipo PersonService nella firma del delegato. Il contenitore del servizio fornirà automaticamente un'istanza della classe PersonService al momento della creazione del componente. Il delegato non crea l'istanza PersonService stessa, ma usa solo l'istanza fornita dal contenitore del servizio.
Interfacce e inserimento delle dipendenze
Per evitare dipendenze da un'implementazione specifica del servizio, è invece possibile configurare un servizio per un'interfaccia specifica e quindi dipendere solo dall'interfaccia. Questo approccio offre la flessibilità di cambiare l'implementazione del servizio, rendendo il codice più testabile e più facile da mantenere.
Si consideri un'interfaccia per la classe PersonService:
public interface IPersonService
{
string GetPersonName();
}
Questa interfaccia definisce il singolo metodo, GetPersonName, che restituisce un oggetto string. Questa classe PersonService implementa l'interfaccia IPersonService:
internal sealed class PersonService : IPersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
Anziché registrare direttamente la classe PersonService, è possibile registrarla come implementazione dell'interfaccia 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();
Questo esempio Program.cs differisce dall'esempio precedente in due modi:
- L'istanza
PersonServiceviene registrata come implementazione dell'interfacciaIPersonService, invece di registrare direttamente la classePersonService. - La firma del delegato ora si aspetta un parametro
IPersonService, anziché un parametroPersonService.
Quando l'app viene eseguita e un client richiede l'URL radice, il contenitore del servizio fornisce un'istanza della classe PersonService perché è registrata come implementazione dell'interfaccia IPersonService.
Suggerimento
Si pensi a un contratto IPersonService. Definisce i metodi e le proprietà che un'implementazione deve avere. Il delegato vuole un'istanza di IPersonService. Non importa affatto l'implementazione sottostante, ma solo che l'istanza abbia i metodi e le proprietà definite nel contratto.
Test con inserimento delle dipendenze
L'uso delle interfacce semplifica il test dei componenti in isolamento. È possibile creare un'implementazione fittizia dell'interfaccia IPersonService a scopo di test. Quando si registra l'implementazione fittizia nel test, il contenitore del servizio fornisce l'implementazione fittizia al componente sottoposto a test.
Si supponga, ad esempio, che invece di restituire una stringa hardcoded, il metodo GetPersonName nella classe PersonService recupera il nome da un database. Per testare il componente che dipende dall'interfaccia IPersonService, è possibile creare un'implementazione fittizia dell'interfaccia IPersonService che restituisce una stringa hardcoded. Il componente sottoposto a test non conosce la differenza tra l'implementazione reale e quella fittizia.
Si supponga anche che l'app esegua il mapping di un endpoint API che restituisce un messaggio di saluto. L'endpoint dipende dall'interfaccia IPersonService per ottenere il nome della persona da salutare. Il codice che registra il servizio IPersonService ed esegue il mapping dell'endpoint API potrebbe essere simile al seguente:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/", (IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
});
app.Run();
Questo è simile all'esempio precedente con IPersonService. Il delegato prevede un parametro IPersonService, fornito dal contenitore del servizio. Come accennato in precedenza, si supponga PersonService che implementa l'interfaccia recuperi il nome della persona da salutare da un database.
Si consideri ora il test XUnit seguente che testa lo stesso endpoint API:
Suggerimento
Non è un problema se non si ha familiarità con XUnit o Moq. La scrittura di unit test non rientra nell'ambito di questo modulo. Questo esempio è solo per illustrare come usare l'inserimento delle dipendenze nei test.
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);
}
}
Il test precedente:
- Crea un'implementazione fittizia dell'interfaccia
IPersonServiceche restituisce una stringa hardcoded. - Registra l'implementazione fittizia con il contenitore del servizio.
- Crea un client HTTP per effettuare una richiesta all'endpoint API.
- Afferma che la risposta dall'endpoint API è quella prevista.
Al test non interessa come la classe PersonService ottiene il nome della persona da salutare. Conta solo che il nome sia incluso nel messaggio di saluto. Il test usa un'implementazione fittizia dell'interfaccia IPersonService per isolare il componente sottoposto a test dall'implementazione reale del servizio.