ASP.NET Core Web API. Dependency Injection

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Dependency Injection или внедрение зависимостей — это встроенная часть платформы .NET, которая позволяет сделать взаимодействующие в приложении объекты слабосвязанными. Обычно, такие объекты связываются через абстракции (чаще всего — интерфейсы). Dependency Injection является одним из основных механизмов расширения возможностей вашего приложения ASP.NET Core.

Зависимости и сервисы

В основе механизма Dependency Injection (или сокращенно — DI) лежит понятие «Зависимость» — объект, от которого зависит другой объект. Например, в какой-то момент времени, нам потребуется использовать в своем приложении (не важно каком — Web API или любом другом) систему логирования. Не зная ничего про механизм DI мы могли создать, например, вот такой класс:

public class SimpleLogger
{
    public void LogInformation(string text)
    {
        Console.WriteLine(text);
    }
}

В этом классе можно определить различные методы, например, выводящие цветной текст в консоли, использующие различные параметры и т.д. Теперь, чтобы использовать этот класс в своем приложении мы должны где-то создать объект и вызвать метод LogInformation(). Например, если мы говорим про приложение Web API, то это может быть контроллер:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    SimpleLogger SimpleLogger { get; set; }
    
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastController()
    {
        SimpleLogger = new(); //создали объект
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        SimpleLogger.LodInformation("Пример работы Simple Logger"); //вызвали метод
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Такой код будет работать и выводить текстовую информацию в консоль:Здесь объект SimpleLogger — зависимость от которой зависит другой классWeatherForecastController. При этом, в представленном примере прослеживается ряд недостатков, которые следует избегать, а именно:

  • если нам потребуется заменить реализацию SimpleLogger на другую, то придётся изменять код зависимого класса (WeatherForecastController).
  • в примере SimpleLogger — довольно простой класс, однако, в рабочем проекте этот класс может также содержать зависимости, которые необходимо каким-либо образом конфигурировать. Например, мы можем захотеть выводить лог не только в консоль, но и в текстовый файл или вообще отправлять лог по Сети. В результате, чтобы настроить логирование в приложении может потребоваться изменять код в нескольких модулях проекта, что может значительно усложнить поддержку такого проекта.
  • также возникают проблемы и с модульным тестированием такого проекта.

Чтобы избавиться от этих проблем мы должны каким-либо образом «отвязать» реализацию SimpleLogger от WeatherForecastController. Наш класс контроллера не должен ничего знать о том, как реализована зависимость, но, при этом, знать какие методы и свойства можно использовать из этой зависимости. Для реализации такого подхода мы можем использовать интерфейсы. Перепишем наш пример SimpleLogger следующим образом:

public interface ISimpleLogger
{
    public void LodInformation(string text);
}


public class SimpleLogger: ISimpleLogger
{
    public void LodInformation(string text)
    {
        Console.WriteLine(text);
    }
}

здесь мы создали интерфейс ISimpleLogger, который реализуется классом SimpleLogger. Теперь мы можем заменить конкретную реализацию логгера в классе WeatherForecastController на интерфейс:

ISimpleLogger SimpleLogger { get; set; }

при этом остается вопрос — как создать конкретный объект логгера, чтобы вывести сообщение в консоль? На данный момент, если вы повторяете код в точности, как в примере, то должны увидеть в Visual Studio следующую ошибку в конструкторе контроллера:

Серьезность Код Описание Проект Файл Строка Состояние подавления Ошибка CS0144 Не удается создать экземпляр абстрактного типа или интерфейса «ISimpleLogger»

изменить код конструктора вот так:

public WeatherForecastController()
{
     SimpleLogger = new SimpleLogger();
}

не вариант так как теряется смысл создания интерфейса — мы всё равно жестко связываем класс контроллера с классом логгера. Чтобы воспользоваться нашим логгером в приложении, мы должны, в общем случае. выполнить два действия:

  1. каким-либо образом указать нашему приложению, что мы будем использовать зависимость типа ISimpleLogger, а реализацией этой зависимости будет класс SimpleLogger
  2. запросить необходимую зависимость в том месте, где эта зависимость требуется, например, в классе контроллера.

Для того, чтобы использовать различные зависимости, в ASP.NET Core используется контейнер DI, представленный интерфейсом IServiceProvider, а сами зависимости называются также сервисами или службами. Чтобы зарегистрировать класс как зависимость в приложении ASP.NET Core мы должны воспользоваться методами свойства Services класса ApplicationBuilder. Зарегистрируем наш сервис логгирования. Откроем файл Program.cs и добавим в него следующую строку кода (с комментарием):

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<ISimpleLogger, SimpleLogger>(); //зарегистрировали новый сервис

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

В данном случае, мы воспользовались одним из методов регистрации сервисов в приложении ASP.NET Core Web API — AddSingleton(). При этом, мы указали тип сервиса — ISimpleLogger и класс, реализующий этот сервис — SimpleLogger. Теперь, все классы приложения, которые будут использовать ISimpleLogger фактически будут вызывать методы, реализованные в классе SimpleLogger.  При этом, стоит обратить внимание на то, что сервисы регистрируются перед вызовом метода Build() класса WebApplicationBuilder.

Чтобы запросить зависимость в классе контроллера также существует несколько возможных подходов, однако сегодня мы рассмотрим только один из них (он же — рекомендуемый разработчиками .NET) — запрос зависимости через конструктор класса. Для этого изменим конструктор класса WeatherForecastController следующим образом:

public WeatherForecastController(ISimpleLogger logger)
{
    SimpleLogger = logger;
}

в параметрах конструктора мы указали тип сервиса, который необходим контроллеру для работы. На этом наша работа по запросу зависимостей закончена — механизм DI сам создаст необходимы объект logger и нам остается только его использовать в приложении. Теперь можно запустить приложение и убедиться, что текстовая строка также выводится в консоль. При этом, мы можем как угодно менять внутренности реализации метода LogInformation(), или создать новый класс, например, SimpleFileLogger, который будет содержать свою собственную реализацию метода LogInformation() и назначить этот класс реализацией сервиса. При этом класс контроллера можно будет не изменять.

Так, мы реализовали в нашем приложении Web API простейший сервис логирования. Сервисы в ASP.NET Core выполняют ключевую роль, наряду с компонентами middleware. С помощью сервисов мы можем расширять функциональность нашего приложения. Например, мы можем создавать сервисы для работы с базами данных, аутентификации и авторизации пользователей, логирования и т.д. При этом, не обязательно разрабатывать эти сервисы самостоятельно — система ASP.NET Core предоставляет нам массу готовых сервисов, покрывающих достаточно большое количество потребностей.

Штатные сервисы ASP.NET Core

Даже «пустое» приложение ASP.NET Core Web API использует штатные сервисы. Для примера, посмотрим какие сервисы регистрируются в приложении Web API. Изменим код файла Program.cs следующим образом:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var services = builder.Services.OrderBy(o=>o.Lifetime);
int i = 1;
foreach (var service in services)
{
    Console.WriteLine($"{i} \t {service.Lifetime} \t {service.ServiceType.Name}");
    i++;
}

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Теперь в консоль будут выводиться все сервисы, зарегистрированные в контейнере DI.

Всего, в моем «пустом» приложении ASP.NET Core Web API на основе контроллеров в контейнере DI было зарегистрировано 219 штатных сервисов. Если же, использовать метод CreateSlimBuilder() для создания приложения, то количество сервисов уменьшится до 204, что тоже немало. Штатные сервисы ASP.NET Core включают в себя сервисы логирования, настройки приложения, сервисы сбора метрик, авторизации и т.д. В контейнере DI все сервисы представлены объектами типа ServiceDescriptor, который предоставляет нам следующую информацию о сервисе:

ImplementationFactory Возвращает фабрику, используемую для создания экземпляров сервиса.
ImplementationInstance Возвращает экземпляр, реализующий сервис.
ImplementationType Возвращает объект Type , реализующий сервис.
Lifetime Возвращает объект ServiceLifetime сервиса — жизненный цикл сервиса.
ServiceType Возвращает объект Type сервиса.

Для использования штатных сервисов ASP.NET Core, обычно используются методы расширения для IServiceCollection, которые начинаются с Add. Такие методы расширения могут регистрировать в контейнере DI как один, так и несколько взаимосвязанных сервисов приложения.

Итого

Механизм Dependency Injection позволяет сделать взаимодействующие в приложении объекты слабосвязанными. Обычно, в качестве типа сервиса выступает интерфейс, который реализуется классами — реализациями сервиса (или по другому — зависимости). Для того, чтобы использовать в приложении ASP.NET Core  сервис необходимо его зарегистрировать в контейнере DI и запросить в классе, который эту зависимость использует.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
guest
0 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии