Как стать автором
Обновить
72.12

Гайд по настройке IoC-контейнера в консольном приложении .NET core

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров4.7K

Статья-гайд от ведущего .NET-разработчика "ITQ Group" Александра Берегового.

Бывает, что нужно написать консольное приложение без использования IHost, но при этом иметь удобства IoC, поддержку конфигурационных файлов и переменных окружающей среды. В этой статье я как раз и расскажу, как с минимальными усилиями сделать такое приложение.

Итак, для начала, создадим новый проект на базе шаблона Console App.

Укажем имя проекта и путь размещения проекта в файловой системе.

На следующем экране выберем фрэймворк. Я буду использовать .Net 6 LTS.

Я отказался от использования Top-level statements, чтобы не скрывать устройство модуля Program.cs.

После завершения мастера создания проекта, в нашем проекте должен находиться только один модуль - Program.cs, как показано на рисунке ниже.

В модуле Program.cs тоже нет ничего необычного:

namespace ConsoleAppDI
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

Первым делом добавим поддержку конфигурационных файлов. Для этого нам нужно подключить Nuget-пакет Microsoft.Extensions.Configuration.

Теперь мы можем использовать пространство имен Microsoft.Extensions.Configuration. Добавим соответствующую директиву using в модуль Program.cs. После этого добавим в нашу программу новый метод, который будет возвращать ссылку на IConfigurationBuilder - CreateConfigurationBuilder():

using Microsoft.Extensions.Configuration;


namespace ConsoleAppDI
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var configuration = CreateConfigurationBuilder(args).Build();
        }


        private static IConfigurationBuilder CreateConfigurationBuilder(string[] args)
        {
            return new ConfigurationBuilder();
        }
    }
}

Теперь добавим в проект конфигурационный файл в формате JSON.

Содержимое конфигурационного файла, сгенерированное Visual Studio, можно удалить, оно нам не понадобится. В свойствах файла appSettings.json нужно включить копирование файла в каталог, в который будет производиться сборка приложения. Для этого в свойствах файла укажите значение Copy if newer, как показано на рисунке ниже.

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

using Microsoft.Extensions.Configuration;
using System.Reflection;
namespace ConsoleAppDI
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var configuration = CreateConfigurationBuilder(args).Build();
        }


        private static IConfigurationBuilder CreateConfigurationBuilder(string[] args)
        {
            return new ConfigurationBuilder().SetBasePath(
                    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
                .AddJsonFile("appSettings.json", false, false);
        }
    }
}

Для поддержки переменных окружения нужно добавить еще один Nuget-пакет - Microsoft.Extensions.Configuration.EnvironmentVariables, и еще один вызов - .AddEnvironmentVariables():

private static IConfigurationBuilder CreateConfigurationBuilder(string[] args)
{
    return new ConfigurationBuilder()
        .SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
        .AddJsonFile("appSettings.json", false, false)
        .AddEnvironmentVariables()
        ;
}

Далее займемся IoC-контейнером. Для этого нам понадобится Nuget-пакет - Microsoft.Extensions.DependencyInjection. Добавим новый метод, который будет создавать и настраивать IoC-контейнер - CreateIocContainer():

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;


namespace ConsoleAppDI
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var configuration = CreateConfigurationBuilder(args).Build();
            var serviceProvider = CreateIocContainer(configuration).BuildServiceProvider();
        }


        private static IServiceCollection CreateIocContainer(IConfigurationRoot configuration)
        {
            var services = new ServiceCollection();


            return services;
        }


        private static IConfigurationBuilder CreateConfigurationBuilder(string[] args)
        {
            return new ConfigurationBuilder()
                .SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
                .AddJsonFile("appSettings.json", false, false)
                .AddEnvironmentVariables();
        }
    }
}

Чтобы не загромождать модуль Program.cs инструкциями по настройке контейнера, ссылками на другие модули приложения и прочим, я рекомендую использовать класс Startup, как это обычно делается в Asp.Net Core приложениях.

