Ninject Bind When Ancestor Type T

У меня есть цепочка зависимостей, которая выглядит примерно так:

public class CarSalesBatchJob
{
    public CarSalesBatchJob(IFileProvider fileProvider)
    { ... }
}

public class MotorcycleSalesBatchJob
{
    public MotorcycleSalesBatchJob(IFileProvider fileProvider)
    { ... }
}    

public class FtpFileProvider : IFileProvider
{
    public FtpFileProvider(IFtpSettings settings)
    { ... }
}

public class CarSalesFtpSettings : IFtpSettings { ... }
public class MotorcycleSalesFtpSettings : IFtpSettings { ... }

До сих пор я использовал привязки, основанные на соглашениях, но это уже недостаточно хорошо, потому что у меня есть более одной реализации для IFtpSettings. Поэтому я решил использовать некоторые контекстные привязки. Сначала blush kernel.Bind<>().To<>().WhenInjectedInto<>() выглядел многообещающим, но это помогает только на первом уровне, а это значит, что если бы у меня были CarSalesFtpFileProvider и MotorcycleSalesFtpProvider, я мог бы это сделать:

kernel.Bind<IFtpSettings>().To<CarSalesFtpSettings>()
    .WhenInjectedInto<CarSalesFtpFileProvider>();
kernel.Bind<IFtpSettings>().To<MotorcycleSalesFtpSettings>()
    .WhenInjectedInto<MotorcycleSalesFtpFileProvider>();

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

Я также заметил, что существует простой старый метод .When(Func<IRequest, bool>) для операторов привязки, поэтому я придумал это как мои обязательные утверждения:

//at this point I've already ran the conventions based bindings code so I need to unbind
kernel.Unbind<IFtpSettings>();
kernel.Bind<IFtpSettings>().To<CarSalesFtpSettings>()
    .When(r => HasAncestorOfType<CarSalesBatchJob>(r));
kernel.Bind<IFtpSettings>().To<MotorcycleSalesFtpSettings>()
    .When(r => HasAncestorOfType<MotorcycleSalesBatchJob>(r));

// later on in the same class
private static bool HasAncestorOfType<T>(IRequest request)
{
    if (request == null)
        return false;

    if (request.Service == typeof(T))
        return true;

    return HasAncestorOfType<T>(request.ParentRequest);
}

Итак, если конструктор запрашивает IFtpSettings, мы рекурсируем дерево запроса, чтобы узнать, соответствует ли какая-либо из запрошенных услуг/типов в цепочке предоставленный тип (CarSalesBatchJob или MotorcycleSalesBatchJob), и если так возвращает true. Если мы добираемся до вершины цепочки, мы возвращаем false.

Извините за подробное объяснение.

Вот мой вопрос: есть ли какая-то причина, почему я не должен обращаться к проблеме таким образом? Является ли это плохой формой? Есть ли лучший способ поиска типов запросов предков? Должен ли я перестроить свою цепочку классов/зависимостей на более "приятный" способ?

Ответ 1

Вы должны использовать request.Target.Member.ReflectedType вместо request.Service. Это тип реализации.

Также WhenAnyAncestorNamed не требует атрибутов. Вы можете пометить привязки своих Рабочих мест, используя метод Named.

Ответ 2

Это не ответ на ваш вопрос, но вы можете решить свою проблему, написав один класс следующим образом:

private sealed class FtpFileProvider<TFileProvider>
     : FtpFileProvider
    where TFileProvider : IFileProvider
{
    public FtpFileProvider(TFileProvider settings)
        : base(settings) { }
}

В этом случае ваша конфигурация будет выглядеть так:

kernel.Bind<IFileProvider>()
    .To<FtpFileProvider<CarSalesFtpSettings>>()
    .WhenInjectedInto<CarSalesBatchJob>();

kernel.Bind<IFileProvider>()
    .To<FtpFileProvider<MotorcycleSalesFtpSettings>>()
    .WhenInjectedInto<MotorcycleSalesBatchJob>();

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

Ответ 3

Мой совет заключается в том, чтобы настроить целевую область ситуации, в которой вы нарушаете соглашение, с вашей регистрацией, а не с самим IFtpSettings. Например, в подобной ситуации я бы сделал следующее:

container.Register<CarSalesBatchJob>(() => {
    ICommonSetting myCarSpecificDependency = container.Resolve<CarSpecificDependency>();
    new CarSalesBatchJob(myCarSpecificDependency);
});

container.Register<MotorcycleSalesBatchJob>(() => {
    ICommonSetting myMotorcycleSpecificDependency = container.Resolve<MotorcycleSpecificDependency>();
    new MotorcycleSalesBatchJob(myMotorcycleSpecificDependency);
});

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

Другими словами, представьте, должны ли эти классы иметь две зависимости, которые необходимо было изменить в контейнере IoC. У вас было бы четыре строки регистрационного кода в разных местах, но все они были бы предназначены для создания экземпляра MotorcycleSalesBatchJob или CarSalesBatchJob и т.д. Если кто-то захотел узнать, как был указан класс, ему придется охотиться за любая ссылка на класс (или базовый класс). Почему бы просто не написать код, который точно объясняет, как каждый из них должен быть создан, все в одном месте?

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

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

container.Register<IProductCatalog>(() => {
    currentState = container.Resolve<ICurrentState>().GetTheState();
    if (currentState.HasSpecialPricing())
       return container.Resolve<SpecialPricingProductCatalog>();
    return container.Resolve<RegularPricingProductCatalog>();
});

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