Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ninject Bind When Ancestor Of Type T

I've got a dependency chain that looks roughly like this:

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 { ... }

Up until now, I've been using conventions based bindings, but that isn't good enough anymore because I've got more than one implementation for IFtpSettings. So I decided to use some contextual bindings. At first blush kernel.Bind<>().To<>().WhenInjectedInto<>() looked promising, but that only helps at the first level, meaning that if I had a CarSalesFtpFileProvider and a MotorcycleSalesFtpProvider I could do this:

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

But it seems pretty stupid to create two concrete implementations of FtpFileProvider that really only differ on what settings I want them to use. I saw that there is a method called WhenAnyAnchestorNamed(string name). But this route requires me to put attributes and magic strings on my batch jobs, which I'm not thrilled about.

I also noticed that there is a plain old .When(Func<IRequest, bool>) method on binding statements, so I came up with this as my binding statements:

//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);
}

So if a constructor asks for IFtpSettings, we recurse up the request tree to see if any of the requested services/types in the chain match the provided type (CarSalesBatchJob or MotorcycleSalesBatchJob) and if so returns true. If we get all the way to the top of the chain, we return false.

Sorry for the long background explanation.

Here is my question: Is there any reason why I should not approach the problem this way? Is this considered bad form? Is there a better way of finding the ancestor request types? Should I restructure my classes/dependency chain to a more "agreeable" fashion?

like image 658
viggity Avatar asked Jun 06 '12 20:06

viggity


3 Answers

You should use request.Target.Member.ReflectedType instead of request.Service This is the implementation type.

Also WhenAnyAncestorNamed does not require Attributes. You can mark the bindings of your Jobs using the Named method.

like image 133
Remo Gloor Avatar answered Nov 17 '22 20:11

Remo Gloor


My advice is to target the destination of the convention-breaking situation with your registration, not the IFtpSettings itself. For example, in a similar situation I would do the following:

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

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

This is about as straight-forward as it can get when it comes to explaining to other programmers how each Batch Job is instantiated. Instead of targeting the registration of ICommonSetting to try to handle each one-off, you handle each one-off in its own case.

To put it another way, imagine if these classes had two dependencies that needed to be altered in the IoC container. You'd have four lines of registration code in different places, but they'd all for the purpose of instantiating a MotorcycleSalesBatchJob, or CarSalesBatchJob, etc. If someone wanted to find out how the class was referenced, he'd have to hunt for any reference to a class (or a base class). Why not just write code that explains exactly how each of these are to be instantiated, all in one place?

The drawback to this (or at least what I've heard from others) is that if the constructor for either of these concrete classes change, then the code will break and you'll have to alter the registration. Well, to me, that's a positive, because I've already taken one step down this type of path with the IoC container changing based on some sort of state, I need to make sure I'm still retaining the expected behavior.

It gets even more fun when you think about the possibilities. You could do something like this:

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

All of the complexity of how things might work in different situations can be broken into separate classes, leaving it to the IoC container to supply the correct class in the right situation.

like image 24
Darren Avatar answered Nov 17 '22 21:11

Darren


This is not really an answer to your question, but you can solve your problem by writing a single class as follows:

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

In that case, your configuration would look like this:

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

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

Please note that in my experience, I found out that in most cases where you think you need context based injection, you actually have a flaw in your design. However, with the given information, it is impossible for me to say anything about this in your case, but you might want to take a look at your code. You might be able to refactor your code in such way that you actually don't need context based injection.

like image 35
Steven Avatar answered Nov 17 '22 21:11

Steven