Использование NInject для связывания общего интерфейса с по умолчанию, если привязка для родового типа не задана

Представьте, что у меня есть следующие классы и интерфейсы:

public interface IService<T> { }

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

public class FooService : IService<Foo> { }

public class BarService : IService<Bar> { }

Тогда мне хотелось бы получить экземпляры из ядра следующим образом:

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

Можно ли установить привязки с помощью NInject (возможно, используя расширение Conventions), так что мне не нужно вручную связывать каждую возможную реализацию IService?

Ответ 1

Недавно я работал над чем-то похожим и придумал несколько более простое решение вашей проблемы (хотя и немного слабее).

Достаточно связать общую реализацию (DefaultService) с общим интерфейсом и конкретными реализациями (FooService, BarService) с конкретными интерфейсами. Когда вы запрашиваете конкретный экземпляр интерфейса, Ninject решает, определяете ли вы конкретную привязку. Если вы это сделали, он предоставит вам соответствующий экземпляр, иначе он попадет в общую привязку. Следующий код должен сделать трюк.

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

EDIT:

Концепция работает во всем Ninject, поэтому вы можете использовать его вместе с Extensions.Conventions. например определите следующее:

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

использовать соглашения для привязки сервисов:

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

и создайте и проверьте соответствующие службы:

Assert.IsInstanceOf<BarService>(kernel.Get<IService<Bar>>());
Assert.IsInstanceOf<FooService>(kernel.Get<IService<Foo>>());
Assert.IsInstanceOf<DefaultService<Dog>>(kernel.Get<IService<Dog>>());

Ответ 2

Я взял на себя смелость реорганизовать ответ из @cbp, так что он работает для новой сигнатуры IBindingGenerator в соглашениях Ninject v3. Это в значительной степени заменяет подпись метода Process() на подпись метода CreateBindings(), но я не тестировал это, поэтому у вас есть шанс немного подкорректировать его, если вы его используете.

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

Ответ 3

Я выяснил, как это сделать через пару часов возиться с NInject Convention GenericBindingGenerator.

Если кто-то заинтересован, я могу опубликовать его.

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