Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Dependency Inversion Principle, IoC Container, and Dependency Injection: Part 4

4.87/5 (28 votes)
23 Apr 2020CPOL4 min read 45.6K   525  
Implementation of LifeTimeOption for custom container
This part of the article explains about the implementation of LifeTimeOption for our custom container. The intent of writing this part of the article is to gain some knowledge for managing dependency resolution lifetime.

Introduction

This is the fourth part of my article on Dependency Inversion Principle, IoC Container & Dependency Injection. In the previous part of this article, I tried to explain how to build your own IoC Container. This part of the article uses most of the code snippets from the previous part. If you are a direct visitor to this part, you may find it difficult to understand the code. So please go through the previous parts before you start reading this. For easy navigation to other articles in the series, I have provided the links below:

Background

Sometimes, lifetime of a dependency resolved object is considered as the most important factor when we need to maintain the state of an object.

The implementation given in my previous part of the article is the simplest implementation which creates a new object when there is a new request to resolve the dependency.

To test this scenario, let's modify our code files as follows:

  • Each object will maintain a property, CreatedOn, so that we can keep track of object creation time.
  • Resolve multiple copies of dependency in different time and compare their time of creation.

DIP.Abstractions.IReader.cs

C#
public interface IReader
{
    DateTime GetCreatedOn();
    string Read();
}

DIP.Implementation.KeyboardReader.cs

C#
public class KeyboardReader:IReader
{
    private DateTime _createdOn;
 
    public KeyboardReader()
    {
        _createdOn = DateTime.Now;
    }
 
    public DateTime GetCreatedOn()
    {
        return _createdOn;
    }
        
    public string Read()
    {
        return "Reading from \"Keyboard\"";
    }
}

Similarly make changes in DIP.Abstractions.IWriter.cs and DIP.Implementation.PrinterWriter.cs.

Now change the implementation of Consumer to test the result.

C#
class Program
{
    static void Main(string[] args)
    {
        // Create the container object
        Container container = new Container();
        
        // Register all the dependencies
        DIRegistration(container);
 
        // Prepare the first copy object and do the copy operation
        Copy copy = new Copy(container);
        copy.DoCopy();
        Console.ReadLine();
 
        // Prepare the second copy object and do the copy operation
        Copy copy2 = new Copy(container);
        copy2.DoCopy();
        Console.ReadLine();
    }
 
    static void DIRegistration(Container container)
    {
        container.Register<IReader,KeyboardReader>();
        container.Register<IWriter,PrinterWrter>();
    }
}

By running the application, you will be getting similar output as follows:

Image 1

As you see from the above output, two instances of Reader object are created at different times. The second instance of reader object is different than the first instance of reader object. This works as we have expected. But there can be some situation where you may want to resolve a dependency to the same object so that the state will be maintained.

This part of the article will explain how to implement LifeTimeOptions for our custom IoC container. There are different types of LifeTimeManager in Microsoft Unity with various benefits. But just for the purpose of understanding, I will be implementing two LifeTimeOptions in our container. The implemented code is attached with this part of the article. And any suggestions to improve the implementation are most welcome.

Implementation of LifeTimeOption

To start with the implementation, let's create an enumeration which will have different LifeTimeOptions which we are going to implement.

C#
public enum LifeTimeOptions
{
    TransientLifeTimeOption,
    ContainerControlledLifeTimeOption
}
  • TransientLifeTimeOption: This option tells the container to create a new instance per call (when there is a call to resolve a dependency)
  • ContainerControlledLifeTimeOption: This option tells the container that we want to maintain only one copy of the resolving dependency, so that the same object will be returned each-time whenever there is a call to resolve a dependency)

Next, we need to create a new type called ResolvedTypeWithLifeTimeOptions, which will store the information regarding the resolved type.

C#
public class ResolvedTypeWithLifeTimeOptions
{
    public Type ResolvedType { get; set; }
    public LifeTimeOptions LifeTimeOption { get; set; }
    public object InstanceValue { get; set; }
 
    public ResolvedTypeWithLifeTimeOptions(Type resolvedType)
    {
        ResolvedType = resolvedType;
        LifeTimeOption = LifeTimeOptions.TransientLifeTimeOptions;
        InstanceValue = null;
    }
 
    public ResolvedTypeWithLifeTimeOptions(Type resolvedType, LifeTimeOptions lifeTimeOption)
    {
        ResolvedType = resolvedType;
        LifeTimeOption = lifeTimeOption;
        InstanceValue = null;
    }
}
  • Resolved Type: The type to which dependency will be resolved
  • LifeTimeOption: LifeTime options for the created object
  • InstanceValue: This property will come into act if LifeTimeOption = ContainerControlledLifeTimeOption, and will provide the same object for all calls to resolve the dependency.

With the introduction of new class, our dictionary declaration of iocMap will be changed to the following:

C#
private Dictionary<Type, ResolvedTypeWithLifeTimeOptions> = 
		new Dictionary<Type,ResolvedTypeWithLifeTimeOptions>();

