Code Focused

How To Refactor for Dependency Injection, Part 5: A Look at 5 DI Containers

DI containers all serve a similar purpose, but with some differences in syntax and functionality. Ondrej Balas explains the differences between Ninject, Castle Windsor, Unity, StructureMap and Autofac.

In the series thus far, I've shown how the use of dependency injection (DI) containers can have a positive impact on the software you write. I have focused primarily on my container of choice, Ninject, but there are plenty of other options out there. The various containers are mostly equivalent but there are some differences in syntax and functionality. The aim of this article is to help you get started with containers you're not familiar with by highlighting some important differences, as well as showing the syntax of each container.

For each container I'll demonstrate binding, resolving a single concrete type that has a dependency on an interface and resolving multiple concrete types bound to one interface.

In the following examples I'll be using the classes and interfaces as shown in Listing 1.

Listing 1. An interface and some classes that will be referenced throughout this article.

public interface IDataRepository
{
  string GetData();
}
public class FakeDataRepository : IDataRepository
{
  public string GetData()
  {
    return "Fake Data";
  }
}
public class RealDataRepository : IDataRepository
{
  public string GetData()
  {
    return "Real Data";
  }
}
public class DataConsumer
{
  private readonly IDataRepository _dataRepository;

  public DataConsumer(IDataRepository dataRepository)
  {
    _dataRepository = dataRepository;
  }

  public void Consume()
  {
    _dataRepository.GetData();
  }
Ninject
The Ninject container object, known as a kernel, is created by instantiating a StandardKernel. Once instantiated, you'll be able to call the Bind method to configure the container, like this:
IKernel kernel = new StandardKernel();
kernel.Bind<IDataRepository>().To<FakeDataRepository>();

Ninject supports automatic self-binding of concrete types, so you won't need to explicitly configure those bindings for the container to resolve them. This means that even without specifying a binding, I can call the following code to get an instance of DataConsumer:

DataConsumer consumer = kernel.Get<DataConsumer>();

Ninject will inspect the constructor of DataConsumer and injects its dependencies to create an instance of it. This is called constructor injection.

Another feature of Ninject is in resolving multiple bindings of the same interface. Here's a sample configuration:

IKernel kernel = new StandardKernel();
kernel.Bind<IDataRepository>().To<FakeDataRepository>();
kernel.Bind<IDataRepository>().To<RealDataRepository>();
IEnumerable<IDataRepository> bothImplementations = kernel.GetAll<IDataRepository>();

The problem with doing this with Ninject, though, is that if you tried to call Get<IDataRepository> rather than GetAll<IDataRepository>, or if you tried to get a DataConsumer (which requires only a single IDataRepository), Ninject would throw a Ninject.ActivationException due to there being more than one matching binding.

Castle Windsor
Of all the containers I've tried, Castle Windsor is the most like Ninject. Aside from the obvious differences in syntax, one of the first things you'll notice is that Windsor does not support automatic self-binding. For the example to work, the DataConsumer will need to explicitly be registered to itself, like this:
IWindsorContainer container = new Castle.Windsor.WindsorContainer();
container.Register(Component.For<IDataRepository>().ImplementedBy<FakeDataRepository>());           container.Register(Component.For<DataConsumer>().ImplementedBy<DataConsumer>());

Note that if you forget to register DataConsumer to itself, Castle Windsor will throw a Castle.MicroKernel.ComponentNotFoundException when attempting to resolve it. Once registered, you can get an instance by calling Resolve:

DataConsumer consumer = container.Resolve<DataConsumer>();

And just like Ninject, Castle Windsor will instantiate DataConsumer, passing an instance of FakeDataRepository into its constructor.

You can resolve multiple bindings of one interface by calling ResolveAll on the container:

IWindsorContainer container = new WindsorContainer();
container.Register(Component.For<IDataRepository>().ImplementedBy<FakeDataRepository>());
container.Register(Component.For<IDataRepository>().ImplementedBy<RealDataRepository>());
IEnumerable<IDataRepository> bothImplementations = container.ResolveAll<IDataRepository>();

Unlike Ninject, Windsor will not throw an exception if you try to resolve just a single instance, but doesn't guarantee you'll always get the same one when multiple instances are bound.

Unity
Unity is configured similarly, like this:
IUnityContainer container = new Microsoft.Practices.Unity.UnityContainer();
container.RegisterType<IDataRepository, FakeDataRepository>();

And then an instance of DataConsumer can be requested like this:

DataConsumer consumer = container.Resolve<DataConsumer>();

Unity works differently than other containers when it comes to multiple bindings, however. The first difference is that each binding can be given a name, and when registering a binding with the same name and type as one that exists, the existing one is replaced. This includes bindings that have no name, like these two:

container.RegisterType<IDataRepository, FakeDataRepository>();
container.RegisterType<IDataRepository, RealDataRepository>();

Because both bindings are for the IDataRepository type and also have the same name (in this case, null), the binding for RealDataRepository will replace the binding for FakeDataRepository.

The second difference is that using ResolveAll will only return bindings that have been given a name:

IUnityContainer container = new UnityContainer();
container.RegisterType<IDataRepository, FakeDataRepository>("Fake");
container.RegisterType<IDataRepository, RealDataRepository>("Real");
IEnumerable<IDataRepository> bothImplementations = container.ResolveAll<IDataRepository>();

If there's no name given to the binding, it won't be included in the results of the ResolveAll<T> call.

And if that weren't confusing enough, objects resolved automatically from within the container (like when it's performing constructor injection) and objects requested with Resolve<T> will only be resolved if they haven't been given a name during binding.

Autofac
Autofac is configured a bit differently than the other containers. With the others you're able to instantiate the container and add bindings directly to it at any time, even after it has already been used to resolve dependencies. With Autofac, you instead create an instance of a ContainerBuilder object and apply configurations to the builder, like this:
ContainerBuilder containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<FakeDataRepository>().As<IDataRepository>();
containerBuilder.RegisterType<DataConsumer>().AsSelf();

And once you're done registering, you must first build a container from the configuration, and then use that container to resolve objects. The syntax for doing that looks like this:

IContainer container = containerBuilder.Build();
DataConsumer consumer = container.Resolve<DataConsumer>();

Once the container has been built, any changes made to the builder won't update the container and attempting to build from the ContainerBuilder more than once will cause it to throw an exception. The container is still mutable even after it has been built, but there are extra steps you must take in order to modify it. You must create a new ContainerBuilder, add the additional bindings to it, and call its Update method:

ContainerBuilder anotherBuilder = new ContainerBuilder();
anotherBuilder.RegisterType<RealDataRepository>().As<IDataRepository>();
anotherBuilder.Update(container); 

Doing this will register RealDataRepository as a binding in addition to the FakeDataRepository binding. This means you can now resolve both by requesting an IEnumerable<IDataRepository>, as shown here:

IEnumerable<IDataRepository> bothImplementations =  
  container.Resolve<IEnumerable<IDataRepository>>();

And like Castle Windsor, Autofac doesn't support automatic self-binding.

In the case of Autofac, an attempt to resolve an object that hasn't been explicitly bound to itself will cause Autofac to throw Autofac.Core.Registration.ComponentNotRegisteredException.

StructureMap
StructureMap is interesting because in addition to having a container object, it also exposes a static ObjectFactory object that can be used to access the container from anywhere. This can be useful, but care should be taken so that you don't inadvertently use the Service Locator anti-pattern. Like the other containers, StructureMap supports the Constructor Injection pattern and it should be used wherever possible.

The way I prefer to create my container with StructureMap is to instantiate the container and pass my configuration options into the constructor:

IContainer container = new Container(c => 
 c.For<IDataRepository>().Use<FakeDataRepository>());

Once instantiated, it can be used just like the other containers, as you can see here:

DataConsumer consumer = container.GetInstance<DataConsumer>();

If for whatever reason I want to change the configuration later, the container also exposes a Configure method that allows you to add to its configuration. In the following code, I use the configure method to add a second binding for IDataRepository, and then resolve both bindings:

container.Configure(c => c.For<IDataRepository>().Use<RealDataRepository>());
IEnumerable<IDataRepository> bothImplementations = container.GetAllInstances<IDataRepository>();
That's a Wrap
I hope this article provides a good reference to anyone comparing the various DI containers, as well as to anyone looking to quickly get up to speed with an unfamiliar container. Check back soon, as I plan to continue the series by demonstrating conventions-based automatic binding and how it can be used to create a plug-in architecture.

About the Author

Ondrej Balas owns UseTech Design, a Michigan development company focused on .NET and Microsoft technologies. Ondrej is a Microsoft MVP in Visual Studio and Development Technologies and an active contributor to the Michigan software development community. He works across many industries -- finance, healthcare, manufacturing, and logistics -- and has expertise with large data sets, algorithm design, distributed architecture, and software development practices.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube