Introduction
Important note: This article is no longer up to date. For correct API, please, look at https://www.codeproject.com/Articles/5349911/Generic-Minimal-Inversion-of-Control-Dependency-In.
Why I Am Writing this Article
Throughout my life, I dealt with many various IoC containers, including MEF, Unity, AutoFac and Castle Dynamic Proxy. Also, I did some research on Ninject.
I saw instances where containers were used without resolving any problems - they were used because the architects of the project could not imagine building projects without IoC.
I also noticed that usually only a small part of the IoC container functionality is actually useful.
Finally, most container packages are huge - much bigger than the useful part of their functionality.
Combining all the reasons above together, I decide to come up with my own container IoCy optimized for performance and very small but containing the essential functionality that can simplify the lives of project's architects and developers.
There was a great CodeProject article Expandable IoC Container by Paulo Zemek. This article describes building a very simple IoC container and in the beginning, I wanted to use that container for my purposes, but after looking at it, I thought that the container described there was a bit too raw. I wanted to have an IoC container that would allow me to add whole assemblies to it and also would allow me to build a plugin architecture and those parts were missing there.
On top of presenting a new IoC container, I also try to describe which part of the IoC container functionality is really useful and how it should be used.
What is an IoC Container
IoC Container is a software that takes care of creating objects for you. Instead of calling an object's constructor, you call a method on the container usually passing to it the Type
of the object you want to obtain, and, perhaps some other parameters also and it will return the object of the requested type to you.
Examples of Usage
Resolving Interface Types Recursively
At this point, for the purpose of examples, I assume we have a generic container, but the methods I describe here are named the same as those of IoCy.
For example, assume that you want to have an interface IOrg
implemented by several classes, including class Organization
. Assume that somehow you preconfigured your Container
to map interface IOrg
to class Organization
(the concrete methods for creating such mapping will be described later). Then you can call IoCContainer.Resolve(...)
method to obtain the object:
IOrg organization = IoCContainer.Resolve<IOrg>();
Note that many times, you would want to create an new object every time you call IoCContainer.Resolve()
method - so it will be called in place of a constructor.
Sometimes, however, you might want the method to return the same object whenever it is called. For example, if you are logging some messages, you probably want to use the same logger in every place, so calling:
ILog log = IoCContainer.Resolve<ilog>();
should return the same object every time you call it. Such objects are called Singletons - there is only one instance of such object per container. You can specify whether you want a certain type to be resolved every time as a Singleton or not at the configuration (mapping) stage of building the container. This stage will be described later in the article.
The container, thus, specifies the maps between the types that you want to resolve and some kind of an object creation strategy.
Note that when you resolve an object, the container will also try to resolve its composable parts (properties). For example, assume that our IOrg
interface has a public
property Manager
of interface type IPerson
, which maps into a concrete class Person
and another property of type ILog
. Assume also that those properties are somehow marked as 'composable'. When you call:
IPerson person = IoCContainer.Resolve<iperson>();
The container will try to resolve those properties also. If those interfaces (or the types that implement them) also have some composable properties - those properties should also be resolved recursively by the container - and so on.
Note that some containers, e.g., Unity assume that all properties are composable. Such assumption leads to longer composition algorithms since every property has to be checked.
Plugin Architecture
Another major usage for IoC containers is to simplify the plugin architecture.
For example, you have multiple teams that develop various widgets for you application. The plugin architecture has potential to print them all together as part of an IoC container. In that case, we should be using the Multi object resolution container feature akin to MEF's ImportMany
.
Each plugin can implement a certain interface say called IPlugin
. The plugins can be copied to a certain folder (should be different from the application folder) and from there, dynamically loaded in the container. Then the application would get them from the container and arrange them, e.g., as tabs or based on their configuration.
Purposes of IoC Containers
Based on the above, there are following major usages of IoC containers:
- Simplifying the injection and allowing to change the object implementation easily. This is important for several reasons, the major one being to be able to mock up test object and inject them instead of the real ones.
- Simplifying the plugin architecture that would allow multiple developers or even multiple teams develop various plugins almost independently.
The above two IoC features are implemented as part of IoCy container.
There are other features that many containers implement but not often used. Allowing a container to create your object also allows to do some crazy things like code generation or code weaving on the fly and other things which can facilitate creating proxies inserting pre or post code to a function or property setter and Aspect Oriented Features. For those, you will have to wait until I marry IoCy and Roxy.
Code Samples
Introduction
In this section, I describe the samples that show how to use the IoCy container.
Code Samples Location
You can download the code from the link above the article, or you can get it from GITHUB at NP.IoCy
There is only one solution - NP.IoCy.IoCContainerAndTests
. The project that contains the IoC container code is NP.IoCy
. There are a number of test projects under TESTS solution folder. Whenever you want to run a test, you will have to set the corresponding project as a startup project.
Overview of the Code
As was mentioned above, the IoCy code is located under NP.IoCy
project. IoCContainer
class represents the container and has most of the code.
NP.IoCy
project also contains Attributes folder with a number of Attributes
defined to be used by the IoCContainer
and a folder Utils that has two very simple extension classes, ObjUtils
and ReflectionUtils
.
Bootstrapper Sample
To run the sample, set the BootstrappingTest
project to be a startup project. The sample consists of Program.cs file and two folders, Interfaces and Implementations that contain corresponding interface and classes:
Here is the code for IOrg
interface:
public interface IOrg
{
string OrgName { get; set; }
IPerson Manager { get; set; }
ILog Log { get; set; }
void LogOrgInfo();
}
And here is the code for Org
class that implements IOrg
interface:
public class Org : IOrg
{
public string OrgName { get; set; }
[Part]
public IPerson Manager { get; set; }
[Part]
public ILog Log { get; set; }
public void LogOrgInfo()
{
Log.WriteLog($"OrgName: {OrgName}");
Log.WriteLog($"Manager: {Manager.PersonName}");
Log.WriteLog($"Manager's Address: {Manager.Address.City}, {Manager.Address.ZipCode}");
}
}
Note that Part
attribute is a hint to IoCContainer
that it needs to resolve the property.
You can see that Organization contains property Manager
of type IManager
property Log
of type ILog
. Taking a look at IPerson
interface, you can see that it contains property Address
of type IAddress
.
For the rest of the interface/class pairs involved, I will only show the class.
Here is the Person
class implementation:
public class Person : IPerson
{
public string PersonName { get; set; }
[Part]
public IAddress Address { get; set; }
}
Here is the Address
class code:
public class Address : IAddress
{
public string City { get; set; }
public string ZipCode { get; set; }
}
Finally, we have two simple implementations of ILog
interface:
FileLog
will create "MyLogFile.txt" within the same folder as the executable and will write logs into it. ConsoleLog
will write the log to console.
public class FileLog : ILog
{
const string FileName = "MyLogFile.txt";
public FileLog()
{
if (File.Exists(FileName))
{
File.Delete(FileName);
}
}
public void WriteLog(string info)
{
using(StreamWriter writer = new StreamWriter(FileName, true))
{
writer.WriteLine(info);
}
}
}
public class ConsoleLog : ILog
{
public void WriteLog(string info)
{
Console.WriteLine(info);
}
}
Now that we have learned the objects' structure and the relationships between the objects, let us take a look at Program.Main()
method within Program.cs file:
static void Main(string[] args)
{
IoCContainer container = new IoCContainer();
#region BOOTSTRAPPING
container.Map<IPerson, Person>();
container.Map<IAddress, Address>();
container.Map<IOrg, Org>();
container.MapSingleton<ILog, FileLog>();
#endregion BOOTSTRAPPING
container.CompleteConfiguration();
IOrg org = container.Resolve<IOrg>();
#region Set Org Data
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "33162";
#endregion Set Org Data
org.LogOrgInfo();
IoCContainer childContainer = container.CreateChild();
childContainer.Map<ILog, ConsoleLog>();
childContainer.CompleteConfiguration();
IOrg orgWithConsoleLog = childContainer.Resolve<IOrg>();
#region Set Child Org Data
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "33162";
#endregion Set Child Org Data
orgWithConsoleLog.LogOrgInfo();
}
We create the container and then bootstrap it, i.e., we define the mappings between the classes and interface within the code:
IoCContainer container = new IoCContainer();
#region BOOTSTRAPPING
container.Map<IPerson, Person>();
container.Map<IAddress, Address>();
container.Map<IOrg, Org>();
container.MapSingleton<ILog, FileLog>();
#endregion BOOTSTRAPPING
Once bootstrapping is over, we signify it by calling container.ConfigurationCompleted()
method.
After this method is called, we cannot modify the container class any more. This is done in order to avoid thread locking after the bootstrapping stage.
Now we create and compose the IOrg
object:
IOrg org = container.Resolve<IOrg>();
The object returned has type Org
and its parts (and parts of those parts) are being resolved from the container and are based on the mappings defined at the boostrapping stage.
Next, we set the data for the organization and its parts:
#region Set Org Data
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "33162";
#endregion Set Org Data
Then, we call org.LogOrgInfo()
method to write the data into the ILog
implementation. Note that we mapped ILog
to FileLog
at the bootstrapping stage so the text will be set to "MyLogFile.txt" file within "bin/debug" folder of the project.
Now is the most interesting part, I demonstrate how easy it is to replace an implementation using IoCy.
Remember that we cannot modify the container after the boostrapping state is over, but we can create a child container:
IoCContainer childContainer = container.CreateChild();
Then we can set whatever mappings we want to change within the child container. In our case, we want to map ILog
to ConsoleLog
instead of FileLog
:
childContainer.Map<ILog, ConsoleLog>();
childContainer.CompleteConfiguration();
Resolving from a child container works in chain of responsibility fashion - whenever it cannot find a mapping, it checks its parent for it. In our case, only ILog
will be resolved from the child container, everything else will be resolved from the parent:
IOrg orgWithConsoleLog = childContainer.Resolve<IOrg>();
Now we set the data on the new object and call LogOrgInfo()
method:
#region Set Child Org Data
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "33162";
#endregion Set Child Org Data
orgWithConsoleLog.LogOrgInfo();
The information will be printed onto the console instead of the file.
Assembly Loading Sample
This sample is located under TESTS/AssemblyLoadingTest folder. It consists of 3 projects:
AssemblyLoadingTest
is the main project that should be made a startup to run this test. - Interfaces project contains interfaces (same as in the last sample, only here they are factored out into a separate project).
- Implementations project contains implementations (also exactly the same as in the last sample).
Implementations project is dependent on Interfaces project and AssemblyLoadingTest
project is dependent on both Implementation and Interfaces.
Note that the implementations now have Implements
class attributes, e.g.
[Implements(typeof(IOrg))]
public class Org : IOrg
{
...
}
or:
[Implements]
public class Person : IPerson
{
...
}
Implements
attribute signals to the IoCContainer
that such implementation should be mapped to another more abstract type (a super class or an interface). Such type is either explicitly specified as an argument to Implements
attribute (as in case of the [Implements(typeof(IOrg))]
), or it is implicitly assumed to be a nontrivial baseclass or the first interface implemented (in case of the Person
class, it will be IPerson
interface).
Now take a look at Program.Main
method within AssemblyLoadingTest
project. It is exactly the same as in the previous sample, except that the whole bootstrapping part is replaced by the following line:
container.InjectAssembly(typeof(Implementations.Org).Assembly);
All the required boostrapping is done automatically by using information from Implements
and Part
attributes within the assembly.
Dynamic Loading Assembly Sample
This sample is located under TEST/DynamicAssemblyLoadingTest folder. Its main project is DynamicAssemblyLoadingTest
.
Almost everything is the same as in the previous sample, aside from a very important detail - the main project DynamicAssemblyLoadingTest
is not dependent on the Implementations
project. Instead, the Implementations project has a post build event that copies its assembly file under "DynamicAssemblyLoadingTest/bin/Debug/Plugins" folder.
Then for Assembly injection, we use InjectDynamicAssemblyByFullPath(...)
method:
container.InjectDynamicAssemblyByFullPath("Plugins\\Implementations.dll");
This method loads the assembly dynamically by the specified path. The results are exactly the same as in the two previous samples.
Multi Part Boostrapping Sample
This sample is located within MultiPartBootstrappingTest
project (this project needs to be make startup).
This sample is very similar to the first sample (all the interfaces and implementations are located within the same project).
There is one important difference, however: class Org
(and the corresponding IOrg
interface have an enumeration of ILogs
objects instead of a single ILog
object:
[MultiPart]
public IEnumerable<ilog> Logs { get; set; }
</ilog>
This enumeration is marked by MultiPart
attribute (instead of Part
attribute).
Implementation of Org.LogOrgInfo()
method is also different in a sense that we go over each ILog
within the enumeration of the logs and send information to each one of them:
public void LogOrgInfo()
{
foreach(ILog log in Logs)
{
log.WriteLog($"OrgName: {OrgName}");
log.WriteLog($"Manager: {Manager.PersonName}");
log.WriteLog($"Manager's Address: {Manager.Address.City}, {Manager.Address.ZipCode}");
}
}
The Program.Main
method is also very similar to that of the first sample aside from boostrapping the ILog
:
container.MapMultiType<ilog, filelog="">();
container.MapMultiType<ilog, consolelog="">();
</ilog,></ilog,>
IoCContainer.MapMultiType()
method adds the implementations to a collection of implementations corresponding to ILog
interface.
When you run the application, you'll see that both file and console receive the same text.
Plugin Sample
Last sample shows how to use IoCy to achieve the plugin architecture when multiple plugins are being dynamically loaded into the system.
The sample is located under PluginsTest folder. It consists of 4 projects:
Interfaces
- contains IPlugin
interface Plugin1
- contains a PluginOne
class that implements IPlugin
interface Plugin2
- contains a PluginTwo
class that implements IPlugin
interface PluginTest
- contains the main program
Here is the very simple code for IPlugin
interface and plugin implementations:
public interface IPlugin
{
void PrintMessage();
}
[MultiImplements]
public class PluginOne : IPlugin
{
public void PrintMessage()
{
Console.WriteLine("I am PluginOne");
}
}
[MultiImplements(typeof(IPlugin))]
public class PluginTwo : IPlugin
{
public void PrintMessage()
{
Console.WriteLine("I am PluginTwo");
}
}
Note that the main project PluginsTest
does not depend on Plugin1
and Plugin2
projects. Instead, Plugin1
and Plugin2
projects have a PostBuild
event that copies their DLLs into PluginTest/bin/Debug/Plugins folder.
Here is the code for Program.Main
method:
static void Main(string[] args)
{
IoCContainer container = new IoCContainer();
container.InjectPluginsFromFolder("Plugins");
container.CompleteConfiguration();
IEnumerable<iplugin> plugins = container.MultiResolve<iplugin>();
foreach(IPlugin plugin in plugins)
{
plugin.PrintMessage();
}
}
As a result, the messages will be printed on the console from each one of the plugins.
Summary
In this article, I demonstrate how to match useful IoC functionality with under 1000 lines of code. The essential IoCy
container functionality is here, though you might tweak it slightly depending on the needs.
As I mentioned above, in the near future, I plan to marry IoCy
and Roxy
providing a full fledged IoC container with code generation capabilities.