Так как я отказался от использования IHost, мне придется реализовать метод расширения .UseStartup<TStartup>(), который за нас уже реализовали для стандартных классов, имплементирующих интерфейс IHost.

Добавим в проект папку Extensions, и затем, добавим в нее класс расширений - ServiceCollectionExtensions.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;


namespace ConsoleAppDI.Extensions
{
    public static class ServiceCollectionExtensions
    {
        private const string ConfigureServicesMethodName = "ConfigureServices";


        public static IServiceCollection UseStartup<TStartup>(this IServiceCollection services, IConfiguration configuration)
            where TStartup : class
        {
            var startupType = typeof(TStartup);
            var cfgServicesMethod = startupType.GetMethod(ConfigureServicesMethodName, new Type[] { typeof(IServiceCollection) });
            var hasConfigCtor = startupType.GetConstructor(new Type[] { typeof(IConfiguration) }) != null;
            var startup = hasConfigCtor
                        ? (TStartup)Activator.CreateInstance(typeof(TStartup), configuration)
                        : (TStartup)Activator.CreateInstance(typeof(TStartup), null);


            cfgServicesMethod?.Invoke(startup, new object[] { services });


            return services;
        }
    }
}

Из приведенного выше кода видно, что в класс добавлен обобщенный метод UseStartup(), обобщенный параметр принимает любой класс. Под капотом метод пытается найти у класса-параметра метод с именем ConfigureServices и выполнить его.

Кроме того, метод анализирует конструктор переданного класса и проверяет, принимает ли конструктор параметр типа IConfiguration. Эта информация используется при инстанцировании класса TStartup. Таким образом, мы сможем передать полученную на предыдущем этапе конфигурацию приложения в экземпляр класса TStartup и использовать ее во время конфигурирования сервисов IoC-контейнера.

Теперь добавим класс Startup. Класс должен содержать метод ConfigureServices принимающий единственный параметр IServiceCollection. Я также добавил конструктор, принимающий IConfiguration.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;


namespace ConsoleAppDI
{
    internal class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }


        public void ConfigureServices(IServiceCollection services)
        {


        }


        public IConfiguration Configuration { get; }
    }
}

Теперь мы можем использовать наш Startup и созданный ранее метод расширения .UseStartup<T>() для настройки IoC-контейнера:

using ConsoleAppDI.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;


namespace ConsoleAppDI
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var configuration = CreateConfigurationBuilder(args).Build();
            var serviceProvider = CreateIocContainer(configuration).BuildServiceProvider();
        }


        private static IServiceCollection CreateIocContainer(IConfigurationRoot configuration)
        {
            var services = new ServiceCollection()
                            .UseStartup<Startup>(configuration)
                            ;


            return services;
        }


        private static IConfigurationBuilder CreateConfigurationBuilder(string[] args)
        {
            return new ConfigurationBuilder()
                .SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
                .AddJsonFile("appSettings.json", false, false)
                .AddEnvironmentVariables();
        }
    }
}

Добавим в проект интерфейс IApplicationRunner с единственным общедоступным методом Run(). Этот интерфейс понадобится для регистрации класса ApplicationRunner, который мы добавим чуть позже, в контейнере IoC.

namespace ConsoleAppDI
{
    internal interface IApplicationRunner
    {
        void Run();
    }
}

Теперь добавим в проект класс ApplicationRunner, который будет реализовывать объявленный выше интерфейс. Этот класс будет содержать логику нашего приложения.

namespace ConsoleAppDI
{
    internal class ApplicationRunner: IApplicationRunner
    {
        public void Run()
        {
            Console.Clear();


            Console.WriteLine($"Hello from {nameof(ApplicationRunner)}");
        }
    }
}

Зарегистрируем класс в методе ConfigureServices() класса Startup:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;


namespace ConsoleAppDI
{
    internal class Startup
    {
        . . .


        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IApplicationRunner, ApplicationRunner>();
        }


        . . .
    }
}

И, наконец, мы можем получить экземпляр нашего класса от сервис-провайдера. Метод Main() нашего консольного приложения будет выглядеть следующим образом:

static void Main(string[] args)
{
    var configuration = CreateConfigurationBuilder(args).Build();
    var serviceProvider = CreateIocContainer(configuration).BuildServiceProvider();
    var runner = serviceProvider.GetRequiredService<IApplicationRunner>();
    runner.Run();
}

Результат выполнения нашего приложения показан на скриншоте ниже:

Добавим чтение конфигурации. Не зря же мы добавляли поддержку конфигурации в приложение? ;)

Разработчики .Net Core здорово потрудились, добавив возможность читать конфигурацию в формате JSON. Теперь добавлять секции и ключи в конфигурационные файлы гораздо проще, чем это было во времена классического .Net и конфигурации в формате XML.

Теперь каждая секция конфигурации во время выполнения приложения может быть представлена POCO-классом, т.е. обычным классом C#.

Добавим класс AppSettings, который будет предоставлять доступ к значениям из конфигурационного файла. Добавим в класс единственное строковое свойство - HelloTemplate.

namespace ConsoleAppDI.Config
{
    internal class AppSettings
    {
        public string HelloTemplate { get; init; }
    }
}

Чтобы зарегистрировать наш класс в Startup, нам понадобится подключить пакет Microsoft.Extensions.Options.ConfigurationExtensions.

Класс AppSettings нужно зарегистрировать в IoC-контейнере следующим образом:

services.Configure<AppSettings>(Configuration.GetSection(nameof(AppSettings)));

Далее, чтобы получить доступ к конфигурации приложения из класса ApplicationRunner, нужно добавить в конструктор класса параметр IOptions<AppSettings>, как показано ниже:

using ConsoleAppDI.Config;
using Microsoft.Extensions.Options;


namespace ConsoleAppDI
{
    internal class ApplicationRunner: IApplicationRunner
    {
        private readonly IOptions<AppSettings> options;


        public ApplicationRunner(IOptions<AppSettings> options)
        {
            this.options = options;
        }


        . . .
    }
}

Используем шаблон приветственного сообщения из конфигурации взамен литерала:

using ConsoleAppDI.Config;
using Microsoft.Extensions.Options;


namespace ConsoleAppDI
{
    internal class ApplicationRunner: IApplicationRunner
    {
        private readonly IOptions<AppSettings> options;


        public ApplicationRunner(IOptions<AppSettings> options)
        {
            this.options = options;
        }


        public void Run()
        {
            var greetingMessage = options.Value.HelloTemplate.Replace("{{app}}", nameof(ApplicationRunner));


            Console.Clear();


            Console.Title = "IoC Console App";
            Console.WriteLine(greetingMessage);
            Console.WriteLine("Press Enter to exit the application");
            Console.ReadLine();
        }
    }
}

Чтобы все заработало, нужно в файл appSettings.json добавить JSON-объект, соответствующий нашему классу AppSettings:

{
  "AppSettings": {
    "HelloTemplate": "Hello from {{app}}!"
  }
}

Заключение

Мы добавили в приложение поддержку конфигурационных файлов, а также выполнили настройку контейнера IoC.

Что нам это даёт? Если посмотреть на код модуля Program.cs, то он остался достаточно лаконичным, метод Main() содержит всего четыре строки кода. Код, выполняющий настройку IoC-контейнера вынесен в отдельный класс Startup.

Ну а далее, мы можем дополнять приложение новыми классами, используя внедрение зависимостей через параметры конструктора, при этом основной модуль Program.cs не будет изменяться.

На этом все.

Исходный код можно скачать по следующей ссылке:
Bitbucket / consoleappdi.example

Теги:
Хабы:
Всего голосов 10: ↑6 и ↓4+2
Комментарии39

Публикации

Информация

Сайт
itq-group.com
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия