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

Комментарии 39

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

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

CreateConfigurationBuilder(args).Build();

CreateIocContainer(configuration).BuildServiceProvider();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Нет.

public ApplicationRunner(IOptions<AppSettings> options)

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

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

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

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

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

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

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

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

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

Это - Service Locator pattern.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

я рекомендую использовать класс 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();
        });

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

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

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

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

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

А можно чуть подробнее пояснить, по какой причине потребовалось реализация собственного микро-хоста? Единственный вариант, который я могу предложить, это реализация мелких утилит командной строки, но по причине требуемого 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

Под .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;
  }
}

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

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

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

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий