Creating a Custom Inversion-of-Control Container

Dependency Inversion Principle and Inversion of Control
The letter ‘D’ in the SOLID design principle stands for ‘Dependency Inversion Principle (DIP)’. The principle refers to the concept that one should depend upon an abstraction rather than concretion. The idea is that for the high-level modules that conventionally depended upon low-level module to be independent, or at least depend on abstractions. One technique to satisfy the DIP is to use Dependency Injection (DI). This is the approach where low-level dependencies are injected into the high-level module upon creation. Details of DIP and DI are beyond the scope of this article, but plenty of materials is available over the web.

Inversion of Control (IoC) is a software approach where the executing program should receive part of its flow of control from a centralized generic reusable library. IoC simplifies the design and makes it more scalable and maintainable by moving all the common repetitive logic into a shared location where it is easily configured and maintained. One of the most popular applications of IoC is to support dependency injection. A typical use case is to setup and configure an IoC container to manage all the various dependencies in the model and be able to provide properly initialized instances when requested by the application. In this scenario, high-level modules are shielded from any knowledge of how to construct complicated low-level modules when needed. This also promotes the Don’t-Repeat-Yourself (DRY) software practice by centralizing the logic into one container making it reusable. There are many IoC containers available over the web and they are all proven to work and scale in a copacetic fashion. So there is no reason for one to implement or design a custom IoC container when several options are readily available and for free. However, in this post, a custom IoC container is implemented from scratch to demonstrate the basics of how an IoC container should work.

Example1: No Using of an IoC
In the example below, a StringWriter class is created with the ability of writing various strings provided by a client to a provided output. The output is defined as any class that implements the IOutput interface. The interface requires the only method Write():

IOutput.cs

public interface IOutput
{
     void Write(string output);
}

There are two concrete implementations of the IOutput interface in this example. One is intended to write to a file on a hard disk and another that writes to a printer. However, since setting up and configuring LPT ports or Tcp/IP ports is beyond the scope of this example, both classes implement the Write() method by writing directly to the console. This is done to keep the example as simple as possible.

Printer.cs

public class Printer : IOutput
{
     public void Write(string output)
     {
          Console.WriteLine("Writing {0} to Printer", output);
     }
}

HardDisk.cs

public class HardDisk : IOutput
{
     public void Write(string output)
     {
          Console.WriteLine("Writing {0} to HardDisk", output);
     }
}

The StringWriter class is a simple class that contains an instance of an IOutput. In other words, the StringWriter class (high-level class) depends on an IOutput (low-level class). The StringWriter class also exposes a WriteString() method that takes in a string and invokes the Write() method on the interface.

StringWriter.cs

public class StringWriter
{
     private readonly IOutput _output;

     public StringWriter(IOutput output)
     {
          _output = output;
     }

     public void WriteString(string value)
     {
          _output.Write(value);  
     }
}

In the main function of the application, a new instance of a StringWriter is constructed. But first, an instance Printer (which implement IOutput) is constructed in order to be injected in the StringWriter’s constructor as a dependency. The the StringWriter.WriteString() method is invoked. The same thing is done with a Hard Disk instance in order to write our strings to the hard disk.

Program.cs and Main

public class Program
{
     public static void Main(string[] args)
     {
          var printer = new Printer();
          var stringWriter = new StringWriter(printer);
          stringWriter.WriteString("Hello World");
         
          var hardDisk = new HardDisk();
          stringWriter = new StringWriter(hardDisk);
          stringWriter.WriteString("Hello World");
     }
}

There are several issues with the design above. In order to successfully construct an instance of StringWriter, one must decide ahead of time on what implementation of an IOutput is to be used. This could lead to lots of repetitive code that should not be a concern to the client. This is also not configurable and it is not possible for one to assign a default implementation of IOutput without a recompilation of the code.

Example2: Using a Dependency Resolver without an IoC
In the example below, the design is improved drastically by setting up a DependencyResolver class whose sole purpose is to decide what concrete implementation of IOutput to hand back when requested by the client. This example does not use an IoC. However, it does invert the control by exposing a ResolveOutput() method on the DependecyResolver class. The method uses a ‘switch’ statement on a property defined in the .Net Settings XML collection of properties. The property is called “Output”, and if it is assigned the value “Printer”, then it will return an instance of a Printer. If it is assigned the value “HardDisk” then it will return an instance of a HardDisk and so on. Below is the definition of the Settings XML file and the DependencyResolver class:

Properties\Settings.settings
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings"
                 CurrentProfile="(Default)"
                 GeneratedClassNamespace="IoCContainer.Properties"
                 GeneratedClassName="Settings">
  <Profiles />
  <Settings>
    <Setting Name="Output" Type="System.String" Scope="User">
      <Value Profile="(Default)">HardDisk</Value>
    </Setting>
  </Settings>
