Using Ninject and binding a default implementation while avoiding the dreaded Service Locator anti-pattern
Asked Answered
L

1

7

Is it possible and/or a good idea to use Ninject (or any other IoC container, for that matter) to create a default binding for situations in which an appropriate implementation does not exist, and use this default binding rather than have to handle an ActivationException when either multiple bindings exist, or no bindings exist for a particular request?

I've been using Ninject's Factory and Conventions extension projects, but I'm wondering if they're masking a mistake I'm making at a more fundamental level, so I've created a test to illustrate what I want to do, as simply as I can:

Given the following:

public interface IWidget { }
public class DefaultWidget : IWidget { }
public class BlueWidget : IWidget { }

And the following xUnit test using FluentAssertions:

[Fact]
public void Unknown_Type_Names_Resolve_To_A_Default_Type()
{
    StandardKernel kernel = new StandardKernel();

    // intention: resolve a `DefaultWidget` implementation whenever the 
    // 'name' parameter does not match the name of any other bound implementation
    kernel.Bind<IWidget>().To<DefaultWidget>();
    kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>();

    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
}

I'm not sure exactly how going down this road, if it's even possible, would lead me to an unsavory implementation of the Service Locator pattern, other than it seems to be a warning given by those who have answered similar questions.

So, is this an abuse/misuse of IoC containers to do what I am asking it to do?

It seems like I should want to bind/resolve types with an IoC for all the explicit types I do have, so that part doesn't seem wrong to me. In real life, there would be many more explicit bindings like BlueWidget, as well as an unknown number of variations on the "RedWidget" string value coming in.

Generally speaking, it seems like the notion of a default implementation of some interface is a not-so-uncommon situation, so where would this mechanism for resolving requests be, if not within the IoC container's realm?

I also plan on using the factory pattern to create IWidget implementations. So far, I've been using the factories created automatically by Ninject.Extensions.Factory with custom instance providers and custom binding generators, but I can't get past this problem.

Would having more control over the factory implementation (in other words, use my own Factory, not the automatic factory from Ninject.Extensions.Factory) help? In the past, I've used Assembly reflection to find candidate types, and use Activation.CreateInstance() to create either the specific implementation I need or a default implementation, but this gets really cumbersome once those implementations have their own constructor-injected dependencies as Dependency-injection principles are applied to those implementations. Hence, my shift to IoC containers for a solution - but this is not working out like I would have hoped.

UPDATE 1 -- SUCCESS USING MY OWN FACTORY IMPLEMENTATION

I'm not really happy with this, because every time a new implementation of IWidget must be written, I'll have to crack open this factory and update it, too. In my example here, I'd have to add another line in the bindings, too - but that's where convention-based binding would come in, which I would plan on using to avoid having to constantly update the binding definitions.

Using this factory implementation,

public interface IWidgetFactory { IWidget Create(string name); }
public class WidgetFactory : IWidgetFactory 
{
    private readonly IKernel kernel;
    public WidgetFactory(IKernel kernel) { this.kernel = kernel; }

    public IWidget Create(string name)
    {
        switch (name)
        {
            case "Blue":
                return this.kernel.Get<IWidget>(typeof (BlueWidget).Name);
            default:
                return this.kernel.Get<IWidget>(typeof (DefaultWidget).Name);
        }
    }
}

I can get this test to pass:

[Fact]
public void WidgetBuilders_And_Customizers_And_Bindings_Oh_My()
{
    StandardKernel kernel = new StandardKernel();
    kernel.Bind<IWidget>().To<DefaultWidget>().Named(typeof(DefaultWidget).Name);
    kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof (BlueWidget).Name);
    kernel.Bind<IWidgetFactory>().To<WidgetFactory>().InSingletonScope();

    kernel.Get<IWidgetFactory>().Create("Blue")
        .Should().BeOfType<BlueWidget>();
    kernel.Get<IWidgetFactory>().Create("Red")
        .Should().BeOfType<DefaultWidget>();
}

