Making Your Own 8K Homegrown Inversion of Control Container

If you develop software, chances are you you’ve worked with Inversion of Control containers and the Dependency Injection pattern. Many frameworks exist to address how to marry concrete implementations to abstract classes and interfaces. A few popular ones on the .NET platform include:

Download the Source for this Example

There are a few reasons why I decided to play with the concept of building my own dependency injection solution. First, I am never content to just “talk to the framework.” I like to know “what lies beneath.” With so many open source projects, it’s not difficult to go behind the scenes and understand just how these frameworks are glued together. Second, sometimes a larger framework in a smaller project just doesn’t make sense (sort of like hitting a tack with a sledgehammer) so understanding some principles of how to roll my own lightweight container will come in handy. Of course, I could also just bust out a simple Service Locater or Factory and achieve similar results, so that brings me to my third point: it’s fun.

What I managed to throw together was and 8K DLL that performs auto-discovery. Nothing fancy here. It simply scans the assemblies you send it for concrete implementations of interfaces and types you ask for. Again, this is more of an exercise of pulling back the cover and looking inside the CLR than an attempt to rival the robust, mature frameworks out there.

The result doesn’t support generics, isn’t attribute-based, doesn’t have support for lifetime management (i.e. singletons), doesn’t have named containers and doesn’t perform stable composition. It does, however, do the trick of marrying a simple implementation to an interface or an abstract class and knows how to hierarchically wire up dependencies as deep as it needs to go.

With this small solution, I was able to make a simple call to a method on an interface:

...
ResolverEngine.Resolve<ICoordinator>().Coordinate();
...

And also loop through several implementations of another interface:

...
ResolverEngine.ResolveAll<IPlugin>().ForEach(p => p.DoSomething());   
...

So how do we go from an interface to the actual implementation of that interface, and create an instance of a class that might have multiple parameters in a constructor we don’t know about before hand?

The way I decided to implement this was to store references to the assemblies we want to scan for dependencies (so that we’re not inspecting mscorlib if we don’t need to, for example), to resolve on demand but cache the resolutions for future look ups.

From that information, we can at least start with our assembly cache and our type-mapper cache (think of an interface or base class type mapping to multiple implementation types).

private static readonly List<Assembly> _assemblyCache = new List<Assembly>();
private static readonly Dictionary<Type, List<Type>> _typeCache = new Dictionary<Type, List<Type>>();

Now for the resolver. Here’s something interesting to sort out: generics only work on an actual type, not a variable pointing to a type. In other words:

// this is fine
IGenericInterface<MyClass> myClassInterface = Factory.GetInterface<MyClass>();

// this is not valid
Type type = typeof(MyClass); 
IGenericInterface<type> myClassInterface = Factory.GetInterface<type>(); 

To be able to recursively resolve dependencies (for example, when constructor injection is used), we’ll need to support the explicit runtime type name. Therefore, our shell for the resolve method ends up looking like this:

public static T Resolve<T>() where T : class
{
    return Resolve(typeof(T).AssemblyQualifiedName) as T;
}

public static object Resolve(string type) 
{
    Type srcType = Type.GetType(type);
    ...
}

Now we can use the generic call or the runtime call without issue.

The algorithm to resolve the type has two parts.

The first part determines how to map the current type to an implemented type. If the current type is a class then it’s simple: we want to instantiate it. If the current type is an interface or a base class, we need to scan the registered assemblies to find types that implement the base class or interface. We need to make sure they are public and not static.

The second part scans the type for the most verbose constructor (we don’t have to do it this way, but its the way many IoC containers work so I decided to implement it like this). Once we find the most verbose constructor, we need to recursively resolve the types specified in the constructor and then activate the instance with those parameters.

Let’s filter the types first.

private static Type[] _FilterTypes(Type srcType, Assembly a)
{
    return a.GetTypes().Where(
                t => t.IsClass && t.IsPublic && !t.IsAbstract && !t.IsInterface && 
                    (srcType.IsInterface && t.GetInterface(srcType.FullName) != null
                    || srcType.IsAbstract && t.IsSubclassOf(srcType))
                ).ToArray();
}

This reads “scan the assembly for all types that are public classes, are not abstract or interfaces, and either implement the source interface or derive from the base class.”

Once we have the mapped type, we need to find the most verbose constructor and then recursively satisfy the dependencies. The _Activate method does this for us:

private static object _Activate(Type t)
{
    object retVal = null; 

    // find the main constructor
    ConstructorInfo ci = (from c in t.GetConstructors() 
              orderby c.GetParameters().Length descending 
              where c.IsPublic && !c.IsStatic
              select c).FirstOrDefault();

    if (ci != null)
    {
        if (ci.GetParameters().Length == 0)
        {
            retVal = Activator.CreateInstance(t);
        }
        else
        {
            ParameterInfo[] parameterInfo = ci.GetParameters();

            object[] parameters = new object[parameterInfo.Length];

            int parameterIndex = 0;
            foreach (ParameterInfo parameter in parameterInfo)
            {
                parameters[parameterIndex++] = Resolve(parameter.ParameterType.AssemblyQualifiedName);
            }

            retVal = Activator.CreateInstance(t, parameters);
        }
    }
    return retVal; 
}

