Статья-гайд от ведущего .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)
centralhardware2
00.00.0000 00:00какие причины использования DI в консольном приложение есть ?
Abbadone
00.00.0000 00:00+1Ровно такие же как и в веб проекте. Консольные приложения тоже бывают очень громоздкими и DI помогает сохранить "чистую" архитектуру.
А еще из личного опыта - это крутой способ заработать очки на тестовом задании :) Я не думал, что это какая-то экзотика, но как оказалось это может удивить)
tsvettsih
00.00.0000 00:00Похоже в статье путаница в терминах DI и IoC.
IoC - это принцип, а DI контейнер - инструмент, помогающий реализовать этот принцип (неймспейс Microsoft.Extensions.DependencyInjection называется правильно, неправильно было бы Microsoft.Extensions.IoC).
Можно ли использовать DI контейнер без IoC? Конечно, DI-контейнеру все равно в каких слоях находятся внедряемые зависимости.
Можно ли реализовать принцип IoC без DI контейнера? Тоже можно, принцип был описан до того, как DI контейнер стал инструментом по умолчанию (хотя в контейнером конечно удобнее).lair
00.00.0000 00:00неймспейс Microsoft.Extensions.DependencyInjection называется правильно, неправильно было бы Microsoft.Extensions.IoC
Он как раз правильно называется, потому что в нем лежит конкретная реализация (контейнер), отвечающая за DI
mvv-rus
00.00.0000 00:00Хорошо. Но почему тогда ключевой для реализации IoC интерфейс IServiceProvider описан не в этом пространстве имен, а прямо в System, а?
На самом деле, мы все знаем правильный ответ: "так сложилось исторически". В частности, IServiceProvider пришел в IoC во всех ее видах - хоть DI, хоть Service Locator - откуда-то из компонентной модели организации приложений. А эту модель MS начала развивать ещё с самого начала разработки .NET, потому что ноги этой модели растут из OLE/OLE2/COM, которая еще лет на десять старше, чем .NET и которую MS активно пропагандировала еще в 90-е.
Короче, такими вопросами IMHO лучше себе вообще голову не забивать.
lair
00.00.0000 00:00Но почему тогда ключевой для реализации IoC интерфейс описан не в этом пространстве имен, а прямо в System, а?
"Так исторически сложилось". Но к вопросу "правильно ли назван
Microsoft.Extensions.DependencyInjection
" это отношения не имеет.mvv-rus
00.00.0000 00:00Имеет. В примере из статьи Dependency Injection отсутствует, да? А почему тогда подключаемое пространство имен (и сборка, содержащая соответствующие классы) называtтся DependencyInjection?
Правильный ответ см. выше.
lair
00.00.0000 00:00В примере из статьи Dependency Injection отсутствует, да?
Нет.
public ApplicationRunner(IOptions<AppSettings> options)
Это как раз dependency injection.
mvv-rus
00.00.0000 00:00Это как раз dependency injection.
Нет. Точнее - не совсем. Потому что реализация интерфейса, передаваемого как параметр конструктора, ищется перед вызовом метода в контейнере сервисов явным образом, через GetRequiredService:
var runner = serviceProvider.GetRequiredService<IApplicationRunner>(); runner.Run();
Это - Service Locator pattern. Вот если бы он сделал программу на основе шаблона WorkerService - там бы такого вызова не было.
А вот разрешение зависимостей где-то внутри контейнера сервисов таки да, можно считать DI. Но вообще этот спор теоретический.lair
00.00.0000 00:00Нет. Точнее - не совсем.
Совсем. С точки зрения этого класса, который ничего, как и полагается, не знает о том, откуда его создали, это DI. У него (класса) есть зависимость, она передана снаружи. Классическое определение DI.
Потому что реализация интерфейса, передаваемого как параметр конструктора, ищется перед вызовом метода в контейнере сервисов явным образом, через GetRequiredService:
Никакого "явным образом". Как параметр передается
IOptions<AppSettings>
, вызоваGetRequiredService<IOptions<AppSettings>>
я не вижу.Это - Service Locator pattern.
Если быть точным, это половина паттерна service locator, потому что традиционно так вышло, что сервис локатор доступен глобально. Но это не важно, на самом деле. Даже если бы здесь был полный service locator, то там - все равно dependency injection. Эти две вещи друг другу не противоречает.
mvv-rus
00.00.0000 00:00Я, пока вы отвечали, уже поправил свой комментарий.
lair
00.00.0000 00:00Не вижу, чтобы поменялось что-то, на что я отвечаю.
lair
00.00.0000 00:00От того, что в примере есть service locator (хотя и это утверждение для меня не однозначно), DI не перестает быть DI. Так что использование
Microsoft.Extensions.DependencyInjection
выглядит полностью уместным.
mvv-rus
00.00.0000 00:00От того, что данная реализация IServiceProvider умеет использовать DI, возможность использовать этот контейнер для Service Locator никуда не делась. Так что название, не упоминающее про альтернативную возможность реализации IoC не является точным.
lair
00.00.0000 00:00От того, что данная реализация IServiceProvider умеет использовать DI, возможность использовать этот контейнер для Service Locator никуда не делась
Возможность - да. Но рекомендована ли эта возможность? Или это название намекает на то, как рекомендуется использовать этот контейнер?
mvv-rus
00.00.0000 00:00-1Намекать - это понятие тонкое. А что до "как рекомендуется", то сразу вспоминаются разные учебники по ASP.NET Core - там, наверное, в каждом первом тексте, где есть глава про конвейер обработчиков запросов (AKA middleware) и самописные(custom) обработчики, есть пример такого обработчика на базе делегата (обычно - стрелочной функции). И там контейнер сервисов, если используется, то используется через Service Locator, потому что DI в самописные обрабочики-делегаты не завезли.
Но Dependency Injection - он, таки да, раскручен: "
четыреDependency Injection - хорошо,двеService Locator - плохо"
AAB74
00.00.0000 00:00Согласен. Исправлю.
Здесь правильно было написать про DI-контейнер.mvv-rus
00.00.0000 00:00Да, наверное - этот термин встречается сильно чаще. Хотя он и не совсем точный, но людям так будет понятнее. Лично я, однако, предпочитаю называть эту сущность контейнером сервисов (но это совсем не общепринято, да).
kurilovigor
00.00.0000 00:00Интересно, если бы вы следующей статьей показали, как перейти от Startup к VerticalSlice
Vanirn
00.00.0000 00:00.AddJsonFile("appSettings.json", false, false)
Разве appsettings.json не поддягивается "автоматом"?
Может проще использовать Microsoft.NET.Sdk.Web вместо "консольного" Sdk?
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(); });
За подробный гайд - спасибо. Пару месяцев назад для меня ему бы цены просто не было.
AAB74
00.00.0000 00:00Имелось ввиду, что можно добавить конфигурационные файлы под нужную среду выполнения, хотя я и не показал, как это сделать, но тем не менее.
AppSettings - нужен, чтобы использовать механизм IOptions<TOptions> или IOptionsMonitor<TOptions>.
Повторюсь, что это лишь пример. В реальном приложении конфигурационных секций, равно как и конфигурационных файлов много больше.
vadref
00.00.0000 00:00На всякий случай добавлю, что для использования метода расширения
SetBasePath
нужно добавить в проект Nuget-пакетMicrosoft.Extensions.Configuration.FileExtensions
.AAB74
00.00.0000 00:00Я не добавлял ссылку на этот пакет явным образом.
Вот список пакетов, на которые ссылается проект:
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.EnvironmentVariables
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Options.ConfigurationExtensions
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
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; } }
mvv-rus
00.00.0000 00:00А еще проще - выкинуть async и await, переименовать ExecuteAsync в Execute и сменить его тип возврата на void ;-)
Ну, или вообще убрать класс WorkerService а код из этого метода перенести прямо в top-level statementszerg903
00.00.0000 00:00Ну, если у вас простейшая задача, то только поддерживаю…
Но в статье целью была заявлено:
... написать консольное приложение без использования IHost, но при этом иметь удобства IoC, поддержку конфигурационных файлов и переменных окружающей среды ...
Это обычно требуется в сложных приложениях, где зависимости прописывается в IoC через extension methods или иным способом (как, например, модули autofac) и проще использовать готовое решение.
mvv-rus
00.00.0000 00:00Вы бы показали, что ли, куда это сложность всталвяться будет. А то я в вашем примере это не увидел.
MonkAlex
А зачем нужен
UseStartup
с рефлекшном, если это полностью наше приложение и никаких доп требований к этой логике нет? В рамках поставленной задачи это явно не нужно =)Ну и ещё мелочь:
CreateConfigurationBuilder(args).Build();
CreateIocContainer(configuration).BuildServiceProvider();
Явно можно унести билды внутрь методов, никому не нужно возвращаемое значение без билда.
AgentFire
Я более того скажу - зачем ВООБЩЕ нужен этот архаичный Startup? Он же сильно нарушает очевидность своей же работы, т.к. работает без контракта.
Нет, понятно, что контракт есть, но не в файле интерфейса, а где-то в дебрях MSDN-ов. Где-то там, в обычных текстах в интернете, находятся нужные сигнатуры методов. Но к чему этот головняк, если весь его функционал полностью доступен и через обычные методы, с очевидными прямо в коде сигнатурами?
Эти
UseStartup<>
тянутся из древности уже не один десяток лет, а интерфейс (да хоть бы и через generic constraint) к нему так толком и не прикрутили. При этом его использую очень много людей, практически повсеместно. Зачем?AAB74
По поводу того, как подключается Startup я спорить не буду, т.к. я не являюсь разработчиком фрэймворка.
А вот по поводу размещения инициализации сервисов в модуле program.cs скажу следующее: мне не нравится, когда этот модуль распухает лишь из-за кода, который добавляет в DI необходимые сервисы.
AAB74
Не очень понял фразу про "полностью наше приложение".
В консольном приложении, как и в любом другом, может быть достаточно сложное внутреннее устройство, поэтому использование DI может быть оправдано. Именно для таких случаев и приведен данный пример.
По поводу второго замечания - я полностью согласен, убрал построение объектов в методы.
Спасибо.
MonkAlex
К DI нет вопросов. Вопрос в том, зачем нам нами написанный
UseStartup
, который потом рефлекшном будет вызывать наш же код?Почему в статье:
new ServiceCollection().UseStartup<Startup>(configuration).BuildServiceProvider()
Вместо например
в любых нужных комбинациях?
mvv-rus
А у вас тут в примере вообще нет DI, а IoC реализован через Service Locator pattern (который теоретикам от программирования нравится сильно меньше, чем DI).
Вообще, чтобы реализовать DI в программе на C#, нужен фреймворк, который скрытым от разработчика "волшебным" образом будет находить в контейнере сервисов (у вас он называется "контейнер IoC", как я понял) и подставлять значения параметров для конструкторов объектов и/или для методов, которые этот самый разработчик непосредственно пишет. А в самом языке C# конструкций для DI нет.
У вас в компании есть такой фреймворк для консольных приложений?