Где возможность RegisterAll с регистрационными контекстами (он же Func?

я могу зарегистрировать один элемент регистрации с контекстом instanceCreator (он же Func<T>), но, похоже, нет такого же пособия с RegisterAll.

TL; DR-найдите принятый ответ и посмотрите обновление 2 (или пропустите обновление 3 по этому вопросу)

вот что я хочу сделать:

container.RegisterAll<IFileWatcher>(
    new List<Func<IFileWatcher>>
    {
        () => new FileWatcher(
            @".TriggersTriggerWatchSomeTrigger.txt",
            container.GetInstance<IFileSystem>()),
        () => new FileWatcher(
            @".TriggersTriggerWatchSomeOtherTrigger.txt",
            container.GetInstance<IFileSystem>())
    });

я попытался добавить расширение на основе предыдущий ответ переполнения стека для нескольких регистраций, но, похоже, последний один в wins:

public static class SimpleInjectorExtensions
{
    public static void RegisterAll<TService>(this Container container,
        IEnumerable<Func<TService>> instanceCreators) 
        where TService : class
    {           
        foreach (var instanceCreator in instanceCreators)
        {
            container.RegisterSingle(typeof(TService),instanceCreator);
        }

        container.RegisterAll<TService>(typeof (TService));
    }
}

мне также любопытно, почему существует необходимость RegisterAll существовать в первую очередь. Это первый контейнер для инъекций зависимостей из 5, который я использовал, что делает различие. Другие просто позволяют зарегистрировать несколько типов против службы, а затем загрузить их все, позвонив Resolve<IEnumerable<TService>> (autofac) или GetAllInstances<TService> (как SimpleInjector, так и Ninject).

обновление

для большей ясности, я пытаюсь построить список элементов это я могу передать композиту, который обрабатывает каждый из отдельных элементов. Он страдает от той же проблемы, что и выше, поскольку он попадает в группу задач, которые все регистрируются для запуска на основе расписаний, триггеров и событий (Rx). Чтобы удалить регистр все на мгновение и вырвать некоторые другие вещи:

container.Register<ITask>(() => new FileWatchTask(
    container.GetInstance<IFileSystem>(),
    container.GetInstance<IMessageSubscriptionManagerService>(),
    configuration,
    container.GetAllInstances<IFileWatcher>()));

вы можете видеть, что я хватаю все экземпляры ранее зарегистрированных наблюдателей файлов.

что мне нужно знать просто избежать этой проблемы и когда она будет реализована (или если нет, то почему его не будет). Я также признаю, что это невозможно, учитывая текущие ограничения простой конструкции инжектора. Чего я не приму, так это того, что мне нужно изменить и адаптировать свою архитектуру, чтобы соответствовать ограничениям инструмента.

обновление 2

давайте поговорим о OCP (Open Closed Principle aka The O in SOLID) и впечатление, которое я получаю в том, как SimpleInjector ломает этот конкретный принцип в некоторых случаях.

открытый закрытый принцип - это просто то, что открыто для расширения, но закрыто для модификации. Это означает, что вы можете изменить поведение объекта без изменения его исходного кода.

теперь перейдем к примеру, который актуален здесь:

var tasks = container.GetAllInstances<ITask>();
foreach (var task in tasks.OrEmptyListIfNull())
{
  //registers the task with the scheduler, Rx Event Messaging, or another trigger of some sort  
  task.Initialize();
}

обратите внимание, насколько это чисто. Чтобы иметь возможность сделать это, мне нужно иметь возможность зарегистрировать все экземпляры интерфейса:

container.RegisterAll<ITask>( 
  new List<Func<ITask>>{
    () => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()),
    () => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()),
    () => new DefaultImportFilesTask(container.GetInstance<IFileSystem>())
  }
);

верно? Итак, урок вот что это хорошо и встреча OCP. Я могу изменить поведение бегуна задач, просто добавив или удалив зарегистрированные элементы. Открыт для расширения, закрыт для модификации.

теперь давайте сосредоточимся на том, чтобы сделать так, как предложено в ответе ниже (до второго обновления, которое, наконец, отвечает на этот вопрос), который автор дает впечатление, чтобы быть лучшим дизайном.

начнем с того, что ответ от maintainer упоминает хороший дизайн для регистрации. Точка зрения, которую я получаю, заключается в том, что я должен принести жертву своему коду, чтобы каким-то образом сделать ITask более гибким для работы с SimpleInjector:

container.Register<ITask<SomeGeneric1>(() => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()));
container.Register<ITask<SomeGeneric2>(() => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()));
container.Register<ITask<SomeGeneric3>(() => new DefaultImportFilesTask(container.GetInstance<IFileSystem>()));

теперь давайте посмотрим, как это делает наш изменить конструкцию:

var task1 = container.GetInstances<ITask<SomeGeneric1>();
task1.Initialize();
var task2 = container.GetInstances<ITask<SomeGeneric2>();
task2.Initialize();
var task3 = container.GetInstances<ITask<SomeGeneric3>();
task3.Initialize();

Оуч. Вы можете видеть, как каждый раз, когда я добавляю или удаляю элемент из регистрации контейнера, мне теперь нужно также обновить другой раздел кода. Два места модификации для одного изменения, я ... ломать множественные вопросы дизайна.

вы можете сказать, почему я прошу контейнер для этого? Ну, это в области запуска, но давайте исследуем, если я не был.

так что я буду использовать инъекцию конструктора, чтобы проиллюстрировать, почему это плохо. Сначала давайте рассмотрим мой пример как инъекцию конструкции.

public class SomeClass {
    public SomeClass(IEnumerable<ITask> tasks){}
}

хороший и чистый.

теперь давайте вернемся к моему пониманию представления принятого ответа (снова до обновления 2):

public class SomeClass {
    public SomeClass(ITask<Generic1> task1,
                     ITask<Generic2> task2,
                     ITask<Generic3> task3
                     ) {}
}

Оуч. Каждый раз мне приходится редактировать несколько областей кода, и давайте даже не будем начинать с того, насколько плох этот дизайн.

какой здесь урок? Я не самый умный парень в мире. Я поддерживаю (или пытаюсь поддерживать :)) несколько фреймворков, и я не пытаюсь притворяться, что знаю больше или лучше других. Мое чувство замысла может быть искажено или я могу ограничивать других каким-то неизвестным способом, о котором я еще даже не думал. Я уверен, что автор имеет в виду хорошо, когда он дает советы по дизайну, но в некоторых случаях это может показаться раздражающим (и немного снисходительным), особенно для тех из нас, кто знает, что мы делаем.

обновление 3

таким образом, на вопрос был дан ответ в обновлении 2 от сопровождающего. Я пытался использовать RegisterAll, потому что мне не пришло в голову, что я могу просто использовать Register<IEnumerable<T>> (и, к сожалению, в документации это не указано). Кажется совершенно очевидным, но когда люди делают перейти от других рамок МОК, они несут некоторый багаж с ними и могут пропустить это удивительное упрощение в дизайне! Я пропустил его, с 4 другими контейнерами DI под моим поясом. Надеюсь, он добавит его в документацию или вызовет его немного лучше.

1 ответов


из вашего первого примера (с использованием List<Func<IFileWatcher>>) я понимаю, что вы хотите зарегистрировать коллекцию временных filewatchers. Другими словами, каждый раз, когда вы повторяете список, должен быть создан новый экземпляр file watcher. Это, конечно, очень отличается от регистрации списка с двумя (одноэлементными) filewatchers (те же экземпляры, которые всегда возвращаются). Однако в вашем вопросе есть некоторая двусмысленность, поскольку в методе расширения вы, похоже, регистрируете их как синглтон. Для остальная часть моего ответа, я предполагаю, что вы хотите преходящее поведение.

общий случай использования, для которого , чтобы зарегистрировать список реализаций для общего интерфейса. Например, приложение, которое имеет несколько IEventHandler<CustomerMoved> реализации, которые все должны быть запущены, когда CustomerMoved событие поднялась. В этом случае вы поставляете RegisterAll метод со списком System.Type экземпляры, и контейнер полностью контролирует проводку этих реализаций для вас. Поскольку контейнер контролирует создание, коллекция называется "container-container".

на RegisterAll однако просто перенаправьте создание обратно в контейнер, что означает, что по умолчанию список приводит к созданию переходных экземпляров (поскольку незарегистрированные конкретные типы разрешаются как переходные). Это кажется неудобным, но это позволяет зарегистрировать список с элементами разных стилей жизни, так как вы можете зарегистрировать каждый элемент явно с помощью образ жизни по выбору. Он также позволяет вам поставлять RegisterAll с абстракциями (например,typeof(IService)), и это также будет работать, так как запрос перенаправляется обратно в контейнер.

ваш случай использования разных. Вы хотите зарегистрировать список элементов одного и того же типа, но каждый с другим значением конфигурации. И чтобы сделать вещи более трудными, вы, кажется, хотите зарегистрировать их как переходных, а не синглетов. Не минуя RegisterAll список типов, но IEnumerable<TService> контейнер не создает и автоматически подключает эти типы, мы называем это коллекцией "container-uncontrolled".

короче говоря: как мы это зарегистрируем? Есть несколько способов сделать это, но мне лично нравится этот подход:

string[] triggers = new[]
{
    @".\Triggers\TriggerWatch\SomeTrigger.txt",
    @".\Triggers\TriggerWatch\SomeOtherTrigger.txt"
};

container.RegisterAll<IFileWatcher>(
    from trigger in triggers
    select new FileWatcher(trigger,
        container.GetInstance<IFileSystem>())
);

здесь мы регистрируем запрос LINQ (который является просто IEnumerable<T>) С помощью RegisterAll метод. Каждый раз, когда кто-то решает IEnumerable<IFileWatcher> он возвращает тот же запрос, но с момента выбора этого запроса содержит new FileWatcher, на итерации всегда возвращаются новые экземпляры. Этот эффект можно увидеть, используя следующий тест:

var watchers = container.GetAllInstances<IFileWatcher>();
var first1 = watchers.First();
var first2 = watchers.First();
Assert.AreNotEqual(first1, first2, "Should be different instances");
Assert.AreEqual(first1.Trigger, first2.Trigger);

как показывает этот тест, мы разрешаем коллекцию один раз, но каждый раз, когда мы ее повторяем (.First() проходит сбор), создается новый экземпляр, но оба экземпляра имеют одинаковую @".\Triggers\TriggerWatch\SomeTrigger.txt" значение.

Итак, как вы можете видеть, нет ограничений, которые мешают вам делать это эффективно. Однако, возможно, нужно думать иначе.

мне также интересно, почему существует необходимость в RegisterAll существовать в первое место.

это очень четкое дизайнерское решение. Вы правы, что большинство других контейнеров просто позволяют вам делать кучу регистраций одного и того же типа, и при запросе коллекции все регистрации возвращаются. Проблема в том, что легко случайно зарегистрировать тип снова, и это то, что я хотел предотвратить.

кроме того, все контейнеры имеют различное поведение, регистрация которого возвращается при запросе одного экземпляра вместо запроса коллекции. Одни возвращают первую регистрацию, другие-последнюю. Я хотел предотвратить и эту двусмысленность.

и последнее, но не менее важное: обратите внимание, что регистрация коллекций предметов одного типа обычно должна быть исключением. По моему опыту 90% времени, когда разработчики хотят зарегистрировать несколько типов одной и той же абстракции, есть некоторая двусмысленность в их дизайне. Сделав регистрацию коллекций явной, я надеялся, что это выплывет.

чего я не приму, так это того, что мне нужно изменить и адаптировать архитектура для удовлетворения ограничений некоторых инструментов.

я согласен с этим. Ваша архитектура должна быть ведущей, а не инструменты. Вы должны выбрать соответствующие инструменты.

но, пожалуйста, обратите внимание, что простой Инжектор имеет много ограничений и большая часть из тех ограничений выбрана преднамеренно для того чтобы простимулировать потребителей иметь чистую конструкцию. Например, каждый раз, когда вы нарушаете один из твердые принципы в вашем коде у вас будут проблемы. У вас будут проблемы с сохранением гибкого кода, удобочитаемыми тестами и Композиция Root ремонтопригодны. Это в действительности держит для всех контейнеров DI, но возможно даже больше для просто инжектора. Это преднамеренно и если разработчики не заинтересованы в применении твердых принципов и хотят контейнер DI, который просто работает в любых обстоятельствах, возможно, простой инжектор не лучший инструмент для работы. Например, применение простого инжектора к базе устаревшего кода может быть сложным.

я надеюсь, что это дает некоторую перспективу на дизайн простого инжектора.

обновление

Если вам нужны синглеты вместо этого, это еще проще. Вы можете зарегистрировать их как следует:

var fs = new RealFileSystem();

container.RegisterSingle<IFileSystem>(fs);

container.RegisterAll<IFileWatcher>(
    new FileWatcher(@".\Triggers\TriggerWatch\SomeTrigger.txt", fs),
    new FileWatcher(@".\Triggers\TriggerWatch\SomeOtherTrigger.txt", fs)
);

обновление 2

вы явно попросили RegisterAll<T>(Func<T>) поддержка лениво создавать коллекцию. На самом деле для этого уже есть поддержка, просто используя RegisterSingle<IEnumerable<T>>(Func<IEnumerable<T>>), как вы можете увидеть здесь:

container.RegisterSingle<IEnumerable<IFileWatcher>>(() =>
{
    return
        from 
    var list = new List<IFileWatcher>
    {
        new FileWatcher(@".\Triggers\TriggerWatch\SomeTrigger.txt", container.GetInstance<IFileSystem>()),
        new FileWatcher(@".\Triggers\TriggerWatch\SomeOtherTrigger.txt", container.GetInstance<IFileSystem>())        
    };

    return list.AsReadOnly();
});

на RegisterAll<T>(IEnumerable<T>) на самом деле удобная перегрузка, которая в конечном итоге вызывает RegisterSingle<IEnumerable<T>>(collection).

обратите внимание, что я явно возвращает список только для чтения. Это опционно, но экстренный механизм безопасности который предотвращает коллекция от изменения кода приложения. При использовании RegisterAll<T> коллекции автоматически оборачиваются итератором только для чтения.

единственный улов с использованием RegisterSingle<IEnumerable<T>> заключается в том, что контейнер не будет повторять коллекцию при вызове container.Verify(). Однако в вашем случае это не будет проблемой, так как, когда элемент коллекции не инициализирует вызов GetInstance<IEnumerable<IFileWatcher>> также потерпит неудачу, и с этим вызов Verify().

обновление 3

я извиняюсь, если у меня создалось впечатление, что я имел в виду, что ваш дизайн неправильный. Я не могу этого знать. Поскольку вы явно спросили, почему некоторые функции отсутствуют, я изо всех сил пытался объяснить обоснование этого. Это не означает, однако, что я думаю, что ваш дизайн плох, поскольку для меня нет способа узнать.

давайте вернемся к тому, как это будет выглядеть с точки зрения сопровождающего дизайн

я не уверен, почему вы думаете, что это мой взгляд на хороший дизайн? Имея SomeClass с конструктором, который нужно менять каждый раз, когда вы добавляете задачу в систему, определенно не является хорошим дизайном. Мы можем спокойно согласиться на это. Что ломает ОСР. Я бы никогда никому не посоветовал это делать. Кроме того, наличие конструктора со многими аргументами-это, по крайней мере, запах дизайна. Следующий небольшой отпуск простого инжектора даже добавляет диагностическое предупреждение относительно типов с слишком много зависимостей, так как это часто является признаком нарушения SRP. Но снова посмотрите, как простой инжектор пытается "помочь" разработчикам здесь, предоставляя рекомендации.

тем не менее, я продвигаю использование общих интерфейсов, и это тот случай, когда простой дизайн инжектора оптимизирован специально. Хорошим примером этого является интерфейс ITask. В таком случае,ITask<T> часто будет абстракцией над некоторым деловым поведением, которое вы хотите выполнить, и T - это объект который содержит все параметры операции для выполнения (вы можете видеть его как сообщение с обработчиком сообщений). Однако это полезно только тогда, когда потребителю необходимо выполнить операцию с определенным набором параметров (конкретная версия T), например, он хочет выполнить ITask<ShipOrder>. Поскольку вы выполняете пакет всех задач без указания параметра, дизайн на основе ITask<T> было бы неловко.

но предположим на секунду это уместно. Предположим это, поэтому я могу объяснить, как простой инжектор оптимизирован в этом случае. В конце этого обновления я покажу вам, как простой инжектор все еще может помочь в вашем случае, поэтому задержите дыхание. В примере кода общие задачи регистрируются следующим образом:

container.Register<ITask<SomeGeneric1>(() => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()));
container.Register<ITask<SomeGeneric2>(() => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()));
container.Register<ITask<SomeGeneric3>(() => new DefaultImportFilesTask(container.GetInstance<IFileSystem>()));

это довольно болезненный способ регистрации всех задач в системе, так как каждый раз, когда вы меняете конструктор реализации задачи, вам придется менять настоящий кодекс. Простой инжектор позволяет вам к типам автоматическ-провода путем смотреть их конструктор. Другими словами, простой инжектор позволяет упростить этот код следующим образом:

container.Register<ITask<SomeGeneric1>, FileWatchTask>();
container.Register<ITask<SomeGeneric2>, DefaultFtpTask>();
container.Register<ITask<SomeGeneric3>, DefaultImportFilesTask>();

это уже гораздо более ремонтопригодно, приводит к лучшей производительности и позволяет добавлять другие интересные сценарии позже, такие как контекстная инъекция (поскольку простой инжектор контролирует весь график объектов). Это рекомендуемый способ регистрации вещей в Simple Injector (предотвратите использование Func, если это возможно).