We grab the constructors that aren’t static and are public. If we have none or the one we find has no parameters, we simply activate the instance. Otherwise, we build an object collection and recursively resolve the dependencies, adding the resolved instances to the collection. We then activate the instance by passing in the collection, which finds and applies the collection to the appropriate constructor.

Now, we can back up to our resolution method and expand it:

public static object Resolve(string type) 
{
    Type srcType = Type.GetType(type);

    if (srcType == null || (!srcType.IsClass && !srcType.IsInterface && !srcType.IsAbstract))
    {
        throw new ArgumentException("type"); 
    }

    object retVal = null;

    if (!srcType.IsInterface && !srcType.IsAbstract)
    {
        retVal = _Activate(srcType);
    }
    else if (_typeCache.ContainsKey(srcType))
    {
        retVal = _Activate(_typeCache[srcType][0]);
    }
    else
    {
        foreach (Assembly a in _assemblyCache)
        {
            // get the mappable types in the assembly 
            foreach(Type t in _FilterTypes(srcType, a))
            {
                // activate and cache it
                object instance = _ActivateAndCache(srcType, t);       
         
                // if we were able to activate and it is appropriate, load it up
                if (instance != null)
                {
                    // finally, return value is the first type we come across
                    retVal = retVal ?? instance;
                }
            }
        }

    }
    return retVal; 
}

We check the type to make sure it is valid. If it’s a class type, we simply activate it. If it is an abstract class or interface, we first check the cache and take the first mapping (again, this is where frameworks offer much more because you can explicitly map which type you want, in our case to simplify we just grab the first available). If it’s not in the cache, we get all of the types and insert them into the cache with _ActivateAndCache.

The _ActivateAndCache method does a reality check on the instance to make sure it is assignable from the interface or abstract class. Only if this passes does it return the instance and cache it:

object retVal = null;

object instance = _Activate(t);                 
     
if (instance != null && srcType.IsAssignableFrom(instance.GetType()))
{
   retVal = instance;
   // cache it
}

return retVal; 

At this stage we’ve pretty much built all we need. The RegisterAssembly method does a little bit more than just store the assembly reference. Because we may have already cached a type, when a new assembly is registered we’ll need to scan the existing types and then add any implementations of those types from the new assembly. That code looks like this:

public static void RegisterAssembly(Assembly assembly)
{
    bool resolve = false; 

    lock (_assemblyCache)
    {
        if (!_assemblyCache.Contains(assembly))
        {
            _assemblyCache.Add(assembly);
            resolve = true;
        }                               
    }

    if (resolve)
    {
        foreach (Type type in _typeCache.Keys)
        {
            foreach (Type t in _FilterTypes(type, assembly))
            {
                _ActivateAndCache(type, t);
            }
        }
    }            
}

Finally, I added a few methods to resolve all instances instead of just one.

Where does that leave us? The test project introduces two very simple interfaces:

public interface IPlugin
{
    void DoSomething();
}

public interface ICoordinator
{
    void Coordinate();
}

In an implementation project, I set up a straightforward implementation of IPlugin, then a base class that implements the interface and another class that derives from the base class. This is all completely contrived to demonstrate what the resolver can do:

public class Plugin1 : IPlugin
{
    public void DoSomething()
    {
        Console.WriteLine("This is the first plugin.");
    }

}

public abstract class PluginBase : IPlugin
{

    public virtual void DoSomething()
    {
        Console.WriteLine("You found me."); 
    }

}

public class PluginMain : PluginBase
{
    public override void DoSomething()
    {
        Console.WriteLine("This is the override, now calling base class.");
        base.DoSomething();
    }
}

The coordinate class demonstrates how the resolver will recursively resolve dependencies. It takes in the interface as well as the base class:

public class Coordinator : ICoordinator
{
    IPlugin _interface;
    PluginBase _base;

    public Coordinator()
    {
    }

    public Coordinator(IPlugin interfaceInstance, PluginBase impl)
    {
        _interface = interfaceInstance;
        _base = impl;
    }

    public void Coordinate()
    {
        Console.WriteLine("Interface:");
        _interface.DoSomething();

        Console.WriteLine("Base class:"); 
        _base.DoSomething();
    }

}

Finally, I put another implementation of the plugin to the main program to show how it doesn’t resolve until that assembly is added. With all of these together, the main thread looks like this:

static void Main(string[] args)
{
    ResolverEngine.RegisterAssembly(typeof(PluginMain).Assembly);

    Console.WriteLine("Coordinator...");

    ResolverEngine.Resolve<ICoordinator>().Coordinate();
    
    Console.WriteLine("Plugins...");

    ResolverEngine.ResolveAll<IPlugin>().ForEach(p => p.DoSomething());             

    Console.WriteLine("Now registering this, and running plugins again...");

    ResolverEngine.RegisterAssembly(typeof(Program).Assembly);

    ResolverEngine.ResolveAll<IPlugin>().ForEach(p => p.DoSomething());           

    Console.ReadLine();
}

While it is fun to compile and run the application, the real power will come from stepping through in debug and watching the various steps of parsing out types and constructors. Looking at ConstructorInfo and ParameterInfo will reveal a lot about how C# interacts with the underlying CLR. Even if you never use this code, it will help you better understand and interact with the various dependency injection frameworks that are available and learn a little more about what they are doing for you behind the scenes. This only brushes the surface … pulling down an open source project and wading through the code will reveal how much more powerful these frameworks are.

Download the Source for this Example

Jeremy Likness

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X