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

Introducing IoCy - Simple and Powerful IoC Container

5.00/5 (9 votes)
13 May 2018CPOL11 min read 26.1K   91  
New simple but powerful IoC container

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:

C#
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:

C#
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:

C#
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:

  1. 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.
  2. 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:

C#
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:

C#
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:

C#
public class Person : IPerson
{
    public string PersonName { get; set; }

    [Part]
    public IAddress Address { get; set; }
}  

Here is the Address class code:

C#
public class Address : IAddress
{
    public string City { get; set; }

    public string ZipCode { get; set; }
}  

Finally, we have two simple implementations of ILog interface:

  1. FileLog will create "MyLogFile.txt" within the same folder as the executable and will write logs into it.
  2. ConsoleLog will write the log to console.
C#
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);
        }
    }
}  
C#
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:

C#
static void Main(string[] args)
{
    // create container
    IoCContainer container = new IoCContainer();

    #region BOOTSTRAPPING
    // bootstrap container 
    // (map the types)
    container.Map<IPerson, Person>();
    container.Map<IAddress, Address>();
    container.Map<IOrg, Org>();
    container.MapSingleton<ILog, FileLog>();
    #endregion BOOTSTRAPPING

    // after CompleteConfiguration
    // you cannot bootstrap any new types in the container.
    // before CompleteConfiguration call
    // you cannot resolve container types. 
    container.CompleteConfiguration();

    // resolve and compose organization
    // all its 'Parts' will be added at
    // this stage. 
    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

    // Create file MyLogFile.txt in the same folder as the executable
    // and write department store info in it;
    org.LogOrgInfo();


    // replace mapping to ILog to ConsoleLog in the child container. 
    IoCContainer childContainer = container.CreateChild();

    // change the mapping of ILog to ConsoleLog (instead of FileLog)
    childContainer.Map<ILog, ConsoleLog>();

    // complete child container configuration
    childContainer.CompleteConfiguration();

    // resolve org from the childContainer.
    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

    // send org data to console instead of a file.
    orgWithConsoleLog.LogOrgInfo();
}  

We create the container and then bootstrap it, i.e., we define the mappings between the classes and interface within the code:

C#
// create container
IoCContainer container = new IoCContainer();

#region BOOTSTRAPPING
// bootstrap container 
// (map the types)
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:

C#
// resolve and compose organization
// all its 'Parts' will be added at
// this stage. 
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:

C#
#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:

C#
// replace mapping to ILog to ConsoleLog in the 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:

C#
// change the mapping of ILog to ConsoleLog (instead of FileLog)
childContainer.Map<ILog, ConsoleLog>();

// complete child container configuration
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:

C#
// resolve org from the childContainer.
IOrg orgWithConsoleLog = childContainer.Resolve<IOrg>();  

Now we set the data on the new object and call LogOrgInfo() method:

C#
#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

// send org data to console instead of a file.
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:

  1. AssemblyLoadingTest is the main project that should be made a startup to run this test.
  2. Interfaces project contains interfaces (same as in the last sample, only here they are factored out into a separate project).
  3. 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.

C#
[Implements(typeof(IOrg))]
public class Org : IOrg
{
   ...
}  

or:

C#
[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:

C#
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:

C#
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:

C#
[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:

C#
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:

C#
    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:

  1. Interfaces - contains IPlugin interface
  2. Plugin1 - contains a PluginOne class that implements IPlugin interface
  3. Plugin2 - contains a PluginTwo class that implements IPlugin interface
  4. PluginTest - contains the main program

Here is the very simple code for IPlugin interface and plugin implementations:

C#
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:

C#
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.

License

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