Много раз я спрашивал себя, что какой 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.
