Статья-гайд от ведущего .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

Комментарии (38)


  1. MonkAlex
    00.00.0000 00:00
    +5

    А зачем нужен UseStartup с рефлекшном, если это полностью наше приложение и никаких доп требований к этой логике нет? В рамках поставленной задачи это явно не нужно =)

    Ну и ещё мелочь:

    CreateConfigurationBuilder(args).Build();

    CreateIocContainer(configuration).BuildServiceProvider();

    Явно можно унести билды внутрь методов, никому не нужно возвращаемое значение без билда.


    1. AgentFire
      00.00.0000 00:00
      +3

      Я более того скажу - зачем ВООБЩЕ нужен этот архаичный Startup? Он же сильно нарушает очевидность своей же работы, т.к. работает без контракта.

      Нет, понятно, что контракт есть, но не в файле интерфейса, а где-то в дебрях MSDN-ов. Где-то там, в обычных текстах в интернете, находятся нужные сигнатуры методов. Но к чему этот головняк, если весь его функционал полностью доступен и через обычные методы, с очевидными прямо в коде сигнатурами?

      Эти UseStartup<> тянутся из древности уже не один десяток лет, а интерфейс (да хоть бы и через generic constraint) к нему так толком и не прикрутили. При этом его использую очень много людей, практически повсеместно. Зачем?


      1. AAB74
        00.00.0000 00:00

        По поводу того, как подключается Startup я спорить не буду, т.к. я не являюсь разработчиком фрэймворка.
        А вот по поводу размещения инициализации сервисов в модуле program.cs скажу следующее: мне не нравится, когда этот модуль распухает лишь из-за кода, который добавляет в DI необходимые сервисы.


    1. AAB74
      00.00.0000 00:00

      Не очень понял фразу про "полностью наше приложение".
      В консольном приложении, как и в любом другом, может быть достаточно сложное внутреннее устройство, поэтому использование DI может быть оправдано. Именно для таких случаев и приведен данный пример.
      По поводу второго замечания - я полностью согласен, убрал построение объектов в методы.
      Спасибо.


      1. MonkAlex
        00.00.0000 00:00

        К DI нет вопросов. Вопрос в том, зачем нам нами написанный UseStartup , который потом рефлекшном будет вызывать наш же код?

        Почему в статье:

        new ServiceCollection().UseStartup<Startup>(configuration).BuildServiceProvider()

        Вместо например

        var services = new ServiceCollection();
        var startup = new Startup(configuration);
        startup.ConfigureServices(services);
        return services.BuildServiceProvider();

        в любых нужных комбинациях?


      1. mvv-rus
        00.00.0000 00:00

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

        А у вас тут в примере вообще нет DI, а IoC реализован через Service Locator pattern (который теоретикам от программирования нравится сильно меньше, чем DI).
        Вообще, чтобы реализовать DI в программе на C#, нужен фреймворк, который скрытым от разработчика "волшебным" образом будет находить в контейнере сервисов (у вас он называется "контейнер IoC", как я понял) и подставлять значения параметров для конструкторов объектов и/или для методов, которые этот самый разработчик непосредственно пишет. А в самом языке C# конструкций для DI нет.

        У вас в компании есть такой фреймворк для консольных приложений?


  1. centralhardware2
    00.00.0000 00:00

    какие причины использования DI в консольном приложение есть ?


    1. Abbadone
      00.00.0000 00:00
      +1

      Ровно такие же как и в веб проекте. Консольные приложения тоже бывают очень громоздкими и DI помогает сохранить "чистую" архитектуру.

      А еще из личного опыта - это крутой способ заработать очки на тестовом задании :) Я не думал, что это какая-то экзотика, но как оказалось это может удивить)


      1. selkwind
        00.00.0000 00:00
        -1

        крутой способ заработать очки на тестовом задании 

        Частично согласен, хотя по здравому смыслу это будет выглядеть как попытка пригнать карьерный Liebherr чтобы выкопать канаву вдоль участка СНТ.


        1. selkwind
          00.00.0000 00:00

          А в чем я не прав по существу?


  1. tsvettsih
    00.00.0000 00:00

    Похоже в статье путаница в терминах DI и IoC.
    IoC - это принцип, а DI контейнер - инструмент, помогающий реализовать этот принцип (неймспейс Microsoft.Extensions.DependencyInjection называется правильно, неправильно было бы Microsoft.Extensions.IoC).
    Можно ли использовать DI контейнер без IoC? Конечно, DI-контейнеру все равно в каких слоях находятся внедряемые зависимости.
    Можно ли реализовать принцип IoC без DI контейнера? Тоже можно, принцип был описан до того, как DI контейнер стал инструментом по умолчанию (хотя в контейнером конечно удобнее).


    1. lair
      00.00.0000 00:00

      неймспейс Microsoft.Extensions.DependencyInjection называется правильно, неправильно было бы Microsoft.Extensions.IoC

      Он как раз правильно называется, потому что в нем лежит конкретная реализация (контейнер), отвечающая за DI


      1. mvv-rus
        00.00.0000 00:00

        Хорошо. Но почему тогда ключевой для реализации IoC интерфейс IServiceProvider описан не в этом пространстве имен, а прямо в System, а?

        На самом деле, мы все знаем правильный ответ: "так сложилось исторически". В частности, IServiceProvider пришел в IoC во всех ее видах - хоть DI, хоть Service Locator - откуда-то из компонентной модели организации приложений. А эту модель MS начала развивать ещё с самого начала разработки .NET, потому что ноги этой модели растут из OLE/OLE2/COM, которая еще лет на десять старше, чем .NET и которую MS активно пропагандировала еще в 90-е.

        Короче, такими вопросами IMHO лучше себе вообще голову не забивать.


        1. lair
          00.00.0000 00:00

          Но почему тогда ключевой для реализации IoC интерфейс описан не в этом пространстве имен, а прямо в System, а?

          "Так исторически сложилось". Но к вопросу "правильно ли назван Microsoft.Extensions.DependencyInjection" это отношения не имеет.


          1. mvv-rus
            00.00.0000 00:00

            Имеет. В примере из статьи Dependency Injection отсутствует, да? А почему тогда подключаемое пространство имен (и сборка, содержащая соответствующие классы) называtтся DependencyInjection?

            Правильный ответ см. выше.


            1. lair
              00.00.0000 00:00

              В примере из статьи Dependency Injection отсутствует, да?

              Нет.

              public ApplicationRunner(IOptions<AppSettings> options)

              Это как раз dependency injection.


              1. mvv-rus
                00.00.0000 00:00

                Это как раз dependency injection.

                Нет. Точнее - не совсем. Потому что реализация интерфейса, передаваемого как параметр конструктора, ищется перед вызовом метода в контейнере сервисов явным образом, через GetRequiredService:

                var runner = serviceProvider.GetRequiredService<IApplicationRunner>(); runner.Run();

                Это - Service Locator pattern. Вот если бы он сделал программу на основе шаблона WorkerService - там бы такого вызова не было.
                А вот разрешение зависимостей где-то внутри контейнера сервисов таки да, можно считать DI. Но вообще этот спор теоретический.


                1. lair
                  00.00.0000 00:00

                  Нет. Точнее - не совсем.

                  Совсем. С точки зрения этого класса, который ничего, как и полагается, не знает о том, откуда его создали, это DI. У него (класса) есть зависимость, она передана снаружи. Классическое определение DI.

                  Потому что реализация интерфейса, передаваемого как параметр конструктора, ищется перед вызовом метода в контейнере сервисов явным образом, через GetRequiredService:

                  Никакого "явным образом". Как параметр передается IOptions<AppSettings>, вызова GetRequiredService<IOptions<AppSettings>> я не вижу.

                  Это - Service Locator pattern.

                  Если быть точным, это половина паттерна service locator, потому что традиционно так вышло, что сервис локатор доступен глобально. Но это не важно, на самом деле. Даже если бы здесь был полный service locator, то там - все равно dependency injection. Эти две вещи друг другу не противоречает.


                  1. mvv-rus
                    00.00.0000 00:00

                    Я, пока вы отвечали, уже поправил свой комментарий.


                    1. lair
                      00.00.0000 00:00

                      Не вижу, чтобы поменялось что-то, на что я отвечаю.


                      1. mvv-rus
                        00.00.0000 00:00

                        Я увидел, где вы увидели DI. Но Service Locator в примере тоже есть.


                      1. lair
                        00.00.0000 00:00

                        От того, что в примере есть service locator (хотя и это утверждение для меня не однозначно), DI не перестает быть DI. Так что использование Microsoft.Extensions.DependencyInjection выглядит полностью уместным.


                      1. mvv-rus
                        00.00.0000 00:00

                        От того, что данная реализация IServiceProvider умеет использовать DI, возможность использовать этот контейнер для Service Locator никуда не делась. Так что название, не упоминающее про альтернативную возможность реализации IoC не является точным.


                      1. lair
                        00.00.0000 00:00

                        От того, что данная реализация IServiceProvider умеет использовать DI, возможность использовать этот контейнер для Service Locator никуда не делась

                        Возможность - да. Но рекомендована ли эта возможность? Или это название намекает на то, как рекомендуется использовать этот контейнер?


                      1. mvv-rus
                        00.00.0000 00:00
                        -1

                        Намекать - это понятие тонкое. А что до "как рекомендуется", то сразу вспоминаются разные учебники по ASP.NET Core - там, наверное, в каждом первом тексте, где есть глава про конвейер обработчиков запросов (AKA middleware) и самописные(custom) обработчики, есть пример такого обработчика на базе делегата (обычно - стрелочной функции). И там контейнер сервисов, если используется, то используется через Service Locator, потому что DI в самописные обрабочики-делегаты не завезли.

                        Но Dependency Injection - он, таки да, раскручен: "четыреDependency Injection - хорошо, двеService Locator - плохо"


    1. AAB74
      00.00.0000 00:00

      Согласен. Исправлю.
      Здесь правильно было написать про DI-контейнер.


      1. mvv-rus
        00.00.0000 00:00

        Да, наверное - этот термин встречается сильно чаще. Хотя он и не совсем точный, но людям так будет понятнее. Лично я, однако, предпочитаю называть эту сущность контейнером сервисов (но это совсем не общепринято, да).


  1. kurilovigor
    00.00.0000 00:00

    Интересно, если бы вы следующей статьей показали, как перейти от Startup к VerticalSlice


  1. Vanirn
    00.00.0000 00:00

    .AddJsonFile("appSettings.json", false, false)
    Разве appsettings.json не поддягивается "автоматом"?

    Может проще использовать Microsoft.NET.Sdk.Web вместо "консольного" Sdk?


  1. Vasjen
    00.00.0000 00:00

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

    в ASP.NET Core тоже так не делают. Использование отдельного файла считается устаревшим подходом.

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

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

    Services.AddTransient<IConfiguration>(sp =>
            {
                IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
                configurationBuilder.AddJsonFile("appsettings.json")
                                    .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                                    .AddUserSecrets<Program>();
                return configurationBuilder.Build();
            });

    За подробный гайд - спасибо. Пару месяцев назад для меня ему бы цены просто не было.


    1. AAB74
      00.00.0000 00:00

      Имелось ввиду, что можно добавить конфигурационные файлы под нужную среду выполнения, хотя я и не показал, как это сделать, но тем не менее.
      AppSettings - нужен, чтобы использовать механизм IOptions<TOptions> или IOptionsMonitor<TOptions>.
      Повторюсь, что это лишь пример. В реальном приложении конфигурационных секций, равно как и конфигурационных файлов много больше.


  1. vadref
    00.00.0000 00:00

    На всякий случай добавлю, что для использования метода расширения SetBasePath нужно добавить в проект Nuget-пакет Microsoft.Extensions.Configuration.FileExtensions.


    1. AAB74
      00.00.0000 00:00

      Я не добавлял ссылку на этот пакет явным образом.

      Вот список пакетов, на которые ссылается проект:
      Microsoft.Extensions.Configuration
      Microsoft.Extensions.Configuration.EnvironmentVariables
      Microsoft.Extensions.Configuration.Json
      Microsoft.Extensions.DependencyInjection
      Microsoft.Extensions.Options.ConfigurationExtensions


  1. kuda78
    00.00.0000 00:00

    А можно чуть подробнее пояснить, по какой причине потребовалось реализация собственного микро-хоста? Единственный вариант, который я могу предложить, это реализация мелких утилит командной строки, но по причине требуемого runime размер все равно будет выходить негуманным.

    • Предложенный код требует copy paste либо оформление в виде собственной реализации Host подобной конструкции

    • Для консольных приложений отлично подходит такой способ инициализации и использования DI

    _host = Host.CreateDefaultBuilder()
        .ConfigureLogging(cfg => cfg.ClearProviders()...)
        .ConfigureAssetManager(cfg => cfg.AddFileProvider())
        .Build();
    
    _assetManager = _host.Services.GetRequiredService<IAssetsManager>();
    
    ...
    
    public static IHostBuilder ConfigureAssetManager(this IHostBuilder hostBuilder, ...)
    {
    ...
    }
    
    • Есть возможность использовать единые механизмы регистрации и инициализации сервисов как для минималистических консольных приложений так и для обычных сервисов / приложений.

    • Из коробки присутствует LogFactory

    • Из коробки штатная обработка Ctrl+C (в том числе через IHostApplicationLifetime)

    • Из коробки вкусности типа IHostedService, как некоторый аналог IApplicationRunner

    • Готовые подходы интеграции с System.CommandLine


  1. zerg903
    00.00.0000 00:00
    +2

    Под .net 6 и выше эти задачи реализуются весьма просто.

    Краткий пример
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Serilog;
    
    // Setup
    // ---------------------
    
    var configuration = new ConfigurationBuilder()
      .AddJsonFile("appsettings.json", false, false)
      .AddJsonFile("appsettings.Development.json", true, false)
      .Build();
    
    Log.Logger = new LoggerConfiguration()
      .WriteTo.Console()
      .CreateLogger();
    
    var services = new ServiceCollection();
    
    services.AddLogging(l => l.ClearProviders().AddSerilog());
    services.AddSingleton<IConfiguration>(configuration);
    services.AddTransient<WorkerService>();
    
    var provider = services.BuildServiceProvider();
    
    // Runtime
    // ---------------------
    
    await provider.GetRequiredService<WorkerService>()
      .ExecuteAsync();
    
    // Types
    // ---------------------
    
    public class WorkerService
    {
      private readonly IConfiguration _cfg;
      private readonly ILogger<WorkerService> _logger;
    
      public WorkerService(ILogger<WorkerService> logger, IConfiguration cfg)
      {
        _logger = logger;
        _cfg = cfg;
      }
    
      public Task ExecuteAsync()
      {
        _logger.LogInformation("Hi!");
    
        return Task.CompletedTask;
      }
    }
    


    1. mvv-rus
      00.00.0000 00:00

      А еще проще - выкинуть async и await, переименовать ExecuteAsync в Execute и сменить его тип возврата на void ;-)
      Ну, или вообще убрать класс WorkerService а код из этого метода перенести прямо в top-level statements


      1. zerg903
        00.00.0000 00:00

        Ну, если у вас простейшая задача, то только поддерживаю… 

        Но в статье целью была заявлено:

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

        Это обычно требуется в сложных приложениях, где зависимости прописывается в  IoC через extension methods или иным способом (как, например, модули autofac) и проще использовать готовое решение.


        1. mvv-rus
          00.00.0000 00:00

          Вы бы показали, что ли, куда это сложность всталвяться будет. А то я в вашем примере это не увидел.