Next, we need to update the code of DIP.MyIoCContainer.Container.cs to support LifeTimeOptions.

C#
public class Container
{
    private Dictionary<Type, ResolvedTypeWithLifeTimeOptions> 
	iocMap = new Dictionary<Type, ResolvedTypeWithLifeTimeOptions>();
 
    public void Register<T1, T2>()
    {
        Register<T1, T2>(LifeTimeOptions.TransientLifeTimeOptions);
    }
 
    public void Register<T1, T2>(LifeTimeOptions lifeTimeOption)
    {
        if (iocMap.ContainsKey(typeof(T1)))
        {
            throw new Exception(string.Format("Type {0} already registered.", 
                                typeof(T1).FullName));
        }
        ResolvedTypeWithLifeTimeOptions targetType = 
                                new ResolvedTypeWithLifeTimeOptions(typeof(T2), 
		lifeTimeOption);
        iocMap.Add(typeof(T1), targetType);
    }
 
    public T Resolve<T>()
    {
        return (T)Resolve(typeof(T));
    }
 
    public object Resolve(Type typeToResolve)
    {
        // Find the registered type for typeToResolve
        if (!iocMap.ContainsKey(typeToResolve))
            throw new Exception(string.Format("Can't resolve {0}. 
            Type is not registered.", typeToResolve.FullName));
 
        ResolvedTypeWithLifeTimeOptions resolvedType = iocMap[typeToResolve];
 
        // Step-1: If LifeTimeOption is ContainerControlled and there is 
        //already an instance created then return the created instance.
        if (resolvedType.LifeTimeOption == 
        LifeTimeOptions.ContainerControlledLifeTimeOptions && 
        resolvedType.InstanceValue != null)
            return resolvedType.InstanceValue;
 
        // Try to construct the object
        // Step-2: find the constructor 
        //(ideally first constructor if multiple constructors present for the type)
        ConstructorInfo ctorInfo = resolvedType.ResolvedType.GetConstructors().First();
 
        // Step-3: find the parameters for the constructor and try to resolve those
        List<ParameterInfo> paramsInfo = ctorInfo.GetParameters().ToList();
        List<object> resolvedParams = new List<object>();
        foreach (ParameterInfo param in paramsInfo)
        {
                Type t = param.ParameterType;
                object res = Resolve(t);
                resolvedParams.Add(res);
        }
 
        // Step-4: using reflection invoke constructor to create the object
        object retObject = ctorInfo.Invoke(resolvedParams.ToArray());
 
        resolvedType.InstanceValue = retObject;
 
        return retObject;
   }
}

In the above code, we have modified the existing Register method and there is another overloaded Register method which just updated LifeTimeOption property of the resolved type.

The Resolve() method is also modified which first verifies the LifeTimeOption for the dependency. And if LifeTimeOption is specified as ContainerControlledLifeTimeOption, then it checks whether the object for the dependency is already created earlier or not. If it is so, it returns the created object or else it creates a new object, stores it, and returns the same to the caller.

Now update the registration of the dependency with LifeTimeOptions.ContainerControlled option and check the result.

C#
static void DIRegistration(Container container)
{
     container.Register<IReader, KeyboardReader>
     (LifeTimeOptions.ContainerControlledLifeTimeOption);
     container.Register<IWriter, PrinterWriter>
     (LifeTimeOptions.TransientLifeTimeOption);
} 

Notice I have given LifeTimeOptions.ContainerControlledLifeTimeOption for IReader dependency and LifeTimeOptions.TransientLifeTimeOption for IWriter dependency.

Update the Main function as follows:

C#
static void Main(string[] args)
{
      // Create the container object
      Container container = new Container();
            
      // Register all the dependencies
      DIRegistration(container);
 
      // Prepare the first copy object and do the copy operation
      Copy copy = new Copy(container);
      copy.DoCopy();
      Console.ReadLine();
 
      // Prepare the second copy object and do the copy operation
      Copy copy2 = new Copy(container);
      copy2.DoCopy();
      Console.ReadLine();
} 

Let's try running the code:

Image 2

Now you can see the difference. For IReader dependency, we have specified ContainerControlledLifeTimeOption. Hence the container creates only one instance and will be returned the same instance for every call to resolve method. For IWriter dependency, we have specified TransientLifeTimeOption. Hence the container creates and returns a new instance for each call to resolve method.

Microsoft provides Microsoft Unity for dependency resolution with various features. In the next chapter, I will be explaining Microsoft Unity and its features.

Summary

This part of the article explains about the implementation of LifeTimeOption for our custom container. The intent of writing this part of the article is to gain some knowledge for managing dependency resolution lifetime. Microsoft provides several other features, which I will be explaining in the next part of the article.

Please post your suggestions (if you have any) to make this article more effective. Smile | <img src=

History

  • 23rd April, 2020: Second revision (updated link to the final article)
  • 21st April, 2013: First revision

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)