Yes, an IOC container. I know that you shouldn’t do it. And if you do it, you’ll get it wrong. But what if there’s no other option out there than rolling up your sleeves and get your hands dirty?
I’ve mentioned that most of my professional development activities circle around Windows Mobile and Windows CE devices using the .NET Compact Framework. Some time ago when I wanted to take the plunge and start using a proper, established and tested IOC container from out there instead of my hand-rolled manual IOC, I found that none of them supported the .NET Compact Framework. Except one: Ninject.
As happy as I was to find at least one IOC container that I could finally swap out my manual IOC for, as sad I became when I could not use it all the way: I tracked down a devious failure in my application as being a bug in the way Ninject handles attributes. This was a showstopper. Remo Gloor of the Ninject project commented on my Stack Oveflow question that he added the issue to their backlog. But I couldn’t wait for the next Ninject version to happen and I didn’t ever want to go back to the manual IOC I had been doing up until lately – the code base size just wasn’t small enough any more.
So, I decided to look into this myself. I decided that I only needed something that could do these basic things:
- Automatically resolve types bound to themselves. Think concrete types.
- Automatically resolve types bound to other types. Think abstract types vs concrete types.
- Either create a new instance every time a type is resolved, or resuse one single instance. Think transient and singleton lifetimes or scopes.
- Present a simple interface.
What were the minimum required amount of building blocks required to fulfill my basic requirements?
I found Ken Egozi’s blog post It’s My Turn To Build An IoC Container In 15 Minutes and 33 Lines which referenced Ayende Rahien’s prequel Building an IoC container in 15 lines of code. By judging their writings in those posts, neither of the containers was actually meant to be used. But I had to make something that could be used. Enter the WeeContainer.
Functionality-wise, I started with Ken Egozi’s code and added the basic feature of choosing a scope (lifetime) for a binding (defines the source type that maps to a target type): transient or singleton. I had no more lifetime needs than that in the applications I was working on.
Presenting a simple interface was just an issue of choosing proper names and not exposing anything that wasn’t needed. I settled on the following idea:
public sealed class WeeContainer
{
public BindingScope DefaultScope { get; set; }
public void Bind<TTarget>() { ... }
public void Bind<TTarget>(BindingScope scope) { ... }
public void Bind<TTarget, TSource>() { ... }
public void Bind<TTarget, TSource>(BindingScope scope) { ... }
public TTarget Resolve<TTarget>() { ... }
}
I thought of adding support for a more fluent interface, since “all the other ones do it”, to support this:
Bind<Target>().To<Source>().As(BindingScope.Singleton);
But I decided that I didn’t need it. I just find
Bind<Target, Source>(BindingScope.Singleton);
easier to both read and write.
Based on my current codebase I decided that the default scope should be transient. Setting a different scope at bind time shouldn’t change the default scope and changing the default scope after a binding has been made shouldn’t affect that binding’s scope, only the coming ones’:
[Test]
public void default_scope_is_transient_by_default()
{
var sut = new WeeContainer();
var expected = BindingScope.Transient;
var actual = sut.DefaultScope;
Assert.AreEqual(expected, actual);
}
[Test]
public void default_scope_is_not_changed_when_bind_has_other_scope()
{
var sut = new WeeContainer();
var expected = sut.DefaultScope;
Assert.AreNotEqual(expected, BindingScope.Singleton);
sut.Bind<List<int>>(BindingScope.Singleton);
var resolved = sut.Resolve<List<int>>();
var actual = sut.DefaultScope;
Assert.AreEqual(expected, actual);
}
[Test]
public void changing_default_scope_does_not_change_previous_binds()
{
var sut = new WeeContainer();
Assert.AreEqual(sut.DefaultScope, BindingScope.Transient);
sut.Bind<IList<int>, List<int>>();
var resolved1 = sut.Resolve<IList<int>>();
sut.DefaultScope = BindingScope.Singleton;
var resolved2 = sut.Resolve<IList<int>>();
Assert.AreNotSame(resolved1, resolved2);
}
[Test]
public void changing_default_scope_changes_coming_binds()
{
var sut = new WeeContainer();
Assert.AreEqual(sut.DefaultScope, BindingScope.Transient);
sut.DefaultScope = BindingScope.Singleton;
sut.Bind<IList<int>, List<int>>();
var resolved1 = sut.Resolve<IList<int>>();
var resolved2 = sut.Resolve<IList<int>>();
Assert.AreSame(resolved1, resolved2);
}
Furthermore, if I introduce a bug in my code I want the code to fail fast and hard to make it easier to find it and minimize the risk that it makes damage to important data. Therefore early checks must be done to verify that types can actually be instantiated:
class NoPublicCtor : IDisposable
{
private NoPublicCtor() { }
public void Dispose()
{
}
}
[Test]
public void binding_to_type_without_public_ctor_throws()
{
var sut = new WeeContainer();
Assert.Throws<AgumentException>(() => sut.Bind<NoPublicCtor>());
Assert.Throws<AgumentException>(() => sut.Bind<NoPublicCtor>(BindingScope.Singleton));
Assert.Throws<AgumentException>(() => sut.Bind<IDisposable, NoPublicCtor>());
Assert.Throws<AgumentException>(() => sut.Bind<IDisposable, NoPublicCtor>(BindingScope.Singleton));
}
[Test]
public void binding_to_abstract_type_throws()
{
var sut = new WeeContainer();
Assert.Throws<AgumentException>(() => sut.Bind<IList>());
Assert.Throws<AgumentException>(() => sut.Bind<IEnumerable, IList>());
Assert.Throws<AgumentException>(() => sut.Bind<IList>(BindingScope.Singleton));
Assert.Throws<AgumentException>(() => sut.Bind<IEnumerable, IList>(BindingScope.Singleton));
}
I think the WeeContainer is good enough while waiting for Ninject to be fixed. I’ll look at the implementation details of WeeContainer in another post. All of the above with more tests, including NUnit dependencies, can be fetched from the Mercurial repository at https://joger.googlecode.com or cloned with hg clone https://joger.googlecode.com/hg/ joger.