It works, but it doesn't feel right, for the following reasons:

  1. I have to inject the IKernel into the IWidgetFactory
  2. For every new implementation of IWidget, the IWidgetFactory must be updated
  3. It seems like a Ninject extension should have been created for this scenario already

END OF UPDATE 1

What would you do in this situation, considering that the count of IWidget implementations is high, the anticipated range of "widget name" arguments is essentially infinite, and all unresolvable widget names should be handled with the DefaultWidget?

You need not read further, but if you're interested, here are the various tests I tried as I worked on this problem:

Here's the complete evolution of tests I went through:

[Fact]
public void Unknown_Type_Names_Resolve_To_A_Default_Type()
{
    StandardKernel kernel = new StandardKernel();

    // evolution #1: simple as possible
    //      PASSES (as you would expect)
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #2: make the only binding to a different IWidget
    //      FAILS (as you would expect)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #3: add the binding to `BlueWidget` back
    //      ACTIVATION EXCEPTION (more than one binding for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #4: make `BlueWidget` binding a *named binding*
    //      ACTIVATION EXCEPTION (more than one binding for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof (BlueWidget).Name);
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #5: change `Get<>` request to specifiy widget name
    //      PASSES (yee-haw!)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    //kernel.Get<IWidget>("BlueWidget").Should().BeOfType<BlueWidget>();

    // evolution #6: make `BlueWidget` binding *non-named*
    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>("BlueWidget").Should().BeOfType<BlueWidget>();

    // evolution #7: ask for non-existance `RedWidget`, hope for `DefaultWidget`
    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>();

    // evolution #8: make `BlueWidget` binding a *named binding* again
    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    //kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>();

    // evolution #9: remove `RedWidget` specification in Get<> request
    //      ACTIVATION EXCEPTION (back to **more than one** binding for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    //kernel.Get<IWidget>().Should().BeOfType<DefaultWidget>();
}
Linguistician answered 16/4, 2013 at 21:44 Comment(7)
You've asked a great question and have clearly put in lots of work and I'm very happy to +1. I personally am confused by the result of #8 and manually verified! Your next step is to go look in the tests for Named Bindings and either a) try to understand them to improve your question b) try to understand it so you can implement your solution using your own Binding Metadata (trust me it will be easy) but ideally you'll find out from the tests whether everything you're observing makes sense and then submit either a bug report or a Pull Request or make a change to your questionGazette
@Ruben, thanks for the comment - are these tests in the main Ninject GitHub repository? I can't find any test file like "NamedBindingsTests", although I did locate the BindingMetadata in the "Planning" folder.Linguistician
Dunno - was just guessing based on my previous explorations, 2s... seems github.com/ninject/ninject/blob/master/src/Ninject.Test/… is closest. Can't recall if there is a wiki, blog post or well trafficked SO answer about default naming behavior as you're asking but I am pretty sure it's somewhere (I personally do either all named or none named so it has never arisen as a concern)Gazette
There was this one but it's pretty old, and Remo Gloor's answer includes a call to .Binding.IsImplicit which doesn't seem to exist anymore. I've been looking on SO for anything including "Ninject Default Fallback Binding", etc, but not much has turned up of use. I've asked a more abstract question on programmers stackexchange to see if this is just a bad idea to start with.Linguistician
Looks like you're on a good track. I find often the best thing to do is to suss out how do write myself an extension method by reading the tests so I can get on with my work. Then in 6 months time I am usually in a position to rip my warty stuff out. I bet if your example was more concrete and/or you had to justify your reqs to yourself more you'd have talked yourself out of it by now! Sorry I can't actually help!Gazette
@Ruben I thought was helping to clarify the question by distilling it down the way I did. It's actually a more concrete example on my machine - I can post that as an update. The problem is, I don't understand the Ninject authors' intentions well enough to recognize the different between a bug and my misuse of the IoC.Linguistician
I was driving at the fact that it's not clear what you're really doing - how many Names are involved and why are you getting into that - is it really a set of objects per request? Per session? Per what? Blue and Red don't help in identifying a higher level solution. In all other respects, your question is at the right level of detail and consists of tests which one can paste into test projects which is ideal from the point of view of someone trying to solve a problem.Gazette
E
2

