Using NInject to bind a generic interface, with a default if a binding for the generic type is not set
Asked Answered
L

3

10

Imagine I have the following classes and interfaces:

public interface IService<T> { }

public class DefaultService<T> : IService<T> { }

public class FooService : IService<Foo> { }

public class BarService : IService<Bar> { }

I would then like to be able to get instances from the Kernel like this:

Kernel.Get<IService<Foo>>();  // Should return FooService
Kernel.Get<IService<Bar>>();  // Should return BarService
Kernel.Get<IService<Dog>>();  // Should return DefaultService
Kernel.Get<IService<Cat>>();  // Should return DefaultService
Kernel.Get<IService<Giraffe>>();  // Should return DefaultService

Is it possible to setup bindings using NInject (possibly using the Conventions extension), so that I don't have to manually bind every single possible implementation of IService?

Lousy answered 20/5, 2011 at 1:49 Comment(0)
D
12

I've been working on something similar recently and came up with somewhat simpler solution of your problem (although a bit weaker).

What should suffice is to bind a generic implementation (DefaultService) to the generic interface, and concrete implementations (FooService, BarService) to the concrete interfaces. When you ask for a concrete instance of the interface, Ninject resolves whether you defined the concrete binding. If you did, it gives you the appropriate instance, otherwise it falls through to the generic binding. The following code should do the trick.

var kernel = new StandardKernel();
kernel.Bind(typeof(IService<>)).To(typeof(DefaultService<>));
kernel.Bind<IService<Foo>>().To<FooService>();
kernel.Bind<IService<Bar>>().To<BarService>();

EDIT:

The concept works throughout the whole Ninject, so you can use it along with Extensions.Conventions as well. e.g. define the following:

public class Foo{}
public class Bar{}
public class Dog{}

public interface IService<T>{}
public class DefaultService<T> : IService<T>{}
public class FooService : IService<Foo>{}
public class BarService : IService<Bar>{}

use conventions to bind the services:

kernel.Bind(x => x.FromThisAssembly()
                  .SelectAllClasses()
                  .InheritedFrom(typeof(IService<>))
                  .BindSingleInterface());

and create and check the appropriate services:

Assert.IsInstanceOf<BarService>(kernel.Get<IService<Bar>>());
Assert.IsInstanceOf<FooService>(kernel.Get<IService<Foo>>());
Assert.IsInstanceOf<DefaultService<Dog>>(kernel.Get<IService<Dog>>());
Denoting answered 26/7, 2012 at 14:19 Comment(5)
Looks good, but what version of NInject are you using? I'm still using 2.2 and I get the error "More than one matching bindings are available." Hopefully they have fixed this in version 3.Lousy
I'm using v3 so it's probably an added feature - haven't realised that when I wrote the answer.Denoting
I should also point out that this doesn't quite fulfill the requirements of my question because you still have to manually bind each of the service classes.Lousy
As I pointed out at the beginning of my answer, my original solution was weaker in that you had to bind the default service and specific services manually, but it worked in the sense that if you haven't specified a binding for some closed generic, it automatically used the open generic one (DefaultService). I have presented that solution because I haven't used Conventions before. I have now read something on how to use Conventions and presented a solution, hope that helps.Denoting
Nice! Time for me to upgrade to v3 I guess.Lousy
M
2

I took the liberty of refactoring the answer from @cbp, so that it works for the new IBindingGenerator signature in Ninject v3 conventions. It's pretty much replacing the Process() method signature with the CreateBindings() method signature, but I didn't test this, so there's a chance you'll have to tweak it a bit if you use it.

/// <summary>
/// Creates bindings on open generic types.
/// This is similar to the out-of-the-box 
/// <see cref="GenericBindingGenerator" />, 
/// but allows a default class to be
/// specified if no other bindings can be found. 
/// See the test case for usages.
/// </summary>
public class GenericBindingGeneratorWithDefault : IBindingGenerator
{
    private static readonly Type TypeOfObject = typeof(object);
    private readonly Type _contractType;
    private readonly Dictionary<Type, Type> _cachedBindings;
    private readonly Type _defaultType;

    public GenericBindingGeneratorWithDefault(Type contractType, Type defaultType)
    {
        if (!(contractType.IsGenericType || contractType.ContainsGenericParameters))
            throw new ArgumentException("The contract must be an open generic type.", 
                "contractType");

        _cachedBindings = new Dictionary<Type, Type>();
        _contractType = contractType;
        _defaultType = defaultType;
    }

    /// <summary>
    /// Creates the bindings for a type.
    /// </summary>
    /// <param name="type">The type for which the bindings are created.</param>
    /// <param name="bindingRoot">The binding root that is used to create the bindings.</param>
    /// <returns>
    /// The syntaxes for the created bindings to configure more options.
    /// </returns>
    public IEnumerable<IBindingWhenInNamedWithOrOnSyntax<object>> CreateBindings(Type type, IBindingRoot bindingRoot)
    {
        if (type == null) throw new ArgumentNullException("type");
        if (bindingRoot == null) throw new ArgumentNullException("bindingRoot");

        if (type.IsInterface || type.IsAbstract) yield break;

        if (type == _defaultType)
        {
           yield return bindingRoot.Bind(_contractType).ToMethod(
               ctx =>
                   {
                       Type requestedType = ctx.Request.Service;
                       Type resolution = _cachedBindings.ContainsKey(requestedType)
                                             ? _cachedBindings[requestedType]
                                             : _defaultType.MakeGenericType(ctx.GenericArguments);
                       return ctx.Kernel.Get(resolution);
                   });
        }
        else
        {
            Type interfaceType = ResolveClosingInterface(type);
            if (interfaceType != null)
            {
               yield return bindingRoot.Bind(type).To(_cachedBindings[interfaceType]);
            }
        }
    }

    /// <summary>
    /// Resolves the closing interface.
    /// </summary>
    /// <param name="targetType">Type of the target.</param>
    /// <returns></returns>
    private Type ResolveClosingInterface(Type targetType)
    {
        if (targetType.IsInterface || targetType.IsAbstract) return null;

        do
        {
            Type[] interfaces = targetType.GetInterfaces();
            foreach (Type @interface in interfaces)
            {
                if ([email protected]) continue;

                if (@interface.GetGenericTypeDefinition() == _contractType)
                {
                    return @interface;
                }
            }
            targetType = targetType.BaseType;
        } while (targetType != TypeOfObject);

        return null;
    }
}
Machute answered 7/3, 2013 at 6:22 Comment(0)
L
1

I figured out how to do this after a couple of hours messing around with NInject Convention's GenericBindingGenerator.

If anyone is interested I can post it.

Update:

/// <summary>
/// Creates bindings on open generic types.
/// This is similar to the out-of-the-box <see cref="GenericBindingGenerator" />, but allows a default class to be
/// specified if no other bindings can be found. See the test case for usages.
/// </summary>
public class GenericBindingGeneratorWithDefault : IBindingGenerator
{
    private static readonly Type TYPE_OF_OBJECT = typeof (object);
    private readonly Type _contractType;
    private Dictionary<Type, Type> _cachedBindings = new Dictionary<Type, Type>();
    private readonly Type _defaultType;

    public GenericBindingGeneratorWithDefault(Type contractType, Type defaultType)
    {
        if ( !( contractType.IsGenericType || contractType.ContainsGenericParameters ) )
        {
            throw new ArgumentException( "The contract must be an open generic type.", "contractType" );
        }
        _contractType = contractType;
        _defaultType = defaultType;
    }

    /// <summary>
    /// Processes the specified type creating kernel bindings.
    /// </summary>
    /// <param name="type">The type to process.</param>
    /// <param name="scopeCallback">the scope callback.</param>
    /// <param name="kernel">The kernel to configure.</param>
    public void Process( Type type, Func<IContext, object> scopeCallback, IKernel kernel )
    {
        if (type == _defaultType)
        {
            kernel.Bind(_contractType).ToMethod(
                ctx =>
                {
                    var requestedType = ctx.Request.Service;
                    var resolution = _cachedBindings.ContainsKey(requestedType)
                                        ? _cachedBindings[requestedType]
                                        : _defaultType.MakeGenericType(ctx.GenericArguments);
                    return ctx.Kernel.Get(resolution);
                });
        }
        else
        {
            Type interfaceType = ResolveClosingInterface(type);
            if (interfaceType != null)
            {
                _cachedBindings[interfaceType] = type;
            }
        }
    }

    /// <summary>
    /// Resolves the closing interface.
    /// </summary>
    /// <param name="targetType">Type of the target.</param>
    /// <returns></returns>
    public Type ResolveClosingInterface( Type targetType )
    {
        if ( targetType.IsInterface || targetType.IsAbstract )
        {
            return null;
        }

        do
        {
            Type[] interfaces = targetType.GetInterfaces();
            foreach ( Type @interface in interfaces )
            {
                if ( [email protected] )
                {
                    continue;
                }

                if ( @interface.GetGenericTypeDefinition() == _contractType )
                {
                    return @interface;
                }
            }
            targetType = targetType.BaseType;
        } while ( targetType != TYPE_OF_OBJECT );

        return null;
    }
}
Lousy answered 20/5, 2011 at 6:47 Comment(3)
You downvote me then expect me to help you out with your problems? Give me a break. Mess around with GenericBindingGenerator like I did.Lousy
I haven't downloaded you and am interested in you solution :) In the meantime I will 'Mess around with GenericBindingGenerator' and post my findings...Daugava
I think you have a bug there but didn't test it: if in your assembly processing you encounter first the default target then _cachedBindings might not have got populated with the non-default target and you will wrongly return the default one.Bedesman

© 2022 - 2024 — McMap. All rights reserved.