Внедрение зависимостей в консольном приложении .Net Core


Здравствуйте, коллеги.

Следует признать, что с появлением платформы .NET Core и обновлением ASP.NET Core практика внедрения зависимостей в .NET ничуть не утратила актуальности. Лаконичный кейс об использовании встроенных контейнеров на платформе .NET разобран в статье Эндрю Лока, перевод которой мы сегодня предлагаем вашему вниманию

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

Вам придется выбрать, какой контейнер использовать: встроенный или сторонний, однако, в конечном итоге вопрос сводится к тому, достаточно ли мощным получается создаваемый вами контейнер для конкретного проекта. В небольшом проекте все может быть нормально, но, если требуется регистрация на основе соглашений, инструменты для логирования/отладки, либо возникают более экзотические прикладные случаи, например, используется внедрение свойств — то проект придется обогащать. К счастью, сторонние контейнеры интегрируются довольно легко и тем временем упрощаются сами.

Зачем использовать встроенный контейнер?

Один из неоднократно встречавшихся мне вопросов – можно ли использовать встроенный провайдер в консольном приложении .NET Core? Коротко – нет, по крайней мере, не «из коробки», однако, добавить такой провайдер совершенно легко. Однако, уже другой вопрос – а стоит ли его использовать.

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

Если вы пишете консольное приложение, то, скорее всего, вам не понадобится MVC или другие ASP.NET-специфичные сервисы. В таком случае может быть столь же просто прямо с самого начала использовать StructureMap или AutoFac вместо встроенного провайдера, возможности которого ограничены.

При этом у самых распространенных сервисов, разработанных для использования с ASP.NET Core, будут расширения для регистрации со встроенным контейнером при помощи IServiceCollection, поэтому, если вы используете такие сервисы как логирование или паттерн Options, то наверняка будет проще использовать готовые расширения, подключая поверх них сторонние решения, если таковые потребуются.

Добавляем инъекцию зависимостей в консольное приложение

Если вы решили, что встроенный контейнер вам подходит, то добавить его в приложение не составляет труда – для этого используется пакет Microsoft.Extensions.DependencyInjection. Чтобы показать, как это делается, создам простое приложение с двумя сервисами:

public interface IFooService
{
    void DoThing(int number);
}

public interface IBarService
{
    void DoSomeRealWork();
}

У каждого из этих сервисов будет единственная реализация. BarService зависит от IFooService, а FooService использует ILoggerFactory для логирования некоторой работы:

public class BarService : IBarService
{
    private readonly IFooService _fooService;
    public BarService(IFooService fooService)
    {
        _fooService = fooService;
    }

    public void DoSomeRealWork()
    {
        for (int i = 0; i < 10; i++)
        {
            _fooService.DoThing(i);
        }
    }
}

public class FooService : IFooService
{
    private readonly ILogger<FooService> _logger;
    public FooService(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<FooService>();
    }

    public void DoThing(int number)
    {
        _logger.LogInformation($"Doing the thing {number}");
    }
}

Как вы уже видите, я использую в приложении новую инфраструктуру логирования – поэтому добавлю соответствующий пакет в project.json. Также добавлю пакет DependencyInjection и пакет Microsoft.Extensions.Logging.Console, чтобы можно было просматривать результаты логирования:

{
  "dependencies": {
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.DependencyInjection": "1.0.0"
  }
}

Наконец, чтобы окончательно сложить все вместе, обновлю мой метод static void main. Сейчас мы его подробно разберем.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Program
{
    public static void Main(string[] args)
    {
        // настроим нашу инъекцию зависимостей
        var serviceProvider = new ServiceCollection()
            .AddLogging()
            .AddSingleton<IFooService, FooService>()
            .AddSingleton<IBarService, BarService>()
            .BuildServiceProvider();

        // конфигурируем консольное логирование
        serviceProvider
            .GetService<ILoggerFactory>()
            .AddConsole(LogLevel.Debug);

        var logger = serviceProvider.GetService<ILoggerFactory>()
            .CreateLogger<Program>();
        logger.LogDebug("Starting application");

        // здесь выполняется работа 
        var bar = serviceProvider.GetService<IBarService>();
        bar.DoSomeRealWork();

        logger.LogDebug("All done!");

    }
}

