Много раз я спрашивал себя, что какой IoC контейнер подойдет для того или иного проекта. Их производительность — это только одна сторона медали. Полное сравнение производительности можно найти здесь. Другая сторона медали — простота и скорость обучения. Так что я решил сравнить несколько контейнеров с этой точки зрения и взял Autofac, Simple Injector, StructureMap, Ninject, Unity, Castle Windsor. На мой взгляд, это наиболее популярные IoC контейнеры. Вы можете найти некоторые из них в списке 20 лучших пакетов NuGet и также я добавил другие по своим предпочтениям. Лично мне очень нравится Autofac и во время работы над этой статьей я еще больше утвердился, что это лучший выбор в большинстве случаев.
Здесь описываются основы IoC контейнеров, таких как конфигурация и регистрации компонентов. Есть мысль так же провести сравнение управления lifetime scope и продвинутых фитч. Примеры кода можно найти в репозитории LifetimeScopesExamples GitHub.
Документация
Во время работы над статьей мне необходимо было обращаться к документации некоторых из IoC. К сожалению, не каждый IoC контейнер имеет хорошее описание и я был вынужден искать решение в Google. Таким образом получилось следующее резюме.
Качество | Комментарий | |
---|---|---|
Autofac | Супер | Документация содержит всё, что необходимо. Дополнительно гуглить ничего не пришлось. Примеры понятные и полезные. |
Simple Injector | Хорошо | Документация похожа на предыдущий, но выглядит чуть сырее. Несколько моментов пришлось погуглить, но решение быстро нашлось. |
Structure Map | Средне | Не все случаи описаны в документации. Описания таких вещей, как регистрация с expression, property и method injections плохие. Необходимо было гуглить. |
Ninject | Есть | Не все случаи описаны. Описания таких вещей, как регистрация с expression, property и method injections плохие. Необходимо было гуглить. Решения искались тяжело. |
Unity | Плохо | Несмотря на количество текста, документация бесполезна, т.к. приходится разбираться в "простынях" текста. Все случаи пришлось гуглить, при этом их сложно найти. |
Castle Windsor | Средне | Не все случаи описаны, или имеют непонятные примеры. Пришлось погуглить. |
Ссылки на документация, чтобы вы сами убедились:
Конфигурация
Здесь я не рассматриваю конфигурацию посредством XML. Все примеры описывают частые случаи конфигурирования IoC контейнеров посредством их интерфейса. Здесь вы можете найти следующее:
- Внедрение через конструкторы.
- Внедрение с помощью свойств.
- Внедрение с помощью методов.
- Регистрация с помощью выражений, когда вы можете указать дополнительную логику по созданию.
- Регистрация по соглашению, когда вы можете автоматически регистрировать всё (просто всё).
- Регистрация с помощью модулей, когда вы можете указать класс, который инкапсулирует конфигурацию.
Цель статьи состоит в том, чтобы привести рабочие примеры для каждого из случаев. Такие сложные сценарии, как параметризованные регистрации лежат за рамками этого текста.
Модель объекта и тестового сценария
Для того чтобы проверить контейнеры IoC я создал простую модель. Есть несколько её модификаций, чтобы использовать property и method injection. Некоторые из IoC контейнеров требуют использования специальных атрибутов, чтобы инициализировать через свойства или методы. Я явно написал об этом в каждой секции.
/*************
* Interfaces *
**************/
public interface IAuthorRepository{
IList<Book> GetBooks(Author parent);
}
public interface IBookRepository{
IList<Book> FindByParent(int parentId);
}
public interface ILog{
void Write(string text);
}
/***********************************************
* Implementation for injection via constructor *
***********************************************/
internal class AuthorRepositoryCtro : IAuthorRepository{
private readonly IBookRepository _bookRepository;
private readonly ILog _log;
public AuthorRepositoryCtro(ILog log, IBookRepository bookRepository) {
_log = log;
_bookRepository = bookRepository;
}
public IList<Book> GetBooks(Author parent) {
_log.Write("AuthorRepository:GetBooks()");
return _bookRepository.FindByParent(parent.Id);
}}
internal class BookRepositoryCtro : IBookRepository{
private readonly ILog _log;
public BookRepositoryCtro(ILog log) {
_log = log;
}
public IList<Book> FindByParent(int parentId) {
_log.Write("BookRepository:FindByParent()");
return null;
}}
internal class ConsoleLog : ILog{
public void Write(string text) {
Console.WriteLine("{0}", text);
}}
Тестовый сценарий создать контейнер и получает объект из него два раза, чтобы посмотреть, как работает их управление timelife scope. Об этом будет следующая статья.
private static void Main(string[] args){
var resolver = Configuration.Simple();
/***********************************************************
* both resolving use the same method of IBookRepository *
* it depends on lifetime scope configuration whether ILog *
* would be the same instance (the number in the output *
* shows the number of the instance) *
***********************************************************/
// the 1st resolving
var books = resolver.Resolve<IAuthorRepository>().GetBooks(new Author());
// the 2nd resolving
resolver.Resolve<IBookRepository>().FindByParent(0);
System.Console.WriteLine("Press any key...");
System.Console.ReadKey();
}
Внедрение через конструкторы
Конфигурация для этого не требует каких-либо специальных атрибутов или имен в своем базовом варианте.
Autofac
var builder = new ContainerBuilder();
builder.RegisterType<AuthorRepositoryCtro>().As<IAuthorRepository>();
builder.RegisterType<BookRepositoryCtro>().As<IBookRepository>();
builder.RegisterType<ConsoleLog>().As<ILog>();
var container = builder.Build();
Simple Injector
var container = new Container();
container.Register<IAuthorRepository, AuthorRepositoryCtro>();
container.Register<IBookRepository, BookRepositoryCtro>();
container.Register<ILog, ConsoleLog>();
StructureMap
var container = new Container();
container.Configure(c =>
{
c.For<IAuthorRepository>().Use<AuthorRepositoryCtro>();
c.For<IBookRepository>().Use<BookRepositoryCtro>();
c.For<ILog>().Use<ConsoleLog>();
});
Ninject
var container = new StandardKernel();
container.Bind<IAuthorRepository>().To<AuthorRepositoryCtro>();
container.Bind<IBookRepository>().To<BookRepositoryCtro>();
container.Bind<ILog>().To<ConsoleLog>();
Unity
var container = new UnityContainer();
container.RegisterType<IAuthorRepository, AuthorRepositoryCtro>();
container.RegisterType<IBookRepository, BookRepositoryCtro>();
container.RegisterType<ILog, ConsoleLog>();
Castle Windsor
var container = new WindsorContainer();
container.Register(Component.For<IAuthorRepository>().ImplementedBy<AuthorRepositoryCtro>());
container.Register(Component.For<IBookRepository>().ImplementedBy<BookRepositoryCtro>());
container.Register(Component.For<ILog>().ImplementedBy<ConsoleLog>());
Внедрение с помощью свойств
Некоторые IoC контейнеры требуют использования специальных атрибутов, которые помогают распознавать свойства для инициализации. Мне лично не нравится этот подход, поскольку модель объекта и IoC контейнер становится сильно связаны. Ninject требует использования атрибута [Inject], Unity требует атрибут [Dependency]. В то же время Castle Windsor не требует ничего, чтобы инициализировать свойства, т.к. у него это происходит по умолчанию.
Autofac
var builder = new ContainerBuilder();
builder.RegisterType<AuthorRepositoryCtro>().As<IAuthorRepository>().PropertiesAutowired();
builder.RegisterType<BookRepositoryCtro>().As<IBookRepository>().PropertiesAutowired();
builder.RegisterType<ConsoleLog>().As<ILog>();
var container = builder.Build();
Simple Injector
У него нет встроенных возможностей для этого, но можно использовать конфигурацию с помощью экспрешнов.
StructureMap
var container = new Container();
container.Configure(c =>
{
c.For<IAuthorRepository>().Use<AuthorRepositoryProp>();
c.For<IBookRepository>().Use<BookRepositoryProp>();
c.For<ILog>().Use(() => new ConsoleLog());
c.Policies.SetAllProperties(x => {
x.OfType<IAuthorRepository>();
x.OfType<IBookRepository>();
x.OfType<ILog>();
});
});
Ninject
var container = new StandardKernel();
container.Bind<IAuthorRepository>().To<AuthorRepositoryProp>();
container.Bind<IBookRepository>().To<BookRepositoryProp>();
container.Bind<ILog>().To<ConsoleLog>();
Unity
var container = new UnityContainer();
container.RegisterType<IAuthorRepository, AuthorRepositoryProp>();
container.RegisterType<IBookRepository, BookRepositoryProp>();
container.RegisterType<ILog, ConsoleLog>();
Castle Windsor
var container = new WindsorContainer();
container.Register(Component.For<IAuthorRepository>().ImplementedBy<AuthorRepositoryProp>());
container.Register(Component.For<IBookRepository>().ImplementedBy<BookRepositoryProp>());
container.Register(Component.For<ILog>().ImplementedBy<ConsoleLog>());
Внедрение с помощью методов
Данный подход, как и предыдущий, может помочь с циклическими ссылками. С другой стороны, это вносит еще один момент, который следует избегать. В нескольких словах API не дает никакого намека на то, что такая инициализация требуется для полноценного создания объекта. Тут чуть подробнее о temporal coupling.
Тут так же некоторые контейнеры IoC требуют использования специальных атрибутов с теми же недостатками. Ninject требует атрибут [Inject] для методов. Unity требует использования атрибута [InjectionMethod]. Все методы, помеченные такими атрибутами, будут выполнены в моментсоздания объекта контейнером.
Autofac
var builder = new ContainerBuilder();
builder.Register(c => {
var rep = new AuthorRepositoryMtd();
rep.SetDependencies(c.Resolve<ILog>(), c.Resolve<IBookRepository>());
return rep;
}).As<IAuthorRepository>();
builder.Register(c => {
var rep = new BookRepositoryMtd();
rep.SetLog(c.Resolve<ILog>());
return rep;
}).As<IBookRepository>();
builder.Register(c => new ConsoleLog()).As<ILog>();
var container = builder.Build();
Simple Injector
var container = new Container();
container.Register<IAuthorRepository>(() => {
var rep = new AuthorRepositoryMtd();
rep.SetDependencies(container.GetInstance<ILog>(), container.GetInstance<IBookRepository>());
return rep;
});
container.Register<IBookRepository>(() => {
var rep = new BookRepositoryMtd();
rep.SetLog(container.GetInstance<ILog>());
return rep;
});
container.Register<ILog>(() => new ConsoleLog());
StructureMap
var container = new Container();
container.Configure(c => {
c.For<IAuthorRepository>().Use<AuthorRepositoryMtd>()
.OnCreation((c, o) => o.SetDependencies(c.GetInstance<ILog>(), c.GetInstance<IBookRepository>()));
c.For<IBookRepository>().Use<BookRepositoryMtd>()
.OnCreation((c, o) => o.SetLog(c.GetInstance<ILog>()));
c.For<ILog>().Use<ConsoleLog>();
});
Ninject
var container = new StandardKernel();
container.Bind<IAuthorRepository>().To<AuthorRepositoryMtd>()
.OnActivation((c, o) => o.SetDependencies(c.Kernel.Get<ILog>(), c.Kernel.Get<IBookRepository>()));
container.Bind<IBookRepository>().To<BookRepositoryMtd>()
.OnActivation((c, o) => o.SetLog(c.Kernel.Get<ILog>()));
container.Bind<ILog>().To<ConsoleLog>();
Unity
var container = new UnityContainer();
container.RegisterType<IAuthorRepository, AuthorRepositoryMtd>();
container.RegisterType<IBookRepository, BookRepositoryMtd>();
container.RegisterType<ILog, ConsoleLog>();
Castle Windsor
var container = new WindsorContainer();
container.Register(Component.For<IAuthorRepository>().ImplementedBy<AuthorRepositoryMtd>()
.OnCreate((c, o) => ((AuthorRepositoryMtd) o).SetDependencies(c.Resolve<ILog>(), c.Resolve<IBookRepository>())));
container.Register(Component.For<IBookRepository>().ImplementedBy<BookRepositoryMtd>()
.OnCreate((c, o) => ((BookRepositoryMtd)o).SetLog(c.Resolve<ILog>())));
container.Register(Component.For<ILog>().ImplementedBy<ConsoleLog>());
Регистрация с помощью выражений
Большинство случаев в предыдущих секциях являются ни чем иным, как регистрация с помощью лямбда-выражений или делегатов. Такой способ регистрации поможет вам добавить некоторую логику в тот момент, когда создаются объекты, но это не динамический подход. Для динамики следует использовать параметризованную регистрацию, чтобы иметь возможность в run-time создавать разные реализации для одного компонента.
Autofac
var builder = new ContainerBuilder();
builder.Register(c => new AuthorRepositoryCtro(c.Resolve<ILog>(), c.Resolve<IBookRepository>()))
.As<IAuthorRepository>();
builder.Register(c => new BookRepositoryCtro(c.Resolve<ILog>()))
.As<IBookRepository>();
builder.Register(c => new ConsoleLog()).As<ILog>();
var container = builder.Build();
Simple Injector
var container = new Container();
container.Register<IAuthorRepository>(() =>
new AuthorRepositoryCtro(container.GetInstance<ILog>(), container.GetInstance<IBookRepository>()));
container.Register<IBookRepository>(() =>
new BookRepositoryCtro(container.GetInstance<ILog>()));
container.Register<ILog>(() => new ConsoleLog());
StructureMap
var container = new Container();
container.Configure(r => {
r.For<IAuthorRepository>()
.Use(c => new AuthorRepositoryCtro(c.GetInstance<ILog>(), c.GetInstance<IBookRepository>()));
r.For<IBookRepository>()
.Use(c => new BookRepositoryCtro(c.GetInstance<ILog>()));
r.For<ILog>().Use(() => new ConsoleLog());
});
Ninject
var container = new StandardKernel();
container.Bind<IAuthorRepository>().ToConstructor(c =>
new AuthorRepositoryCtro(c.Inject<ILog>(), c.Inject<IBookRepository>()));
container.Bind<IBookRepository>().ToConstructor(c =>
new BookRepositoryCtro(c.Inject<ILog>()));
container.Bind<ILog>().ToConstructor(c => new ConsoleLog());
или
container.Bind<IAuthorRepository>().ToMethod(c =>
new AuthorRepositoryCtro(c.Kernel.Get<ILog>(), c.Kernel.Get<IBookRepository>()));
container.Bind<IBookRepository>().ToMethod(c =>
new BookRepositoryCtro(c.Kernel.Get<ILog>()));
container.Bind<ILog>().ToMethod(c => new ConsoleLog());
Unity
var container = new UnityContainer();
container.RegisterType<IAuthorRepository>(new InjectionFactory(c =>
new AuthorRepositoryCtro(c.Resolve<ILog>(), c.Resolve<IBookRepository>())));
container.RegisterType<IBookRepository>(new InjectionFactory(c =>
new BookRepositoryCtro(c.Resolve<ILog>())));
container.RegisterType<ILog>(new InjectionFactory(c => new ConsoleLog()));
Castle Windsor
var container = new WindsorContainer();
container.Register(Component.For<IAuthorRepository>()
.UsingFactoryMethod(c => new AuthorRepositoryCtro(c.Resolve<ILog>(), c.Resolve<IBookRepository>())));
container.Register(Component.For<IBookRepository>()
.UsingFactoryMethod(c => new BookRepositoryCtro(c.Resolve<ILog>())));
container.Register(Component.For<ILog>().UsingFactoryMethod(c => new ConsoleLog()));
Ninject имеет различия между конфигурированием с помощью ToMethod и ToConstructor. В нескольких словах, когда вы используете ToContructor вы также можете использовать условия. Следующая конфигурация не будет работать для ToMethod.
Bind<IFoo>().To<Foo1>().WhenInjectedInto<Service1>();
Bind<IFoo>().To<Foo2>().WhenInjectedInto<Service2>();
Регистрация по соглашению
В некоторых случаях вам не нужно писать код конфигурации вообще. Общий сценарий выглядит следующим образом: сканирование assembly для поиска нужных типов, извлечение их интерфейсов и регистрация их в контейнере, как пара интерфейс-реализация. Это может быть полезно для очень больших проектов, но может быть сложно для разработчика незнакомово с проектом. Следует помнить несколько моментов.
Autofac регистрирует все возможные варианты реализаций и сохраняет их во внутреннем массиве. В соответствии с документацией, он будет использовать самый последний вариант для резолва по умолчанию. Simple Injector не имеет готовых методов для автоматической регистрации. Вы должны сделать это вручную (пример ниже). StructureMap и Unity требуют public классы имплементаций, т.к. их сканеры другие не видят. Ninject требует дополнительный NuGet пакет Ninject.Extensions.Conventions. И он так же требует public-классы имплементаций.
Autofac
var builder = new ContainerBuilder();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).AsImplementedInterfaces();
var container = builder.Build();
Simple Injector
var container = new Container();
var repositoryAssembly = Assembly.GetExecutingAssembly();
var implementationTypes = from type in repositoryAssembly.GetTypes()
where type.FullName.Contains("Repositories.Constructors")
|| type.GetInterfaces().Contains(typeof (ILog))
select type;
var registrations =
from type in implementationTypes
select new { Service = type.GetInterfaces().Single(), Implementation = type };
foreach (var reg in registrations)
container.Register(reg.Service, reg.Implementation);
StructureMap
var container = new Container();
container.Configure(c => c.Scan(x => {
x.TheCallingAssembly();
x.RegisterConcreteTypesAgainstTheFirstInterface();
}));
Ninject
var container = new StandardKernel();
container.Bind(x => x.FromThisAssembly().SelectAllClasses().BindDefaultInterfaces());
Unity
var container = new UnityContainer();
container.RegisterTypes(
AllClasses.FromAssemblies(Assembly.GetExecutingAssembly()),
WithMappings.FromAllInterfaces);
Castle Windsor
var container = new WindsorContainer();
container.Register(Classes.FromAssembly(Assembly.GetExecutingAssembly())
.IncludeNonPublicTypes()
.Pick()
.WithService.DefaultInterfaces());
Регистрация с помощью модулей
Модули могут помочь вам разделить вашу конфигурацию. Вы можете сгруппировать их по контексту (доступ к данным, бизнес-объекты) или по назначению (production, test). Некоторые из контейнеров IoC может сканировать сборки в поисках своих модулей. Тут я описал основной способ их использования.
Autofac
public class ImplementationModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<AuthorRepositoryCtro>().As<IAuthorRepository>();
builder.RegisterType<BookRepositoryCtro>().As<IBookRepository>();
builder.RegisterType<ConsoleLog>().As<ILog>();
}
}
/*********
* usage *
*********/
var builder = new ContainerBuilder();
builder.RegisterModule(new ImplementationModule());
var container = builder.Build();
Simple Injector
Ничего такого нет.
StructureMap
public class ImplementationModule : Registry
{
public ImplementationModule()
{
For<IAuthorRepository>().Use<AuthorRepositoryCtro>();
For<IBookRepository>().Use<BookRepositoryCtro>();
For<ILog>().Use<ConsoleLog>();
}
}
/*********
* usage *
*********/
var registry = new Registry();
registry.IncludeRegistry<ImplementationModule>();
var container = new Container(registry);
Ninject
public class ImplementationModule : NinjectModule
{
public override void Load()
{
Bind<IAuthorRepository>().To<AuthorRepositoryCtro>();
Bind<IBookRepository>().To<BookRepositoryCtro>();
Bind<ILog>().To<ConsoleLog>();
}
}
/*********
* usage *
*********/
var container = new StandardKernel(new ImplementationModule());
Unity
public class ImplementationModule : UnityContainerExtension
{
protected override void Initialize()
{
Container.RegisterType<IAuthorRepository, AuthorRepositoryCtro>();
Container.RegisterType<IBookRepository, BookRepositoryCtro>();
Container.RegisterType<ILog, ConsoleLog>();
}
}
/*********
* usage *
*********/
var container = new UnityContainer();
container.AddNewExtension<ImplementationModule>();
Castle Windsor
public class ImplementationModule : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<IAuthorRepository>().ImplementedBy<AuthorRepositoryCtro>());
container.Register(Component.For<IBookRepository>().ImplementedBy<BookRepositoryCtro>());
container.Register(Component.For<ILog>().ImplementedBy<ConsoleLog>());
}
}
/*********
* usage *
*********/
var container = new WindsorContainer();
container.Install(new ImplementationModule());
PS
В следующих текстах рассмотрю lifetime scope management и advanced features.
Комментарии (15)
goncharov_a_v
31.05.2016 14:30Если исходить из примеров, а так же из логики что «тот лучше, для правильного использования которого надо читать меньше документации», то Unity лучший из представленных: почти во всех примерах используется один способ регистрации. Далее, способ регистрации и внедрения важны, но есть и еще один, не менее важный, вопрос — управление жизненным циклом объектов, особенно в части IDisposable.
correy
31.05.2016 14:30После «Autofac Качество Супер» читать не стал.
Razaz
31.05.2016 16:35+1В принципе сейчас это самый бодрый DI контейнер. Который может работать как от простенького Register -> ConstructorInjection до разных наворотов с владением объектом(Диспоузеры, Owned и тд.), скоупами, автоматическими фабриками, работой с метаданными и любыми другими хотелками. Например EventBus на Autofac+Castle Dynamic Proxy.
Плюс пачка готовых интеграций ко всему чему только можно.
djank
31.05.2016 18:54+1Посмотрите ещё в сторону DryIoc — лидера по производительности в обзоре, на который вы дали ссылку. Приличная документация, качественный код, покрытый тестами, автор (белорус, насколько я понял) поддерживает проект уже довольно долгое время и оперативно реагирует на баг-репорты, поставка в виде дллэлки или файлом в проект. В моём проекте эта библиотека помогла тем, что смогла «решить» довольно запутанный граф зависимостей с generics и множественными конструкторами (конечно, это промах в архитектуре, но всё же). Короче, маст, как говорится, хэв.
Razaz
31.05.2016 20:50+1Ему бы на гитхаб, а то даже следить за проектом неудобно.
И вот это немного дико выглядит…
di.Register(Made.Of(() => GetSession(Arg.Of())));
di.WithWebApi(config);
DarkOrion
Это чувство, когда в статье про IoC-контейнеры нет ни одного xml-конфига.
PS: а можно еще сравнение со Spring вне конкурса? В районе 2011 писал одно приложение сразу на java и шарпе, удивился насколько спринг тогда обгонял unity.
ETman
XML умышленно не стал рассматривать, т.к. для этого нужна отдельная статья. В добавок Spring не хочу, т.к. он мне так же не нравится, как и Unity.
ZOXEXIVO
XML-это тормоза. Понятно, что это гибко, но уже давно все XML конфиги удалены и забыты.
Зачем вам такая гибкость? Заменить CacheService с обычного на распределенный? Это примерно как с базой данных — ой, мы используем сотни абстракций, репозиториев и.т.п. на случай, если нам нужно будет сменить базу, а на деле это нужно в 0.0001% случаев.
Проще иметь Setup.cs на каждую сборку где статически описаны все зависимости (можно с группировкой) и все эти Setup тоже вызываются из кода без рефлексии и.т.п. Все четко и понятно.
Razaz
Это кстати решается через те же автофабрики типа IIndex<Key,Service>. Можно куда угодно настройку вкрутить что бы нужную имплементацию получать.
DarkOrion
C такой логикой IoC-контейнер не нужен, да и вообще вынос любых настроек\ресурсов в файл.
Если:
1) Вы готовы ради того, чтобы выводить 11, а не 10 строк на страницу лезть в код и пересобирать ПО;
2) Вам нужно, чтобы приложение запускалось на пару секунд быстрее (на время чтения конфига, ведь на самом деле тормозит не XML, а создание\инициализация объектов);
То да, пишите Setup.cs\Init.cs, создавайте там все объекты руками и руками же собирайте зависимости.
Если вам нужно гибкое приложение — то IoC-контейнер, настройки, конфиги.
Наверное у нас с вами разные кейсы использования IoC, я вижу да, что люди уходят от xml-конфигов, но часто это просто размазывание его аннотациями по всему коду (что несколько убивает его смысла, на мой взгляд).
Razaz
В принципе выставлять весь конфиг наружу — плохая идея. Можно выставлять настройки тех компонент, которые можно менять. Остальное нефиг трогать.
Смысл DI в IoC, а не в конфигах всего через Xml.
areht
> 1) Вы готовы ради того, чтобы выводить 11, а не 10 строк на страницу лезть в код и пересобирать ПО;
Я для этого IoC то заводить не готов.