тем не менее, имея архитектуру, где задача является центральным элементом, вы, вероятно, добавляли бы новые реализации задач довольно регулярно. Это приведет к десяткам регистрационных строк и необходимости вернуться к этому коду, чтобы добавить строку каждый раз, когда вы добавляете задачу. Простой инжектор, однако, имеет функцию регистрации партии, которая позволяет сжать это обратно в одну строку кода:

// using SimpleInjector.Extensions;
container.RegisterManyForOpenGeneric(typeof(ITask<>), typeof(ITask<>).Assembly);

позвонив по этой линии, контейнер будет искать все ITask<T> реализации, которые расположены в сборке интерфейса, и он зарегистрирует их для вас. Поскольку это делается во время выполнения с помощью отражения, строка не должна изменяться при добавлении новых задач в систему.

и поскольку вы говорите об OCP, IMO Simple Injector имеет большую поддержку OCP. В некоторые моменты он даже превосходит все остальные фреймворки. Когда я думаю о OCP, я думаю об одном специфическая картина: картина декоратора. Шаблон декоратора-очень важный шаблон для использования при применении OCP. Межсекторальные проблемы, например, не следует добавлять путем изменения какой-либо части самой бизнес-логики, но лучше всего добавлять путем упаковки классов с декораторами. С простым инжектором, декоратор можно добавить с как раз одиночной строкой кода:

// using SimpleInjector.Extensions;
container.RegisterDecorator(typeof(ITask<>), typeof(TransactionTaskDecorator<>));

это гарантирует, что (транзиторная) TransactionTaskDecorator<T> оборачивается вокруг всех ITask<T> реализации, когда они получили решенный. Эти декораторы интегрированы в конвейер контейнера, что означает, что они могут иметь собственные зависимости, могут иметь инициализаторы и могут иметь определенный образ жизни. И декораторы могут быть легко уложены:

container.RegisterDecorator(typeof(ITask<>), typeof(TransactionTaskDecorator<>));
container.RegisterDecorator(typeof(ITask<>), typeof(DeadlockRetryTaskDecorator<>));

это обертывает все задачи в декораторе транзакций и снова обертывает этот декоратор транзакций в декораторе повторной попытки взаимоблокировки. И даже декораторов можно применять условно:

container.RegisterDecorator(typeof(ITask<>), typeof(ValidationTaskDecorator<>),
    context => ShouldApplyValidator(context.ServiceType));

и если ваш декоратор имеет общий тип ограничение, простой инжектор автоматически применит декоратор, когда общие ограничения типа совпадут, ничего не нужно делать с этим. И поскольку простой инжектор генерирует деревья выражений и компилирует их до делегатов, все это разовая стоимость. Это не означает, что это бесплатно, но вы заплатите только один раз, а не за решение.

нет другой библиотеки DI, которая делает добавление декораторов таким же легким и гибким, как простой инжектор.

так это где простой инжектор действительно сияет, но это вам не очень помогает: -). Общие интерфейсы не помогут вам в этом случае, но все же, даже в вашем случае, вы можете сделать свою регистрацию более доступной. Если у вас много реализаций задач в системе (то есть гораздо больше трех), вы можете автоматизировать такие вещи:

var taskTypes = (
    from type in typeof(ITask).Assemby.GetTypes()
    where typeof(ITask).IsAssignableFrom(type)
    where !type.IsAbstract && !type.IsGenericTypeDefinition
    select type)
    .ToList();

// Register all as task types singleton
taskTypes.ForEach(type => container.Register(type, type, Lifestyle.Singleton));

// registers a list of all those (singleton) tasks.
container.RegisterAll<ITask>(taskTypes);

альтернативно, с простым инжектором 2.3 и выше, вы можете пройти в Registration экземпляры непосредственно в RegisterAll метод:

var taskTypes =
    from type in typeof(ITask).Assemby.GetTypes()
    where typeof(ITask).IsAssignableFrom(type)
    where !type.IsAbstract && !type.IsGenericTypeDefinition
    select type;

// registers a list of all those (singleton) tasks.
container.RegisterAll(typeof(ITask),
    from type in taskTypes
    select Lifestyle.Singleton.CreateRegistration(type, type, container));

это предполагает, однако, что все эти реализации задач имеют один открытый конструктор, и все аргументы конструктора разрешимы (нет значений конфигурации, таких как int и string). Если это не так, есть способы изменить поведение фреймворка по умолчанию, но если вы хотите что-то знать об этом, было бы лучше перенести это обсуждение на новый вопрос SO.

еще раз, мне жаль, если я раздражал вас, но я скорее раздражать некоторых разработчиков, чем упустить возможность помочь многим другим: -)