</SettingsFile>

DependencyResolver.cs

public class DependencyResolver
{
     public IOutput ResolveOutput()
     {
          switch (Settings.Default.Output)
          {
               case "Printer":
                    return new Printer();
               case "HardDisk":
                    return new HardDisk();
               default:
                    throw new Exception(string.Format("Unable to find an output of type {0}", Settings.Default.Output));
            }
     }
}

In the main method of the application, all that is required before constructing an instance of the StringWriter is to call the ResolveOutput() method to obtain an instance of an IOutput. Since this setting is driven by an XML property, the value can be changes at run-time without having to recompile the code. However, The client will still need to know about the dependency between the StringWriter and the IOutput interface and no control is inverted. But it is an improvement from the previous example.

Program.cs and Main

public class Program
{
     public static void Main(string[] args)
     {
          var dependencyResolver = new DependencyResolver();
          var output = dependencyResolver.ResolveOutput();
          var stringWriter = new StringWriter(output);
          stringWriter.WriteString("Hello World");
     }
}

Example3: Creating a Custom IoC Container
In order to improve the previous example further, the resolution of the dependency should not be done on the IOutput class, which is the low-level class. Instead, it should be applied directly on the StringWriter class. This way, the client is completely shielded from having to know what the StringWriter depends upon and it can simply request an instance of a StringWriter directly. However, prior to requesting the StringWriter instance, the DependencyResolver class needs to be configured correctly by registering all the dependency ahead of time.

Registering Dependencies
The first element to be implemented in the DependencyResolver IoC is a private field that is a dictionary that maps a system type to another. The default constructor of the DependencyResolver will initialize the dictionary. A Register() method is exposed to add mapping entries to the dictionary map.

The registration of the dependencies should be done once at the beginning of the application and there is no need to do it again as long as the same DependencyResolver instance is used. For that reason, it might be a good idea to setup the DependencyResolver as a singleton object. However, in this example things are kept as simple as possible.

Implementing the Resolve() Method on the IoC
The second and last piece to completing the DependecyResolver custom IoC is to define the generic Resolve() method. The method checks the mapping dictionary for the requested type. If it does not exist, it throws an exception. Otherwise, it uses reflection to get a handle on the default constructor of the requested type. If the type exists and the constructor does not require any parameters, it uses the Activator.CreateInstance() method from the reflection library to make the instance and return it. If the constructor exists and it requires parameters, then the class iterates over each parameter and invokes the same Resolve() method recursively to resolve each dependency. Finally, it calls the Invoke() method on the constructor passing it all the resolved dependencies and returns the type. The final implementation of the IoC looks like the following:

DependencyResolver.cs

public class DependencyResolver
{
     private readonly Dictionary<Type, Type> _dependencyMap;

     public DependencyResolver()
     {
          // Initialize the mapping dictionary
          _dependencyMap = new Dictionary<Type, Type>();
     }

     public void Register<T1, T2>()
     {
          // Register the requested mapping in the dictionary
          _dependencyMap.Add(typeof(T1), typeof(T2));
     }

     public T Resolve<T>()
     {
          // Call a helper function
          return (T) Resolve(typeof(T));
     }

     private object Resolve(Type type)
     {
          Type resolvedType;

          try
          {
               // Locate the resolved type in the mapping dictionary if it exists
               resolvedType = _dependencyMap[type];
          }
          catch
          {
               throw new Exception(string.Format("Unable to find an output of type {0}", type.Name));  
          }
          
          // Find the first constructor of the requested type and it's parameters
          var constructor = resolvedType.GetConstructors().First();
          var parameters = constructor.GetParameters();

          // If no parameters exits, use reflection to make a new instance of the class
          if (!parameters.Any())
          {
               return Activator.CreateInstance(resolvedType);
          }
          
          // If any constructor parameters are required, 
          // resolve them recursively using the Resolve() method
          var resolvedParameters = parameters.Select(p => Resolve(p.ParameterType)).ToArray();
          return constructor.Invoke(resolvedParameters);
     }
}

The final and complete client code used to register the dependencies and request an instance of the StringWriter class is defined in the code below:

Program.cs and Main

public class Program
{
     public static void Main(string[] args)
     {
          // Create an instance of a DependencyResolver
          var dependencyResolver = new DependencyResolver();
          
          // Register all the dependencies in the system
          dependencyResolver.Register<StringWriter, StringWriter>();
          dependencyResolver.Register<IOutput, HardDisk>();
  
          // Once the DependencyResolver is properly prepped, request 
          // an instance of a StringWriter
          var stringWriter = dependencyResolver.Resolve<StringWriter>();
          stringWriter.WriteString("Hello World");
     }
}

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *