Ninject динамически привязывается к реализации

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

public class Partner
{
    public int PartnerID { get; set; }
    public string ExportImplementationAssembly { get; set; }
}

public interface IExport
{
    void ExportData(DataTable data);
}

В другом месте у меня есть 2 dll, которые реализуют IExport.

public PartnerAExport : IExport
{
    private readonly _db;
    public PartnerAExport(PAEntities db)
    {
        _db = db;
    }
    public void ExportData(DataTable data)
    {
        // export parter A's data...
    }
}

Затем для партнера Б;

public PartnerBExport : IExport
{
    private readonly _db;
    public PartnerBExport(PAEntities db)
    {
        _db = db;
    }
    public void ExportData(DataTable data)
    {
        // export parter B's data...
    }
}

Текущая привязка Ninject:

public class NinjectWebBindingsModule : NinjectModule
{
    public override void Load()
    {
        Bind<PADBEntities>().ToSelf();
        Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
                          .SelectAllClasses()
                          .BindDefaultInterfaces()
                   );
    }
}

Итак, как мне настроить привязки так, чтобы я мог это сделать;

foreach (Partner partner in _db.Partners)
{
    // pseudocode...
    IExport exportModule = ninject.Resolve<IExport>(partner.ExportImplementationAssembly);
    exportModule.ExportData(_db.GetPartnerData(partner.PartnerID));
}

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

ОБНОВЛЕНИЕ: я обновил свой код с помощью решения BatteryBackupUnit, так что теперь у меня есть следующее;

Bind<PADBEntities>().ToSelf().InRequestScope();
Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
                    .SelectAllClasses()
                    .BindDefaultInterfaces()
                    .Configure(c => c.InRequestScope())
            );

Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.Modules.*.dll")
                    .SelectAllClasses()
                    .InheritedFrom<IExportService>()
                    .BindSelection((type, baseTypes) => new[] { typeof(IExportService) })
            );
Kernel.Bind<IExportServiceDictionary>().To<ExportServiceDictionary>().InSingletonScope();
ExportServiceDictionary dictionary = KernelInstance.Get<ExportServiceDictionary>();

Создание экземпляров реализации экспорта в двух тестовых модулях работает и отлично создает экземпляр контекста PADBEntites. Однако все другие привязки на моем уровне служб теперь больше не работают для остальной части системы. Точно так же я не могу привязать слой экспорта, если я изменю аргумент PADBEntities variable/ctor на компонент ISomeEntityService. Кажется, я пропустил последний шаг в настройке привязок, чтобы получить эту работу. Есть предположения?

Ошибка: «Ошибка активации ISomeEntityService. Нет доступных привязок, и тип не является самопривязываемым»

Обновление 2: в конце концов, путем проб и ошибок, используя решение BatteryBackupUnit, я получил это, хотя я не слишком доволен обручами, чтобы прыгать с мысли. Любое другое более лаконичное решение приветствуется.

Я изменил исходную привязку соглашения;

        Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
                          .SelectAllClasses()
                          .BindDefaultInterfaces()
                   );

к гораздо более подробному и явному;

Bind<IActionService>().To<ActionService>().InRequestScope();
Bind<IAuditedActionService>().To<AuditedActionService>().InRequestScope();
Bind<ICallService>().To<CallService>().InRequestScope();
Bind<ICompanyService>().To<CompanyService>().InRequestScope();
//...and so on for 30+ lines

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

Обновление 3: не обращайте внимания на проблему с привязками в обновлении 2. Похоже, я обнаружил ошибку в Ninject, связанную с наличием нескольких модулей привязки в библиотеке, на которую ссылаются. Изменение в модуле A, даже если оно никогда не попадало в точку останова, приведет к поломке проекта, явно использующего другой модуль B. Поймите сами.


person DiskJunky    schedule 07.09.2015    source источник
comment
ИМО, ответы ниже реализуют какую-то форму фабрики, что является правильным ответом. Введите фабрику, пусть фабрика вернет правильный IExport. Несмотря на некоторые (содержательные) комментарии в ответах ниже, наличие этого на фабрике помогает оградить вас от специфических функций ninject.   -  person Trevor Ash    schedule 07.09.2015
comment
@Atoms, здесь в некоторой степени требуется ninject, поскольку для реализации экспорта требуются другие службы в их ctors, которые уже связаны через ядро ​​ninject. Имеет смысл повторно использовать их, а не реализовывать конкретную фабрику.   -  person DiskJunky    schedule 07.09.2015
comment
Какая привязка отсутствовала при использовании соглашения о двух частях? Также обратите внимание, что с соглашением о двух частях вы бы связали каждый IExport дважды. Для первого соглашения вы должны были исключить все IExport. Я предлагаю вам создать новый вопрос о том, как сформировать вопрос. Возможно, другая платформа SE, такая как codereview или программисты, была бы лучше. Предложение. Почему бы не ввести соглашение для всех типов, оканчивающихся на Service, и специально связать все остальные? Кроме того, если есть более конкретные привязки, их можно поместить в NinjectModule в конкретных сборках.   -  person BatteryBackupUnit    schedule 07.09.2015
comment
@BatteryBackupUnit, я бы так и сделал, если бы знал как :) Мой нинжект на уровне n00b. Я видел повторяющиеся привязки, но отфильтровал их при заполнении словаря. Как исключить IExport в исходной привязке? Что касается создания правила для служб, у меня также есть ... Reader и ... Writer, но я думаю, что их тоже можно настроить. Хитрость заключается в том, «как». Документация Ninject не самая лучшая, и сложно понять, что вам нужно для решения данной проблемы. Отсюда этот вопрос в первую очередь   -  person DiskJunky    schedule 07.09.2015
comment
@Да, я понял. однако вопросы, касающиеся контекстной привязки для IExportи того, как разработать обычные привязки, следует разделить. как спроектировать обычные привязки, не подходит для SO. Конкретный вопрос => как исключить тип хорошо подходит для SO. Ответ: вы можете использовать метод Wherefilter для исключения всех типов, реализующих IExport. Дополнительную информацию о соглашениях см. также здесь   -  person BatteryBackupUnit    schedule 07.09.2015
comment
@BatteryBackupUnit, честно говоря, я извлеку и повторно опубликую соглашения. Я пометил ваш ответ как ответ, так как он привел меня к рабочему решению. Спасибо за помощь!   -  person DiskJunky    schedule 08.09.2015


Ответы (2)


Важно отметить, что, хотя фактическое «совпадение условий» является условием времени выполнения, вы на самом деле заранее знаете возможный набор совпадений (по крайней мере, при запуске при сборке контейнера), о чем свидетельствует использование соглашений. Это то, что касается условных/контекстных привязок (описано в Ninject WIKI и рассмотрено в нескольких вопросах). Таким образом, вам на самом деле не нужно выполнять привязку в произвольное время выполнения, вам просто нужно выполнить разрешение/выбор в произвольное время (на самом деле разрешение может быть выполнено заранее => сбой раньше).

Вот возможное решение, которое включает:

  • создание всех привязок при запуске
  • преждевременный сбой: проверка привязок при запуске (через создание экземпляров всех связанных IExport)
  • выбор IExport в произвольной среде выполнения

.

internal interface IExportDictionary
{
    IExport Get(string key);
}

internal class ExportDictionary : IExportDictionary
{
    private readonly Dictionary<string, IExport> dictionary;

    public ExportDictionary(IEnumerable<IExport> exports)
    {
        dictionary = new Dictionary<string, IExport>();
        foreach (IExport export in exports)
        {
            dictionary.Add(export.GetType().Assembly.FullName, export);
        }
    }

    public IExport Get(string key)
    {
        return dictionary[key];
    }
}

Корень композиции:

// this is just going to bind the IExports.
// If other types need to be bound, go ahead and adapt this or add other bindings.
kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
        .SelectAllClasses()
        .InheritedFrom<IExport>()
        .BindSelection((type, baseTypes) => new[] { typeof(IExport) }));

kernel.Bind<IExportDictionary>().To<ExportDictionary>().InSingletonScope();

// create the dictionary immediately after the kernel is initialized.
// do this in the "composition root".
// why? creation of the dictionary will lead to creation of all `IExport`
// that means if one cannot be created because a binding is missing (or such)
// it will fail here (=> fail early).
var exportDictionary = kernel.Get<IExportDictionary>(); 

Теперь IExportDictionary можно внедрить в любой компонент и просто использовать как «обязательное»:

foreach (Partner partner in _db.Partners)
{
    // pseudocode...
    IExport exportModule = exportDictionary.Get(partner.ExportImplementationAssembly);
    exportModule.ExportData(_db.GetPartnerData(partner.PartnerID));
}
person BatteryBackupUnit    schedule 07.09.2015
comment
хорошая реализация в силу того, что ее легче понять, чем у Стива. Однако привязка не удалась, когда я пытался создать экземпляр словаря, когда я начал добавлять другие интерфейсы/объект PADBEntities в ctors реализации экспорта. Любые идеи? - person DiskJunky; 07.09.2015
comment
Это (скорее всего) означает, что не все типы, требуемые некоторыми IExport, связаны. Обратите внимание, что в своем ответе я изменил привязку соглашения для типов PartnerAdapter.*.dlls. Вы можете изменить это обратно на то, что у вас было изначально, или сделать более подходящее соглашение/привязку. Я сделал изменение, потому что я не рекомендую такого рода очень широкое соглашение. Скорее, я бы реализовал dll несколько NinjectModule и загрузил их с помощью kernel.Load. Если мое подозрение неверно, опубликуйте полное исключение (тип, сообщение, трассировка стека, потенциальные внутренние исключения..) - person BatteryBackupUnit; 07.09.2015
comment
Я так и подумал, но выбор привязки, который у вас есть в вашем примере, нарушает существующую привязку, которая была у меня в OP, - он больше не может видеть ссылки IService в уже настроенных мной ctors. Ошибка: Error activating ISomeEntityService. No matching bindings are available and the type is not self-bindable. Эта привязка работает, как указано в исходном сообщении, но не при добавлении привязки для IExport. - person DiskJunky; 07.09.2015
comment
Я обновил свой вопрос уродливым, но работающим решением для привязки. Можете ли вы предложить какие-либо улучшения? - person DiskJunky; 07.09.2015

Я хотел бы выполнить привязку Ninject на основе условия выполнения, которое заранее неизвестно при запуске.

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

Вместо этого примите решение во время выполнения... во время выполнения, переместив это в прокси-класс для IExport. Как именно выглядит такой прокси, зависит от вашей конкретной ситуации, но вот пример:

public sealed class ExportProxy : IExport
{
    private readonly IExport export1;
    private readonly IExport export2;
    public ExportProxy(IExport export1, IExport export2) {
        this.export1 = export1;
        this.export2 = export2;
    }

    void IExport.ExportData(Partner partner) {
        IExport exportModule = GetExportModule(partner.ExportImplementationAssembly);
        exportModule.ExportData(partner);
    }

    private IExport GetExportModule(ImplementationAssembly assembly) {
        if (assembly.Name = "A") return this.export1;
        if (assembly.Name = "B") return this.export2;
        throw new InvalidOperationException(assembly.Name);
    }
}

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

public sealed class ExportProxy : IExport
{
    private readonly Func<ImplementationAssembly, IExport> exportProvider;
    public ExportProxy(Func<ImplementationAssembly, IExport> exportProvider) {
        this.exportProvider = exportProvider;
    }

    void IExport.ExportData(Partner partner) {
        IExport exportModule = this.exportProvider(partner.ExportImplementationAssembly);
        exportModule.ExportData(partner);
    }
}

Предоставляя прокси с Func<,>, вы все равно можете принять решение в том месте, где вы регистрируете свой ExportProxy (корень композиции), где вы можете запросить систему для сборок. Таким образом, вы можете заранее зарегистрировать реализации IExport в контейнере, что повышает проверяемость конфигурации. Если вы зарегистрировали все реализации IExport с помощью ключа, вы можете выполнить следующую простую регистрацию для ExportProxy

kernel.Bind<IExport>().ToInstance(new ExportProxy(
    assembly => kernel.Get<IExport>(assembly.Name)));
person Steven    schedule 07.09.2015
comment
Эффективно, но проблематично — потенциально существует большое количество партнеров, использующих неизвестное конечное количество реализаций экспорта. Для вашего решения потребуется новый аргумент конструктора и обновление if или switch каждый раз, когда мы хотим добавить новую реализацию. Тем не менее, я не рассматривал этот подход - person DiskJunky; 07.09.2015
comment
@DiskJunky: мое решение не требует нового аргумента ctor; это просто простой пример. Адаптируйте решение к вашему конкретному случаю. Если у вас много сборок, вы можете вместо этого внедрить словарь с отображением. - person Steven; 07.09.2015
comment
По какой-то причине мы, как разработчики (включая меня), пытаемся решить все проблемы с помощью инструментов, которые мы используем, в то время как некоторые проблемы не для решения инструментов. Если мы сделаем шаг назад и посмотрим на проблему так, как будто мы вообще не используем инструмент, это часто приводит к более чистому и практичному решению. - person Steven; 07.09.2015
comment
@Steven: Как бы выглядел этот словарь? Как это будет поддерживаться? Как бы это проверить? Стоит ли оно того, если есть, скажем, 100 записей? - person BatteryBackupUnit; 07.09.2015
comment
@BatteryBackupUnit: Как бы вы подумали о наборе сборок в .NET и заполнили соответствующие типы в словаре? Точный ответ, конечно, зависит от точных требований ОП, но решение будет тривиальным. С Ninject вы даже можете (ab) использовать ядро ​​как словарь; см. мое обновление. - person Steven; 07.09.2015
comment
Идея построения сразу всего графа объектов противоречит идее InRequestScope. Если нужно InRequestScope, это всегда будет сложно сделать. Построение всего графа объектов по каждому запросу (со всеми возможными IExports) на самом деле не кажется такой уж хорошей идеей — с точки зрения производительности. Тогда оптимизация должна попытаться заменить InRequestScope созданием DbContext при необходимости и передачей его (вместо того, чтобы вводить его InRequestScope ) - person BatteryBackupUnit; 07.09.2015
comment
@Стивен, очень интересный подход! Я должен попробовать это, чтобы увидеть, работает ли это - я немного не уверен, правильно ли разрешится этот поздний вызов kernel.Bind() в лямбде, но стоит попытаться реализовать - person DiskJunky; 07.09.2015
comment
@BatteryBackupUnit, в этом случае мы можем игнорировать InRequestScope() — к счастью, для экспорта он не нужен. Я бы просто скопировал/вставил из существующей реализации привязки. удалю из поста за ненадобностью - person DiskJunky; 07.09.2015
comment
@BatteryBackupUnit: хотя я предпочитаю, чтобы все компоненты, являющиеся частью графа объектов, были зарегистрированы как синглтоны, это не то, что я пытался обсудить здесь, и это не то, о чем я говорил. Я имел в виду, что контейнер должен заранее знать, какую регистрацию/привязку следует внедрить в потребителя. Эта конкретная привязка может по-прежнему иметь образ жизни с ограниченной областью действия, что приводит к множеству экземпляров. Даже при наличии нескольких экземпляров точная форма графа объектов известна во время компоновки. - person Steven; 07.09.2015
comment
Хорошо, я понял. Но разве ExportProxy не лишний? Можно просто использовать Func<ImplementationAssembly, IExport> exportProvider напрямую. Конечно, если бы он использовался в нескольких местах, я бы также извлек логику в файл ExportProxy. Однако, если IExport имеет несколько методов/аргументов (которые не требуются для принятия решения о том, какой фактический экспорт использовать), тогда я бы предпочел использовать ExportProvider вместо прокси (который в основном совпадает с ответом, который я предоставил). - person BatteryBackupUnit; 07.09.2015
comment
@BatteryBackupUnit: для меня все дело в сокрытии деталей реализации от потребителя. Внедрение Func<Asm, IExport> или ExportDictionary в потребителей информирует их о существовании нескольких реализаций, а потребитель получает дополнительную концепцию/абстракцию для работы. Но, в конце концов, это зависит от контекста, потому что с моим решением может потребоваться OP для изменения интерфейса IExport. Хотя измененная абстракция может быть лучше, но если изменение невозможно, вы снова получите дополнительную фабрику/поставщика, подобную абстракции, такой как ваш IExportDictionary. - person Steven; 07.09.2015