Ninject default contextual binding
Asked Answered
G

4

19

I have an interface with a few different concrete implementations. I am trying to give Ninject a default to use and only use the other implementation if a name matches. For instance, I have the following bindings.

Bind<ISomething>().To<DefaultSomething>()
Bind<ISomething>().To<OtherSomething>().Named("55abd8b8-097f-4e1c-8d32-95cc97910604");

What I would like is if the Named section doesn't match, to use the DefaultSomething implementation. When I pass in the explicitly bound guid, it works fine. When I pass in any other guid I get the "No matching bindings are available" exception.

Bind<ISomething>().To<OtherSomething>().Named("55abd8b8-097f-4e1c-8d32-95cc97910604");
Bind<ISomething>().To<DefaultSomething>()

Bind<ISomething>().To<DefaultSomething>()
Bind<ISomething>().To<OtherSomething>().When(ctx => ctx.Service != null && ctx.Service.Name == "55abd8b8-097f-4e1c-8d32-95cc97910604");

I have also tried using .When to check the binding and I have tried reversing the order like below however I am never able to bind unless I pass in the Guid that is explicitly named.

This article seems to indicate that default bindings work, so I must be doing something wrong. Any suggestions?


Edit: Here is a complete example showing the problem I am trying to solve. The desired behavior is for kernel.Get<INumber>("Three").Write() to return "Unknown Number"

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ninject;

namespace NinjectTest
{
    interface INumber
    {
        string Write();
    }

    class UnknownNumber : INumber
    {
        public string Write()
        {
            return "Unknown Number";
        }
    }

    class One : INumber
    {
        public string Write()
        {
            return "1 = One";
        }
    }

    class Two : INumber
    {
        public string Write()
        {
            return "2 = Two";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            StandardKernel kernel = new StandardKernel();
            kernel.Bind<INumber>().To<UnknownNumber>();
            kernel.Bind<INumber>().To<One>().Named("One");
            kernel.Bind<INumber>().To<Two>().Named("Two");

            Console.WriteLine(kernel.Get<INumber>("One").Write());
            Console.WriteLine(kernel.Get<INumber>("Two").Write());
            Console.WriteLine(kernel.Get<INumber>("Three").Write());

            Console.ReadLine();
        }
    }
}
Gipon answered 13/5, 2011 at 20:36 Comment(0)
L
22

You completely missunderstood named bindings:

Giving a binding a name is NOT a condition. You will still get all of them when requesting them without a constraint. Adding a name changes absolutely nothing on its own.

Requesting an instance using a name adds the constraint:

only bindings whose name matches the given one shall be returned

In your case, you gave me an instance whose binding's name is "three". And you expect it to return UnknownNumber, which does not even have a name.

This can be achieved by either

  1. passing a parameter and adding conditions to the bindings that check if the parameter matches, or
  2. passing a constraint that fits the name or the unnamed instance and declare the unnamed one implicit.

Option 1:

public class CustomerIdParameter : Parameter
{
    public CustomerIdParameter(string id) : base("CustomerId", (object)null, false)
    {
        this.Id = id;
    }
    public string Id { get; private set; }
}

kernel.Bind<ISomething>().To<Default>();
kernel.Bind<ISomething>().To<Other>()
      .When(r => r.Parameters.OfType<CustomerIdParameter>()
                             .Single().Id == "SomeName");

kernel.Get<IWeapon>(new CustomerIdParameter("SomeName")).ShouldBeInstanceOf<Sword>();

I leave it up to you to write the extension methods to make the definition and resolve easier.

Option 2:

Bind<ISomething>().To<Default>().Binding.IsImplicit = true;
Bind<ISomething>().To<Other>().Named("SomeName")

public static T GetNamedOrDefault<T>(this IKernel kernel, string name)
{
    return kernel.Get<T>(m => m.Name == null || m.Name == name);
}

