Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

ninject

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?

like image 782
cbp Avatar asked May 20 '11 01:05

cbp


3 Answers

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>>());
like image 151
Jan Avatar answered Nov 08 '22 08:11

Jan


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;
    }
}
like image 20
Jeff Avatar answered Nov 08 '22 08:11

Jeff


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;
    }
}
like image 1
cbp Avatar answered Nov 08 '22 10:11

cbp