Dependency Injection - Part Five - Using A DI Container (Autofac)


In the previous post, we saw how we can inject dependencies without any DI container, by embracing abstractions. However, in this post, we will see how DI containers help us by instantiating the dependencies and provide them whenever and wherever required.

What is a DI Container?
In the example for our last post, we had to instantiate dependencies on our own, before injecting them via a constructor. Consequently, as the level of dependencies increases, injections get quite complex. A DI container comes to our rescue in such situations.
A container takes the responsibility of instantiating and providing the dependency instances, irrespective of the level of dependencies. As a result, it is a container that manages the scope of the instantiated objects. However, at some point in our application, we may want to control the life time of a component on our own. We can achieve that with help from our container.
Now, in order to resolve any dependency, the container has to be made aware of them first. This means that we need to educate the container to return an instance of concrete implementation whenever and wherever I inject a dependency of the type ISomeInterface in our application. That said, there are containers like Autofac, that require us to register every type that needs to be resolved or injected as a dependency. On the other hand, there are containers like Ninject that require us to register only the types that are to be injected as a dependency.

In the following code sample, we will be using Autofac, as our DI container. However, some other well-known DI containers are StructureMap, CastleWindsor, Spring.NET, Microsoft's Unity, Ninject and much more. Rather, it's just a matter of one's choice. Let's have a look at some code, and then we shall see how a container works. 
  1. public class IoCBuilder  
  2. {  
  3.     internal static IContainer Build()  
  4.     {  
  5.         ContainerBuilder builder = new ContainerBuilder();  
  6.         RegisterTypes(builder);  
  7.         return builder.Build();  
  8.     }  
  10.     private static void RegisterTypes(ContainerBuilder builder)  
  11.     {  
  12.         builder.RegisterType<Commerce>();  
  13.         builder.RegisterType<CurrencyConverter>().As<ICurrencyConverter>();  
  14.         builder.RegisterType<TextLogger>().As<ILogger>();  
  15.         builder.RegisterType<EmailNotifier>().As<INotificationManager>();  
  16.         builder.RegisterType<CreditCardProcessor>().As<IPaymentProcessor>();              
  17.     }  
  18. }  
  21. public class Commerce  
  22. {  
  23.     private IPaymentProcessor _paymentProcessor;  
  24.     private INotificationManager _notificationManager;  
  25.     private ILogger _logger;  
  27.     public Commerce(IPaymentProcessor paymentProcessor, INotificationManager notificationManager, ILogger logger)  
  28.     {  
  29.         _paymentProcessor = paymentProcessor;  
  30.         _notificationManager = notificationManager;  
  31.         _logger = logger;  
  32.     }  
  34.     public void ProcessOrder(Order order)  
  35.     {  
  36.         decimal paidAmount = order.UnitPrice * order.Quantity;  
  37.         bool paymentSuccessful = _paymentProcessor.ProcessPayment(paidAmount);  
  39.         if(paymentSuccessful)  
  40.         {  
  41.             _notificationManager.NotifyCustomer(notification: "payment successful");  
  42.         }  
  43.         else  
  44.         {  
  45.             _notificationManager.NotifyCustomer(notification: "payment failed");  
  46.             _logger.Log(errorMessage: "payment failed");  
  47.         }  
  48.     }  
  49. }  
  52. public class Program  
  53. {  
  54.     static void Main(string[] args)  
  55.     {  
  56.         IContainer container = IoCBuilder.Build();  
  57.         Order customerOrder = new Order { Id = 1, ProductName = "Product", Quantity = 3, UnitPrice = 75 };  
  58.         Commerce commerce = container.Resolve<Commerce>();  
  59.         commerce.ProcessOrder(customerOrder);  
  60.     }  
  61. }  

How does a DI container work? 

  • The first thing to remember is, that the container needs to know about all the types we want it to instantiate for us. In other words, we need to register the types with the container. In general, this is known as Registration.
  • As can be seen in the IoCBuilder class, we have registered our types against their respective interface. As a result, when we add a dependency of an interface, the container will provide an instance of the class registered against that particular interface.
  • At the same time, it is not necessary that we register all our types against an interface. Commerce class, for instance, has been registered as self and not against any interface.
  • Moreover, the process of requesting the container to return an instance of a type is known as Resolving a type.
  • While resolving a type, the DI container looks at the type's constructor. If the type has some dependencies, the container will first resolve these dependencies, and then create its instance.
  • The container enumerates through these dependencies and looks into their constructor. If a dependency class has its own dependencies, then the container will try to resolve them first. And in order to do that, it consistently checks the type registrations.
  • It is equally important to know, that not all DI containers require all types to be explicitly registered. Autofac does, whereas, Unity does not.
In this article, we saw a very basic use of a DI container for injecting our dependencies at any level. There are a lot more ways in which a container allows us to configure our dependencies. While this can be a little overwhelming, we need to be extra careful to avoid any unexpected behaviour in our application. In the next post, we will see one such configuration, a.k.a. As Implemented Interfaces in Autofac.