Don't know where this stacks up, but it works. You can not pass an named parameter in though, it only works with an empty parameter. In other words you can't do this:

var stuff1 = kernel.Get<IWidget>("OrangeWidget");

Unless OrangeWidget exists. Rather to get the default you need to do this:

var stuff2 = kernel.Get<IWidget>();

Here is an example:

IDictionary dic = new Dictionary<string, string>();

dic.Add("BlueWidget", "BlueWidget");
dic.Add("RedWidget", "RedWidget");

kernel.Bind<IWidget>().To<DefaultWidget>()
    .When(x => x.Service.Name != (string)dic[x.Service.Name]); 
kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget");

var stuff1 = kernel.Get<IWidget>("BlueWidget");
var stuff2 = kernel.Get<IWidget>();

This is a cool Ninject post you might be interested in...

I'd like to add something about Named parameters. This is based on this documentation. The Named parameter allows you to call out your bindings by name when you Get<> them, but it does not "default" to anything. So you would actually have to pass the name DefaultWidget to get that binding. This works:

kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget");
kernel.Bind<IWidget>().To<DefaultWidget>().Named("DefaultWidget");

var blueOne = kernel.Get<IWidget>("BlueWidget");
var defaultOne = kernel.Get<IWidget>("DefaultWidget");

If anyone can figure out how to implement a default, I'd be curious to know how, though I've never needed it. I'm a student of Ninject and really like it.

UPDATE:

I got it. Found a cool solution here.

I created a class to extend Ninject:

public static class NinjectExtensions
{
    public static T GetDefault<T>(this IKernel kernel)
    {
        return kernel.Get<T>(m => m.Name == null);
    }

    public static T GetNamedOrDefault<T>(this IKernel kernel, string name)
    {
        T result = kernel.TryGet<T>(name);

        if (result != null)
            return result;

        return kernel.GetDefault<T>();
    }
}

Here is your Unknown_Type method...

public static void Unknown_Type_Names_Resolve_To_A_Default_Type()
{
    StandardKernel kernel = new StandardKernel();

    IDictionary dic = new Dictionary<string, string>();

    dic.Add("BlueWidget", "BlueWidget");
    dic.Add("RedWidget", "RedWidget");

    kernel.Bind<IWidget>().To<DefaultWidget>().When(x => x.Service.Name != (string)dic[x.Service.Name]);
    kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget");

    // this works!
    var sup = kernel.GetNamedOrDefault<IWidget>("Not here");

    var stuff1 = kernel.Get<IWidget>("BlueWidget");
    var stuff2 = kernel.Get<IWidget>();
}
Earthy answered 10/9, 2013 at 16:16 Comment(4)
Interesting - your first example seems similar to my "evolution #5" (see my list of test evolutions at the bottom of my question), and I was able to get it to pass without additional binding conditions using When.(x => … like you've shown. Since I posted the question, I've begun to believe that I was abusing the capabilities of Ninject by attempting to insert that type-specific default fallback logic into the bindings, and I resorted to using a simple switch statement in my code to retrieve a DefaultWidget when it couldn't find the specific one I requested.Linguistician
It may be more of a general design question, and perhaps there are answers on Programmers or the like, since part of this question deals the capabilities of Ninject, and the other part makes you wonder if that logic should be coupled to any IoC container - Ninject or otherwise.Linguistician
I actually use an IOC Container and put all my 'new' statements in the bind modules. Then I call everything like this IOCCOntainer.instance.Get<typeofobject>(). I find it really organizes my code and I can switch to test mode by pointing to a different bind module to instance my mocks. I do mock all my services and such and some people feel 3rd party mocks are better. This way my first pass of tests are easy. Ninject does this easily so that's why I like it and why I hang around sites like Stackoverflow to learn more tricks. Your question was interesting because I never use defaults.Earthy
+1 for the reference to your source for your extension method.Linguistician

© 2022 - 2024 — McMap. All rights reserved.