Общие сведения о внедрении зависимостей

Завершено

ASP.NET приложения Core часто нуждаются в доступе к тем же службам в нескольких компонентах. Например, некоторым компонентам может потребоваться доступ к службе, которая извлекает данные из базы данных. ASP.NET Core использует встроенный контейнер внедрения зависимостей (DI) для управления службами, которые использует приложение.

Внедрение зависимостей и инверсия элемента управления (IoC)

Шаблон внедрения зависимостей — это форма инверсии элемента управления (IoC). В шаблоне внедрения зависимостей компонент получает свои зависимости из внешних источников, а не сам по себе. Этот шаблон отделяет код от зависимости, что упрощает тестирование и обслуживание кода.

Рассмотрим следующий файл 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();

И следующий файл PersonService.cs :

namespace MyApp.Services;

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

Чтобы понять код, начните с выделенного app.MapGet кода. Этот код сопоставляет HTTP-запросы GET для корневого URL-адреса (/) делегату, который возвращает приветственное сообщение. Подпись делегата определяет PersonService параметр с именем personService. Когда приложение запускается и клиент запрашивает корневой URL-адрес, код внутри делегата службы, чтобы получить текст, который будет включен в приветственное сообщение.

Где делегат получает PersonService службу? Он неявно предоставляется контейнером службы. Выделенная builder.Services.AddSingleton<PersonService>() строка сообщает контейнеру службы создать новый экземпляр PersonService класса при запуске приложения и предоставить этот экземпляр любому компоненту, которому он нужен.

Любой компонент, которому требуется PersonService служба, может объявить параметр типа PersonService в своей подписи делегата. Контейнер службы автоматически предоставит экземпляр PersonService класса при создании компонента. Делегат не создает PersonService сам экземпляр, он просто использует экземпляр, который предоставляет контейнер службы.

Внедрение интерфейсов и зависимостей

Чтобы избежать зависимостей от конкретной реализации службы, можно настроить службу для определенного интерфейса, а затем просто зависеть от интерфейса. Этот подход обеспечивает гибкость переключения реализации службы, что делает код более тестируемым и удобным для обслуживания.

Рассмотрим интерфейс для PersonService класса:

public interface IPersonService
{
    string GetPersonName();
}

Этот интерфейс определяет один метод, GetPersonNameкоторый возвращает string. Этот PersonService класс реализует IPersonService интерфейс:

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

Вместо того чтобы напрямую зарегистрировать PersonService класс, его можно зарегистрировать как реализацию 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();

Этот пример Program.cs отличается от предыдущего примера двумя способами:

  • Экземпляр PersonService регистрируется в качестве реализацииIPersonService интерфейса (в отличие от регистрации PersonService класса напрямую).
  • Теперь подпись делегата IPersonService ожидает параметр вместо PersonService параметра.

Когда приложение запускается и клиент запрашивает корневой URL-адрес, контейнер службы предоставляет экземпляр PersonService класса, так как он зарегистрирован в качестве реализации IPersonService интерфейса.

Совет

Подумайте о IPersonService контракте. Он определяет методы и свойства, которые должна иметь реализация. Делегат хочет экземпляра IPersonService. Он не заботится вообще о базовой реализации, только о том, что экземпляр имеет методы и свойства, определенные в контракте.

Тестирование с внедрением зависимостей

Использование интерфейсов упрощает тестирование компонентов в изоляции. Вы можете создать макет реализации IPersonService интерфейса для тестирования. При регистрации реализации макета в тесте контейнер службы предоставляет реализацию макета для проверяемого компонента.

Например, предположим, что вместо возврата жестко закодированных строк GetPersonName метод в PersonService классе извлекает имя из базы данных. Чтобы протестировать компонент, зависящий от IPersonService интерфейса, можно создать макет реализации IPersonService интерфейса, возвращающего жестко закодированную строку. Тестируемый компонент не знает разницу между реальной реализацией и макетной реализацией.

Кроме того, предположим, что приложение сопоставляет конечную точку API, которая возвращает приветственное сообщение. Конечная точка зависит от IPersonService интерфейса, чтобы получить имя приветствия пользователя. Код, регистрирующий службу и сопоставляющий IPersonService конечную точку API, может выглядеть следующим образом:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

Этот пример аналогичен предыдущему примеру IPersonService. Делегат ожидает IPersonService параметр, который предоставляет контейнер службы. Как упоминалось ранее, предположим, что интерфейс PersonService реализует имя пользователя для приветствия из базы данных.

Теперь рассмотрим следующий тест XUnit, который проверяет ту же конечную точку API:

Совет

Не беспокойтесь, если вы не знакомы с XUnit или Moq. Написание модульных тестов выходит за рамки этого модуля. В этом примере просто показано, как внедрение зависимостей можно использовать в тестировании.

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

Предыдущий тест:

  • Создает макет реализации IPersonService интерфейса, который возвращает жестко закодированную строку.
  • Регистрирует реализацию макета в контейнере службы.
  • Создает HTTP-клиент для выполнения запроса к конечной точке API.
  • Утверждает, что ответ конечной точки API соответствует ожидаемому.

Тест не заботится о том, как PersonService класс получает имя пользователя для приветствия. Он заботится только о том, что имя включено в приветственное сообщение. Тест использует макет реализации IPersonService интерфейса, чтобы изолировать компонент, тестируемый из реальной реализации службы.