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
public interface IReader
{
DateTime GetCreatedOn();
string Read();
}
DIP.Implementation.KeyboardReader.cs
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.
class Program
{
static void Main(string[] args)
{
Container container = new Container();
DIRegistration(container);
Copy copy = new Copy(container);
copy.DoCopy();
Console.ReadLine();
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:
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.
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.
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:
private Dictionary<Type, ResolvedTypeWithLifeTimeOptions> =
new Dictionary<Type,ResolvedTypeWithLifeTimeOptions>();
Next, we need to update the code of DIP.MyIoCContainer.Container.cs to support LifeTimeOptions
.
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)
{
if (!iocMap.ContainsKey(typeToResolve))
throw new Exception(string.Format("Can't resolve {0}.
Type is not registered.", typeToResolve.FullName));
ResolvedTypeWithLifeTimeOptions resolvedType = iocMap[typeToResolve];
if (resolvedType.LifeTimeOption ==
LifeTimeOptions.ContainerControlledLifeTimeOptions &&
resolvedType.InstanceValue != null)
return resolvedType.InstanceValue;
ConstructorInfo ctorInfo = resolvedType.ResolvedType.GetConstructors().First();
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);
}
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.
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:
static void Main(string[] args)
{
Container container = new Container();
DIRegistration(container);
Copy copy = new Copy(container);
copy.DoCopy();
Console.ReadLine();
Copy copy2 = new Copy(container);
copy2.DoCopy();
Console.ReadLine();
}
Let's try running the code:
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.
History
- 23rd April, 2020: Second revision (updated link to the final article)
- 21st April, 2013: First revision