Сравнение конфигураций Dependency Injection фреймворков

в 5:48, , рубрики: .net, autofac, C#, castle.windsor, comparison, dependency injection, ioc, Ninject, simple injector, structuremap, unity-container

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

Автор: ETman

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js