But honestly I think what you want to do doesn't seem to be a proper design:

  1. Keep your access to the kernel to an absolute minimum. What you're doing here is a ServiceLocator-like usage of Ninject.
  2. If no binding is available for an expected instance, I'd rather expect an exception than using a default instance because this is a bug.
Lipoid answered 13/5, 2011 at 23:1 Comment(12)
Named bindings are not a condition? Is the documentation on the Ninject wiki wrong when they say "Named bindings are the simplest (and most common) form of conditional binding" (github.com/ninject/ninject/wiki/Contextual-Binding) and then they go on to use it in the same way I am using in my example, except without a default fallback if no name matches.Gipon
"If no binding is available for an expected instance I'd rather expect an exception than using a default instance because this is a bug." This doesn't make sense so me. If I have hundreds of different clients, and only a handful use something other than a default implementation, it makes a lot more sense to add a default implementation and only override it when it needs to be different.Gipon
I think Ninject is close enough to a factory for me to use (or abuse) it as one. I'll switch back to a factory for this, and use Ninject for what it's meant for. This stackoverflow question was quite useful in finding my problem: #282906Gipon
I said adding a name is just metadata and no condition. The constraint added by the request does the selection in this case. And if your constraint is that you want an instance of the binding whose name is three you are never getting the no name one.Lipoid
Asking for Three and expecting UnknownNumber is a misconfigured system for me. Why not just ask for the default implementation in this case? e.g. in your first example ask for Guid.EmptyLipoid
And yes I know the docu does not show that the behavior is handled differently if used with constraints or with conditions. I wish a day had more than 24h. I will improve the docu one day when its priority gets to the top of my todo list. But until then the community has to do this.Lipoid
"e.g. in your first example ask for Guid.Empty": In my case all of my entities have a guid associated with them, and I do not know if there is a custom implementation or not. I was trying to rely on Ninject to determine whether there was a custom implementation provided or to just use the default binding based on the client id passed. This would have allowed me to add custom implementations and map them using Ninject without having to modify any of the underlying code. It felt like an elegant solution compared to creating a factory to handle it.Gipon
Elegant would be to have a field that tells you the strategy to use, one of which is the default one. This way you can use one of the existing ones for a new entity or change it for an existing one without having to change your code! Your implementation requires a new build for every entity that does not use the default strategy. But if you want to go this way Ninject allows it. See my post. But it still seams to be a design flaw to me.Lipoid
I think there might be a communication problem here. My implementation only requires adding a new implementation and updating the Ninject module to bind it to a guid. Those Guids are pulled from the database and associated with clients, they are not hard coded in the application.Gipon
No, I never thought they are hardcoded other than in the binding but still you have a problem: e.g a uses impl x later you add b that shall reuse impl x. becouse you use a unique guid you have tho change yout bindings and redeploy. while I would only add it to the db. Istop trying to convince you now as I don't have the time. Stay with you design if you are happy. And use one of the two options I told you in my answer to get Ninject to do as you want.Lipoid
Understood. In this case the chance of there being any reuse is extremely small. These are used for integration into client systems. For instance, one of the real examples I am using this for is our authentication service. The vast majority of our users will use the default auth provider which uses our own database. One of our clients has their own SSO provider, so we created a custom AuthProvider which instead of checking our database, will check with the client's SSO service. No other clients will re-use this custom SSO provider.Gipon
Ok added examples of the two options I mentioned.Lipoid
L
6

You can also simply add a condition for your binding to not have a condition, like so:

kernel.Bind<IObject>().To<Object1>().When(
           x => x.ParentContext != null && !x.ParentContext.Binding.IsConditional)
          .InRequestScope();

kernel.Bind<IObject>().To<Object2>().InRequestScope()
          .Named("WCFSession");

When doing a standard Inject without a Name specified, the first binding will be used. When specifying a name, the named binding will be used. It's not the prettiest solution, but it works.

Liebermann answered 24/6, 2013 at 19:50 Comment(0)
P
5

