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 библиотеки, которые реализуют IExport

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

Тогда для партнера B;

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 Аргумент переменной / 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.

2 ответа

Решение

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

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

  • создание всех привязок при запуске
  • преждевременный сбой: проверка привязок при запуске (через создание всех связанных IExports)
  • выбор 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));
}

Я хотел бы сделать привязку 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)));
Другие вопросы по тегам