IoC comparison: Autoregistration in Ninject
The Ninject IoC container has an extension that does autoregistration, or "Convention based binding" as they call it. I have looked at basic IoC in Ninject before, and I quite like it. Autoregistration is not a third party add-on, but is part of the ninject source tree here on github. Ninject autoregistration does not work in quite the same vein as Windsor or Unity's fluent interfaces, but it feels like the "Ninject way", i.e. similar to the other parts of ninject such as using classes as modules.
The simple case is this:
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.BindWith<DefaultBindingGenerator>();
});
BusinessProcess bp = kernel.Get<BusinessProcess>();
In this code we ask the kernel to scan, and in that scan specify an assembly (other versions of this method provide options such as the calling assembly, or a list of assemblies) and supply a Binding generator class to create the bindings for types.
The DefaultBindingGenerator class simply binds classes to interfaces using the convention that the interface has the same name, with "I" prepended. So it will map the class Foo to IFoo and that's it.
I wasn't happy with that, and it wasn't sufficient for these tests, so I wrote my own binding generator. You just have to make type that implements IBindingGenerator, which is an interface which has only one method – Process, which is called for every type that the scanner finds. This task turned out to be fairly simple and fun. The code is here. I'm fairly happy with the rules that I came up with: if a type has interfaces, bind it to those interfaces. If not, bind it to itself. System interfaces (e.g. IDisposable) are ignored. Why did I bother to bind a type to itself, given that Ninject will do auto-resolution and resolve the type even if it is not registered? So that it can be registered as a singleton or with other configuration.
These rules work for my tests and are fairly general, and I could see how they could be customised further if need be; e.g. give the binding generator predicates to filter registrations or list of types to be excluded or registered in certain ways.
Note that the interface is not fluent at all. I made all registrations as singletons as follows:
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.BindWith<NinjectServiceToInterfaceBinder>();
scanner.InSingletonScope();
});
All of these methods on the scanner return void and so are not designed for chaining. The custom binding generator that I wrote has no say in what is a singleton - fair enough, that is a seperate concern to binding. The InSingletonScope method does not take any parameters - it applies to everything that the scanner finds. There are also methods InTransientScope, InThreadScope, and InRequestScope; which are similar. This does raise the question of how you mix and match; making some objects singletons and others not. For the particular scenario, I was able to register everything except BusinessProcess as singleton, and leave BusinessProcess unregistered. It was then auto-resolved as Transient by default. This won't work for every case.
You can also accomplish the same by doing two or more scans, and specifying different options on each of them. This seems like a duplication of code and runtime effort, but it works and is shown as an alternative approach in the code below. However it has the same potential for error as the Include predicates on Unity's fluent interface – you have two different predicates that should be equal and opposite, but nothing is forcing them to remain synchronised, and misconfurations may not be noticed due to automatic resolution.
In summary the Autoregistration in Ninject follows the Ninject way, may require some custom classes, but this is not too hard. It's not as flexible as some, but it works well enough.
The code for the test is maintained in the github repository here and is as is below
namespace IoCComparison.AutoregisterTests
{
using System.Linq;
using AutoregisteredClasses.Interfaces;
using AutoregisteredClasses.Services;
using AutoregisteredClasses.Validators;
using Ninject;
using Ninject.Extensions.Conventions;
using NUnit.Framework;
using IoCComparison.AutoregisterTests.NinjectExtensions;
/// <summary>
/// Need https://github.com/ninject/ninject.extensions.conventions
/// </summary>
[TestFixture]
public class NinjectTest
{
[Test]
public void CanMakeBusinessProcessWithDefaultBindingGenerator()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.BindWith<DefaultBindingGenerator>();
});
BusinessProcess bp = kernel.Get<BusinessProcess>();
Assert.IsNotNull(bp);
}
[Test]
public void CanMakeBusinessProcessWithServiceToInterfaceBinder()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.BindWith<NinjectServiceToInterfaceBinder>();
});
BusinessProcess bp = kernel.Get<BusinessProcess>();
Assert.IsNotNull(bp);
}
[Test]
public void CanMakeSingletonInstance()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.BindWith<NinjectServiceToInterfaceBinder>();
scanner.InSingletonScope();
});
BusinessProcess businessProcess1 = kernel.Get<BusinessProcess>();
BusinessProcess businessProcess2 = kernel.Get<BusinessProcess>();
Assert.AreSame(businessProcess1, businessProcess2);
}
[Test]
public void CanMakeTransientInstance()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.BindWith<NinjectServiceToInterfaceBinder>();
scanner.InTransientScope();
});
BusinessProcess businessProcess1 = kernel.Get<BusinessProcess>();
BusinessProcess businessProcess2 = kernel.Get<BusinessProcess>();
Assert.AreNotSame(businessProcess1, businessProcess2);
}
[Test]
public void CanMakeTransientInstanceWithSingletonDependencies()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
// exclude the type 'BusinessProcess' from scanning and singleton
// NInject will auto-resolve it as transient
// I don't know how the reverse would be accomplished
scanner.Where(t => t != typeof(BusinessProcess));
scanner.BindWith<NinjectServiceToInterfaceBinder>();
scanner.InSingletonScope();
});
BusinessProcess businessProcess1 = kernel.Get<BusinessProcess>();
BusinessProcess businessProcess2 = kernel.Get<BusinessProcess>();
Assert.AreNotSame(businessProcess1, businessProcess2);
Assert.AreSame(businessProcess1.CustomerService, businessProcess2.CustomerService);
Assert.AreSame(businessProcess1.OrderService, businessProcess2.OrderService);
}
[Test]
public void CanMakeTransientInstanceWithSingletonDependenciesTwoScans()
{
IKernel kernel = new StandardKernel();
// scan for types that are registered as singletons
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.Where(t => t != typeof(BusinessProcess));
scanner.BindWith<NinjectServiceToInterfaceBinder>();
scanner.InSingletonScope();
});
// scan for types that are registered as transient
kernel.Scan(scanner =>
{
scanner.From(typeof(BusinessProcess).Assembly);
scanner.Where(t => t == typeof(BusinessProcess));
scanner.BindWith<NinjectServiceToInterfaceBinder>();
scanner.InTransientScope();
});
BusinessProcess businessProcess1 = kernel.Get<BusinessProcess>();
BusinessProcess businessProcess2 = kernel.Get<BusinessProcess>();
Assert.AreNotSame(businessProcess1, businessProcess2);
Assert.AreSame(businessProcess1.CustomerService, businessProcess2.CustomerService);
Assert.AreSame(businessProcess1.OrderService, businessProcess2.OrderService);
}
[Test]
public void CanGetAllValidators()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(IValidator).Assembly);
scanner.WhereTypeInheritsFrom<IValidator>();
scanner.BindWith<NinjectServiceToInterfaceBinder>();
});
var validators = kernel.GetAll<IValidator>();
Assert.IsNotNull(validators);
Assert.AreEqual(3, validators.Count());
}
[Test]
public void CanFilterOutValidatorRegistrations()
{
IKernel kernel = new StandardKernel();
kernel.Scan(scanner =>
{
scanner.From(typeof(IValidator).Assembly);
scanner.WhereTypeInheritsFrom<IValidator>();
// excluding the FailValidator should leave 2 of them
scanner.Where(t => t != typeof(FailValidator));
scanner.BindWith<NinjectServiceToInterfaceBinder>();
});
var validators = kernel.GetAll<IValidator>();
Assert.IsNotNull(validators);
Assert.AreEqual(2, validators.Count());
}
}
}
My custom binding generator is as follows:
public class NinjectServiceToInterfaceBinder : IBindingGenerator
{
public void Process(Type type, Func<IContext, object> scopeCallback, IKernel kernel)
{
if (type.IsInterface || type.IsAbstract)
{
return;
}
IList<Type> interfaceTypes = type.GetInterfaces()
.Where(t => ! t.IsSystemType()).ToList();
if (interfaceTypes.Count > 0)
{
foreach (Type interfaceType in interfaceTypes)
{
kernel.Bind(interfaceType).To(type).InScope(scopeCallback);
}
}
else
{
// if the type has no interfaces - bind to self
kernel.Bind(type).To(type).InScope(scopeCallback);
}
}
1 Comment
Jeff said
Brilliant post! I'm trying to implement my own custom IBindingGenerator with Ninject v3. The interface has changed from the Process() method, as I'm sure you're aware, to:
public IEnumerable<IBindingWhenInNamedWithOrOnSyntax<object>> CreateBindings(Type type, IBindingRoot bindingRoot)
In your post, "...have to make type that implements IBindingGenerator, which is an interface which has only one method – Process, **which is called for every type that the scanner finds**..." really made it click for me, so thanks for spelling it out.