It's quite possible to do this in Ninject, it just doesn't happen to be the way the resolution behaves by default. The IKernel.Get<T> extension does not ask for the "default" binding, it asks for any binding; in other words it does not apply any constraints. If there is more than one matching binding then it throws an exception to that effect.

Try out these two extension methods:

static class KernelExtensions
{
    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 namedResult = kernel.TryGet<T>(name);
        if (namedResult != null)
            return namedResult;
        return kernel.GetDefault<T>();
    }
}

The first one gets the "default" binding - i.e. whichever one you've bound that has no name. The second one tries to get a named binding, but if it doesn't find that, then it reverts to the default.


Of course, Remo is not wrong either; you should avoid using Ninject or any other container this way unless you have a particularly good reason to. This is the Service Locator (anti-)pattern, not true dependency injection. You should be using the When syntax for conditional bindings, either using complex conditions or just decorating the classes which need special bindings, i.e.:

Bind<IFoo>().To<SpecialFoo>().WhenInjectedInto<ClassThatNeedsSpecialFoo>();

or...

Bind<IFoo>().To<SpecialFoo>().WhenMemberHas<SpecialAttribute>();

class InjectedClass
{
    public InjectedClass([Special]IFoo) { ... }
}

That is the right way to handle default and conditional bindings. Named bindings are really only useful when you're trying to implement a factory pattern and you want to wrap the IoC container in your custom factory. That's OK, but please use it sparingly, as you end up throwing away many/most of the benefits of dependency injection that way.


Alternatively, you could actually implement your own activation behaviour and use it to override the default in Ninject - everything is modular and gets shoved into the "Components" collection. But this is not for the faint of heart, so I don't plan on including a detailed tutorial here.

Profusive answered 14/5, 2011 at 2:55 Comment(4)
Thanks for the answer. The GetNamedOrDefault extension seems to be very close to what I was originally looking for, but I was expecting the conditional logic to be in the Module with the binding, not in the kernel. Now I just have to figure out whether I go with this, or implement a factory to return the custom types. But that is a topic for another question.Gipon
@Timothy: Some of the conditional logic is in the module. There are two parts to a condition, the data and the test. With named bindings, you're only declaring data, so you need to execute the test when you do resolution. The only way to eliminate any conditional logic or metadata checks in the resolution itself is to make your data an aspect of the binding context itself, such as an attribute or class type, which you do with the When extension.Profusive
Then why was I getting the same error when using .When(ctx => ctx.Service != null && ctx.Service.Name == <guid>); Intuitively it seems like that should have accomplished what I was looking for, but I am clearly not understanding something about the .When clause in the bindings. I was able to successfully bind when the Guid matched, but it said it could not bind even though I have a generic Bind without any conditional.Gipon
@Timothy: That's not a valid condition. You're confusing the name of the service with the name of the binding. The Service.Name is IFoo in this answer or ISomething in your answer. If you want to make use of conditional bindings then you need to use the library correctly, as a dependency-injection system; if you use it as a factory/service-locator then there is no such thing as a "default", just as there is no default in any valid factory implementation. You shouldn't get an error mind you, you'll just always get the default service no matter what arguments you supply to Get.Profusive
C
0

I checked the ParaSwarm's solution and it worked for a simple test project. But in a real project his solution didn't fit. I managed to solve it by the following code:

const string specificServiceName = "For" + nameof(SpecificService);
kernel.Bind<IService>()
    .To<DefaultService>()
    .When(x => x.Constraint == null || !x.Constraint(new BindingMetadata { Name = specificServiceName }))
    .InTransientScope();

kernel.Bind<IService>()
    .To<SpecificService>()
    .InTransientScope()
    .Named(specificServiceName);

PS: my answer does not solve the author's problem, but it can help someone with searching similar problem (as ParaSwarm's answer helps me)

Comose answered 22/7, 2022 at 11:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.