Первым делом мы сконфигурируем контейнер инъекции зависимостей, создав ServiceCollection, добавив наши зависимости и, наконец, собрав IServiceProvider. Этот процесс равнозначен методу ConfigureServices в проекте ASP.NET Core, причем, здесь в фоновом режиме происходит практически то же самое. Как видите, мы используем расширяющий метод IServiceCollection, чтобы добавить в наше приложение сервисы логирования, а затем регистрируем наши собственные сервисы. serviceProvider – это наш контейнер, которым мы можем пользоваться для разрешения сервисов в нашем приложении.
На следующем этапе нам потребуется сконфигурировать инфраструктуру логирования с провайдером, так, чтобы результаты логирования куда-то выводились. Сначала выберем экземпляр ILoggerFactory из нашего новоиспеченного serviceProvider и добавим консольный логгер.

В оставшейся части программы мы видим дальнейшее внедрение зависимостей. Сначала выбираем ILogger<T> из контейнера, а затем — экземпляр IBarService. Согласно нашим регистрациям, IBarService – это экземпляр BarService, в который будет внедрен экземпляр FooService.

Далее мы можем запустить наше приложение и убедиться, как красиво разрешаются все наши зависимости!



Добавление StructureMap к консольному приложению

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

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

Сначала нужно добавить StructureMap к зависимостям project.json:

{
  "dependencies": {
    "StructureMap.Microsoft.DependencyInjection": "1.2.0"
  }
}

Далее обновить метод static void main, чтобы StructureMap использовался для регистрации наших собственных зависимостей:

public static void Main(string[] args)
{
    // добавляем сервисы фреймворка
    var services = new ServiceCollection()
        .AddLogging();

    // добавляем StructureMap
    var container = new Container();
    container.Configure(config =>
    {
        // Регистрируем информацию в контейнере при помощи нужных API StructureMap…
        config.Scan(_ =>
                    {
                        _.AssemblyContainingType(typeof(Program));
                        _.WithDefaultConventions();
                    });
        // Заполняем контейнер информацией из коллекции сервисов
        config.Populate(services);
    });

    var serviceProvider = container.GetInstance<IServiceProvider>();

    // оставшаяся часть метода не изменилась
}

На первый взгляд такая версия метода может показаться сложнее предыдущей – на самом деле, так и есть – но, в то же время, этот метод и гораздо мощнее. В примере со StructureMap не придется явно регистрировать наши сервисы IFooService или IBarService – они автоматически регистрировались по соглашению. Когда приложение начинает разрастаться, подобная регистрация на основе соглашений становится чрезвычайно мощным инструментом, особенно в сочетании с отладочными возможностями и вариантами исправления ошибок, которые перед вами открываются.

В этом примере я показал, как использовать StructureMap с адаптером для работы с методами расширения IServiceCollection, однако, мой вариант, безусловно, не является обязательным. Совершенно допустимо применять StructureMap как единственный источник регистрации, просто требуется вручную регистрировать все сервисы, добавляемые в составе методов расширения AddPLUGIN.

Итоги

В этой статье я рассказал, как целесообразно использовать встроенный контейнер для внедрения зависимостей в приложении .NET Core. Я показал, как добавить к проекту коллекцию ServiceCollection, зарегистрировать и сконфигурировать фреймворк логирования, а также извлечь из него сконфигурированные экземпляры сервисов. Наконец, я продемонстрировал, как можно использовать сторонний контейнер в комбинации со встроенным, чтобы усовершенствовать процедуру регистрации – в частности, реализовать регистрацию по соглашениям.

Весь проект (а также многие другие проекты, о которых я рассказывал у себя в блоге) вы найдете у меня на Github.




К сожалению, не доступен сервер mySQL