Здравствуйте, коллеги.
Следует признать, что с появлением платформы .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
.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
.ServiceCollection
, зарегистрировать и сконфигурировать фреймворк логирования, а также извлечь из него сконфигурированные экземпляры сервисов. Наконец, я продемонстрировал, как можно использовать сторонний контейнер в комбинации со встроенным, чтобы усовершенствовать процедуру регистрации – в частности, реализовать регистрацию по соглашениям.К сожалению, не доступен сервер mySQL