Click here to Skip to main content
15,881,248 members
Articles / Programming Languages / C#

Expandable IoC Container

Rate me:
Please Sign up or sign in to vote.
4.93/5 (66 votes)
16 Mar 2018CPOL25 min read 109.7K   683   105   64
Learn how to create a very small (less than 3KB) yet very expandable IoC container

Background

I recently wrote the article Software Architecture in which I talked about the first "fix" to many frameworks and I think that such fix is usually required in many IoC containers.

But even if some of them don't need any fix, I usually work in companies that don't like to use external code (aside from the .NET Framework). Those companies are usually afraid of using "hard to maintain" or "discontinued" solutions and so, they avoid using IoC containers at all, as .NET doesn't come with a default one and creating an IoC container is seen as something hard, that no-one else will be able to maintain. And maybe they are right, as I usually see lots of articles explaining how to create IoC containers that are hard (to write and sometimes to use) and are usually limited, so someone will need to change its code, and will suffer to do it.

So my idea here is to show how we can create an easy to use IoC container using only .NET base classes, which will be really expandable, which means that to expand it to support different scenarios, you will not need to change its source code.

Note

I will not discuss what is an IoC container here or its advantages. If you need to understand what's an IoC container, then I suggest that you start by reading about Inversion of Control in Wikipedia.

Dictionaries

The heart of an IoC container is the mapping between a "request" and a "result generator".

For example, if we wanted to call factory.Create("Button"); we would want to map a string with a delegate capable of creating buttons but, as we probably want to be able to create other objects, we may want to have a function capable of creating objects, which in such a particular utilisation will create a button.

To do that, we can use a Dictionary<string, Func<object>>. But as we don't want to expose such a dictionary to the users, we may end-up with a class like this:

C#
public sealed class Factory
{
  private readonly Dictionary<string, Func<object>> _dictionary =
    new Dictionary<string, Func<object>>();

  public void Register(string name, Func<object> creator)
  {
    _dictionary[name] = creator;
  }
  public object Create(string name)
  {
    Func<object> creator = _dictionary[name];
    return creator();
  }
}
// Note, I've omitted null validations and checking if the name exists or not
// to make the code smaller. Of course in the final code, we should have the right
// parameter validations and exceptions.

With this simple solution, we can have a configurable factory, which we can say is already a kind of IoC container. After all, a code that does factory.Create("Button"); is probably expecting a Button instance, but it may be any subclass of Button.

Type Instead of String

But as you may already imagine, searching things by a string is not good, especially because we can have more than one class with the same name, but different namespace, and even refactoring may become harder. So, considering that IoC is usually based on interfaces and that even if we use a factory, we will usually know the expected type of an object, we can change the string used as the dictionary key by a Type.

So, we could have something like this:

C#
public sealed class Factory
{
  private readonly Dictionary<Type, Func<object>> _dictionary = 
                              new Dictionary<Type, Func<object>>();

  public void Register(Type type, Func<object> creator)
  {
    _dictionary[type] = creator;
  }
  public object Create(Type type)
  {
    Func<object> creator = _dictionary[type];
    return creator();
  }
}
// Note, I've omitted null validations and checking if the type exists or not
// to make the code smaller. Of course in the final code, we should have the right
// parameter validations and exceptions.

And with this new version, instead of doing:

C#
Button button = (Button)factory.Create("Button");

we would do:

C#
Button button = (Button)factory.Create(typeof(Button));

It may not seem to be a big improvement, as we would still need to cast the result to Button, but now we can already improve the solution to use generics. So, the Create method could look like this:

C#
public T Create<T>()
{
  Func<object> creator = _dictionary[typeof(T)];
  object untypedResult = creator();
  return (T)untypedResult;
}

And thanks to this improvement, the user could do this:

C#
var button = factory.Create<Button>();

Without having do to any casts and having to write the type (Button) only once. (Yeah, I used var on purpose, so I could write Button only once).

Compile Time Validations?

Up to this moment, there's a problem. The user could do something like this:

C#
factory.Register(typeof(Button), () => "Test");

And, as we can't analyze what's inside a delegate, we can't validate the result of such a delegate, so it will be registered and it will throw an exception when a Create is invoked, not when the Register is invoked.

So, to solve that, we can rewrite this class like this:

C#
public sealed class Factory
{
  private readonly Dictionary<Type, Delegate> _dictionary = new Dictionary<Type, Delegate>();

  public void Register<T>(Func<T> creator)
  {
    _dictionary[typeof(T)] = creator;
  }
  public T Create<T>()
  {
    Func<T> creator = (Func<T>)_dictionary[typeof(T)];
    return creator();
  }
}
// Note, I've omitted null validations and checking if the type exists or not
// to make the code smaller. Of course in the final code, we should have the right
// parameter validations and exceptions.

In this solution, we don't store the delegate as a Func<object> anymore. Some may believe that it would work as a Func<Button> can be seen as a Func<object>, but this will fail if value-types are used and, as I am not trying to put any constraints here, I changed the delegate type to Delegate.

Now the register is generic too and it receives a Func<T>, so it will not allow a Register<Button>(() => "Test") to be done, being validated at compile time.

And instead of casting the result on the create (which would cause boxing and unboxing for value types), we cast the delegate that's on the dictionary, and we will already have a typed result.

So now we can do:

C#
factory.Register(() => new Button());

Or the more explicit:

C#
factory.Register<Button>(() => new Button());

In fact, considering how factories and IoC containers are used, we are more likely to do something like:

C#
factory.Register<IButton>(() => new ButtonThatImplementsIButton());

And so, I don't like that we can't disable type inference as that may cause some confusion. But, well, nothing is perfect.

With the actual implementation, we already have a very powerful Factory/IoC Container/Service Locator, which I think you can see is very small.

With it, we can already create a factory instance at one part of the application, initialize it the way we want (creating the real classes or stub classes to do our unit tests, for example) and another part of the application can depend on the "interface" without knowing which will be the real class that would be created.

We can already create a big application that uses such a Factory. Maybe we would want to have a single factory instance acessible to the application, which would require some locking if the application is multi-threaded, but we could do that by writing another class that's responsible of being static and doing the locks so we don't need to change the actual class.

It could be something like:

C#
public static GlobalFactory
{
  private static readonly Factory _factory = new Factory();

  public static void Register<T>(Func<T> creator)
  {
    lock(_factory)
      _factory.Register(creator);
  }
  public static T Create<T>()
  {
    lock(_factory)
      return _factory.Create<T>();
  }
}

This new class doesn't have the best performance of all, but it would be a solution if we really want a global factory and we don't want to change our factory anymore.

Lacking a First Fix

I think there's a bigger problem with the actual factory, and I am not talking about thread-safety, speed or the lack of null-checking. It is not as expandable as it should be.

Going back to the article Software Architecture, the first fix I said that many frameworks need is an event before failing. In this particular code, our Create method will fail if a type is not found in the dictionary (and will use the dictionary exception until I put the right validation).

That is: If you do a Factory.Create<SomeType>() and there's no registration for such a type, it will fail. We can of course consider that as a misconfiguration, but imagine that we want to configure our factory to create instances of IList<T> with a List<T>. Would we configure all the possible T? That is, will we configure IList<int> to return a new List<int>(), a IList<string> with a new List<string> and all the possible configurations?

My answer is that we shouldn't do that. Also, trying to support a special registration to generic types will overcomplicate the code and will not be as powerful as it should. In fact, it will solve that problem but it could still fail in other situations (like trying to load the implementation from external assemblies only when they are requested for the first time).

To solve such a problem, it is enough to have an event. Having an event like this:

C#
public event EventHandler<ResolveCreationEventArgs> ResolveCreation;

Where to support it we will have the following type:

C#
public sealed class ResolveCreationEventArgs:
  EventArgs
{
  public Type RequestedType { get; internal set; }
  public object Result { get; set; }
}

Will be enough to solve the problem. Of course, we need to change the Create method, so it could be like this:

C#
public T Create<T>()
{
  Delegate untypedDelegate;

  if (_dictionary.TryGetValue(typeof(T), out untypedDelegate))
  {
    Func<T> creator = (Func<T>)untypedDelegate;
    return creator();
  }

  var handler = ResolveCreation;
  if (handler != null)
  {
    var args = new ResolveCreationEventArgs();
    args.RequestedType = typeof(T);
    handler(this, args);

    if (args.Result != null)
      return args.Result;
  }

  throw new InvalidOperationException
  ("It is not possible to find an instance to the given type: " + typeof(T).FullName);
}

Almost There

The Create method is bigger now, but I can't say it is too complex. With this solution, it is already possible to find the instance by loading external assemblies if needed.

Yet, I don't like this solution. The problem here may be related to performance. In fact, it doesn't need to exist if the handlers are well written, but I don't really want to count on that. For example, I could add a handler to try to load things from external assemblies and another one specialized for generic lists. The problem here is composed of the following factors:

  • If I really add the handler that looks for an external assembly and then one specialized for lists, it will first look for an assembly (involving disk reads) before using the implementation for lists that works in memory. But in fact, that may be wanted, as it is possible that there is an assembly with a specific implementation for a list type (like, a specific implementation for IList<MyObservatleType>).
  • There's no cache. So, if I create a list, considering the first problem, it will look for a file and then will use the handler for lists. When I create a second list, it will do all the job again.
  • Even if the handlers were in the inverse order, I can ask for a list, the first handler is executed, creates the result, and the second handler is executed and, if it doesn't check that there's a result, it will lose time trying to find an appropriate assembly.

In fact, I will leave the order problem untouched. If there are two event handlers that can generate results for the same type, it will be the responsibility of the code that initializes the factory to put the handlers in order.

But to solve the other problems, instead of calling the event to get a result, we will call the event to try to get a delegate, which we will also register in the dictionary so the next times we don't need to invoke the event again.

So, the ResolveCreationEventArgs will not have a Result property. It will be a Creator property, like this:

C#
public Delegate Creator { get; set; }

Or, even better, as we don't allow invalid delegates, we can put an immediate validation. Like this:

C#
private Delegate _creator;
public Delegate Creator
{
  get
  {
    return _creator;
  }
  set
  {
    if (value != null)
    {
      Type funcType = typeof(Func<>).MakeGenericType(RequestedType);
      if (value.GetType() != funcType)
        throw new InvalidOperationException
            ("The Creator must be of type: " + funcType.FullName);
    }

    _creator = value;
  }
}

And the Create method not only will use the new property and store the delegate in its dictionary when one is returned, it will also get the invocation list of the ResolveCreation, being able to stop as soon as one of the handlers gives a creator:

C#
public T Create<T>()
{
  Delegate untypedDelegate;
  if (!_dictionary.TryGetValue(typeof(T), out untypedDelegate))
  {
    var allHandlers = ResolveCreation;
    if (allHandlers != null)
    {
      var args = new ResolveCreationEventArgs();
      args.RequestedType = typeof(T);

      foreach(EventHandler<ResolveCreationEventArgs> 
              handler in allHandlers.GetInvocationList())
      {
        handler(this, args);
        if (args.Creator != null)
        {
          untypedDelegate = args.Creator;

          _dictionary.Add(typeof(T), args.Creator);

          break;
        }
      }
    }
  }

  if (untypedDelegate == null)
    throw new InvalidOperationException
    ("It is not possible to find an instance to the given type: " + typeof(T).FullName);

  Func<T> creator = (Func<T>)untypedDelegate;
  return creator();
}

And finally, we have a very expandable Factory/IoC Container/Service Locator that's capable of "lazy-loading" the creator delegates, losing time with the event only when a specific type is requested for the first time.

That means that it is already capable of working with generic types and even loading the implementation from external assemblies.

Of course, it is not coming ready to load data from external assemblies or to deal with generic types, but we can add such support without having to touch the source code of this class anymore.

So, a possible implementation handler that loads the types from external assemblies can be this:

C#
ioc.ResolveGet += (sender, args) =>
{
  string name = args.RequestedType.Name;
  if (!name.StartsWith("I"))
    return;

  name = name.Substring(1);

  string dllName = name + ".dll";
  if (!File.Exists(dllName))
    return;

  var externalAssembly = Assembly.LoadWithPartialName(name);
  var type = externalAssembly.GetTypes()[0];
  var newExpression = Expression.New(type);
  var funcType = typeof(Func<>).MakeGenericType(args.RequestedType);
  var lambda = Expression.Lambda(funcType, newExpression);
  args.Getter = lambda.Compile();
};

And one to deal with generic types can be this:

C#
ioc.ResolveGet += (sender, args) =>
{
  var type = args.RequestedType;
  if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IList<>))
  {
    var getListCreator = typeof(Program).GetMethod("GetListCreator");
    getListCreator = getListCreator.MakeGenericMethod(type.GetGenericArguments());
    args.Getter = (Delegate)getListCreator.Invoke(null, null);
  }
};
public static Func<IList<T>> GetListCreator<T>()
{
  return () => new List<T>();
}

A Matter of Names

You probably saw that I named the class Factory, but in the text I usually said Factory/IoC Container/Service Locator.

In fact, Factory is a bad name. Each creator delegate is a factory. But, well, they don't even need to be a factory.

Some IoC containers allow the user to configure if they want a "singleton instance" or if they want to create a new instance everytime. With the use of the "creator" delegate, we are free to create new instances everytime or to simply return the same instance all the time.

So, to correct the names, I decided to:

  • Rename the class to ExpandableIocContainer
  • Rename the Create method to Get and the creator parameters to getter
  • I added some methods for the sake of completeness
  • Well, there may be other renames and I decided to put the parameter validations that missed. So, the code is this:
C#
using System;
using System.Collections.Generic;

namespace ExpandableIocSample
{
  public sealed class ResolveGetEventArgs:
    EventArgs
  {
    public Type RequestedType { get; internal set; }

    private Delegate _getter;
    public Delegate Getter
    {
      get
      {
        return _getter;
      }
      set
      {
        if (value != null)
        {
          Type funcType = typeof(Func<>).MakeGenericType(RequestedType);
          if (value.GetType() != funcType)
            throw new InvalidOperationException
               ("The Getter must be of type: " + funcType.FullName);
        }

        _getter = value;
      }
    }
  }

  public sealed class ExpandableIocContainer
  {
    private readonly Dictionary<Type, Delegate> _dictionary = 
                                                new Dictionary<Type, Delegate>();

    public void Register<T>(Func<T> getter)
    {
      if (getter == null)
        throw new ArgumentNullException("getter");

      _dictionary[typeof(T)] = getter;
    }
    public bool Unregister<T>()
    {
      return _dictionary.Remove(typeof(T));
    }

    public Func<T> GetGetter<T>()
    {
      var getter = TryGetGetter<T>();

      if (getter == null)
        throw new InvalidOperationException
        ("It is not possible to find a getter to the given type: " + typeof(T).FullName);

      return getter;
    }
    public Func<T> TryGetGetter<T>()
    {
      Delegate untypedDelegate;
      if (!_dictionary.TryGetValue(typeof(T), out untypedDelegate))
      {
        var allHandlers = ResolveGet;
        if (allHandlers != null)
        {
          var args = new ResolveGetEventArgs();
          args.RequestedType = typeof(T);

          foreach (EventHandler<ResolveGetEventArgs> 
                   handler in allHandlers.GetInvocationList())
          {
            handler(this, args);
            if (args.Getter != null)
            {
              untypedDelegate = args.Getter;

              _dictionary.Add(typeof(T), args.Getter);

              break;
            }
          }
        }
      }

      Func<T> getter = (Func<T>)untypedDelegate;
      return getter;
    }
    public T Get<T>()
    {
      var getter = TryGetGetter<T>();

      if (getter == null)
        throw new InvalidOperationException
        ("It is not possible to find an instance to the given type: " + typeof(T).FullName);

      return getter();
    }

    public event EventHandler<ResolveGetEventArgs> ResolveGet;
  }
}

If you look at the new code, you can see that I've added the methods Unregister, GetGetter and TryGetGetter. The purpose of the last two methods is not to get the value directly, but to return the delegate that's used to get/create new values. So, if the caller wants to create more than one instance, it will be able to avoid unnecessary searches.

I did that only to have the fastest solution of all if needed, but for most scenarios where IoC containers are used, the performance difference can be ignored as dictionary searches are really fast.

Multi-threading, AOP, etc.

I really don't expect that the IoC container will be used by many threads, so I wrote it without any threading support in mind.

In fact, I am not making any thread-checks either, even if that goes against what I said in the Rock Solid Quality article, but that's because I think the easiest way to make it thread-safe is to create another class that does the locks before calling this class, and if I do thread checks, that will fail.

Also, the actual solution doesn't support AOP (Aspect-oriented programming). You can, of course, register your getter delegates creating the instances and then adding the other aspects before returning, but if you want to register the getters without caring about other aspects to then tell which aspects to add, then the actual class doesn't support it.

I thought about putting a Got event so you could change the result (adding the aspects) before returning, but I decided to make something simpler and more powerful:

Instead of using the ExpandableIoCContainer directly, I recommend you to use the IIocContainer interface. So, the actual change to the class was to make it use such interface, which is declared like this:

C#
public interface IIocContainer
{
  void Register<T>(Func<T> getter);
  bool Unregister<T>();

  T Get<T>();

  event EventHandler<ResolveGetEventArgs> ResolveGet;
}

By seeing the IoC container itself by an interface, you are free to replace the actual IoC container with one that may add the extra concerns (like locking to support multi-threading, AOP support, whatever). That is, you will be able to add concerns to your IoC container, which could possibly be used to add concerns to the generated results (yeah, you will use AOP to add AOP support to the items, great and confusing, right?).

In such an interface, I didn't put the TryGetGetter and GetGetter methods as I think those are too implementation specific and it would violate the purpose of a replaceable solution.

Service Locator vs Dependency Injection

One thing that most IoC containers have is Dependency Injection. Dependency Injection differs from a Service Locator because the target classes don't need to know that there's an IoC container. Things are simply "injected".

To better understand, this is how a Service Locator works:

C#
public class ClassWithoutDependencyInjection
{
  public ClassWithoutDependencyInjection()
  {
    OtherService = ServiceLocator.Get<IOtherService>();
  }

  public IOtherService OtherService { get; private set; }
}

And this is Dependency Injection:

C#
public class ClassWithDependencyInjection
{
  public ClassWithDependencyInjection(IOtherService otherService)
  {
    if (otherService == null)
      throw new ArgumentNullException("otherService");

    OtherService = otherService;
  }

  public IOtherService OtherService { get; private set; }
}

As you can see, the only difference is that the ClassWithoutDependencyInjection knows the ServiceLocator (has a reference to it) while in the second case, it doesn't know the service locator at all, but it requires an OtherService to be provided. The second solution is considered better design.

But I was just calling the class that I presented here as Ioc Container/Service Locator. So, is it limited?

And the answer is no. The only thing the class doesn't give you is an "automatic" way to fill constructor parameters but as everything is done by a delegate, you can register the creation of that ClassWithDependencyInjection using the IoC container. To do that, the registration will be something like this:

C#
ioc.Register<ClassWithDependencyInjection>
(
  () =>
  {
    IOtherService otherService = ioc.Get<IOtherService>();
    return new ClassWithDependencyInjection(otherService);
  }
);

And, as this is the code to configure the IoC container, you don't need to worry that there's a reference to the IoC container. The ClassWithDependencyInjection will support dependency injection correctly and the final users of the IoC container will not need to see how the instances of the ClassWithDependencyInjection are created, they will only ask for one. The only thing missing is a "register" where we only tell the type to be created and it discovers the parameters in the constructor and does the job automatically, but that is not a real requirement, only a "nice to have" feature.

Such a feature could be created very easily with Reflection, but it would be slow. So, to make the solution a little more complete, I decided to support such kind of registration using a very fast approach. But as I am not sure if there would be a better way of doing this in the future or even if you will really want to use it, I am going to put such a method in an extension class. Even if it seems strange that I am giving a single solution and using an extension method, the purpose is to keep the main class "untouched".

The code to support real dependency injection is this:

C#
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpandableIocSample
{
  public static class ExpandableIocContainerExtensions
  {
    // Note: I usually use my ReflectionHelper to be sure that I will get the right method
    // in case there's a future change (be it to the method name or even if a new overload
    // is added). But as I don't want to add any extra reference, I am using normal reflection.
    // If the get is renamed or overload, the next line should be corrected.
    private static readonly MethodInfo _getMethod = 
            typeof(ExpandableIocContainer).GetMethod("Get");

    public static void RegisterCallingConstructor<T>
           (this IIocContainer iocContainer, ConstructorInfo constructor=null)
    {
      RegisterCallingConstructor<T, T>(iocContainer, constructor);
    }

    public static void RegisterCallingConstructor<TPublic, TImplementation>
           (this IIocContainer iocContainer, ConstructorInfo constructor=null)
    {
      if (iocContainer == null)
        throw new ArgumentNullException("iocContainer");

      if (constructor == null)
      {
        var constructors = typeof(TImplementation).GetConstructors();
        if (constructors.Length == 0)
          throw new InvalidOperationException("The type " + 
          typeof(TImplementation).FullName + " doesn't have any public constructor.");

        if (constructors.Length == 1)
          constructor = constructors[0];
        else
        {
          var maxParameters = constructors.Select(c => c.GetParameters().Length).Max();

          constructor = constructors.Where
                        (c => c.GetParameters().Length == maxParameters).Single();
        }
      }

      var iocContainerExpression = Expression.Constant(iocContainer);

      var parameters = constructor.GetParameters();
      int parameterCount = parameters.Length;
      var arguments = new Expression[parameterCount];
      for(int i=0; i<parameterCount; i++)
      {
        var parameter = parameters[i];
        var typedGet = _getMethod.MakeGenericMethod(parameter.ParameterType);
        var argument = Expression.Call(iocContainerExpression, typedGet);
        arguments[i] = argument;
      }

      var newExpression = Expression.New(constructor, arguments);
      var lamda = Expression.Lambda<Func<TPublic>>(newExpression);
      var implementedDelegate = lamda.Compile();
      iocContainer.Register<TPublic>(implementedDelegate);
    }
  }
}

And so, to register a type on the container that will use the container itself to fill the constructor parameters, it is enough to do:

C#
iocContainer.RegisterCallingConstructor
</*PossibleInterfaceHere,*/ ClassWithDependencyInjection>();

And as this is a nice to have feature, I made it work by searching for the constructor with most parameters, so your constructor can be overloaded. But trying not to limit the solution, if you don't want to use the constructor with most parameters, you are free to give the right constructor as parameter.

Comparison

I usually see people comparing the speed of IoC containers. I already believed this solution was fast by its simplicity. To test it, I compared it with SimpleInjector and, well, when mine was taking 5 seconds to access singleton, SimpleInjector was taking 7. I did some other tests and the difference was equivalent.

But my purpose is not to give a full performance comparison. What I want is to compare certain features found in common IoC containers.

Transient / Singleton

By using delegates, we can easily support both transient and singleton modes.

A singleton mode is nothing more than simply returning the same item all the time, so it could be done like this:

C#
SomeType instance = new SomeType();
iocContainer.Register<SomeType>(() => instance);

And the transient, well, it is even simpler:

C#
iocContainer.Register<SomeType>(() => new SomeType());

Passing Non-IoC Values to the Constructor

A problem with some IoC containers is that they want to fill all the constructor parameters using their own resolution.

But by the simplicity of delegates, we can mix IoC returned values and fixed values.

It is as easy as this:

C#
iocContainer.Register<SomeType>(() => 
   new SomeType(iocContainer.Get<SomeReference>(), "Our non-IOC value"));

In such line, the iocContainer.Get<SomeReference>() will use the IoC container to resolve the reference while the "Our non-IOC value", well, it is not coming from the IoC.

Passing Parameters to the Get Method

I just talked about passing non-IoC values to a constructor call, but that value was still a "fixed value at the registration". What about a value that I want to give when calling the Get() method?

For example, something like:

C#
iocContainer.Get<ISecureName>("name here");

Is it possible?

And the answer is: Yes, but not directly.

I thought about giving an extra parameter to the Get method, which could then be passed down to every delegate. But that would also force the delegates to receive an extra parameter or to "wrap" one delegate without parameters with a delegate that receives and ignores a parameter.

This will reduce performance and will compromise the Unregister method. Also, it is hard to tell the right parameter to give when you are constructing an object that has many dependencies and so it is constructing many sub-objects.

So, the "work around" to pass parameters to the Get method which can finally be used to create the right instance is to use "context" variables or, more precisely, [ThreadStatic] variables.

The basic idea is: You set the [ThreadStatic] variables you call the get method and, to avoid leaks, you clear the [ThreadStatic] variables. I am saying [ThreadStatic] variables as this is how "context" variables are created. They are "static" (so accessible by any instance) but not shared with other threads, so there's no risk of threading issues.

As this is probably ugly, if you really need to pass specific parameters, it is probably good to create a better solution that hides the use of such [ThreadStatic] variable, but almost every "context" is created by using [ThreadStatic] variables.

So, the most naive solution would be:

C#
SecureNameContext.Value = "name here";
try
{
  iocContainer.Get<ISecureName>();
}
finally
{
  SecureNameContext.Value = null;
}
// And the SecureNameContext.Value should be a 
// [ThreadStatic] field or a property that reads/writes 
// a [ThreadStatic] field.

But we could improve it to look more like:

C#
using(new SecureNameContext("name here"))
  iocContainer.Get<ISecureName>();

And of course, there's a lot more we can improve. So, instead of trying to give the solution, I will let you do it if you need.

Attributes

This container doesn't use any attributes because, well, it doesn't need to.

In my opinion, having attributes like [Import] is almost equivalent to calling the service locator at the constructor of the class. The difference is that you are putting that line of code "outside" a property instead of putting that line of code inside the constructor (which usually makes things harder to understand, not easier).

So, if you want the right dependency injection, your objects should ask for their dependencies on the constructor and then the solution to call the constructor filling its values will work. And, of course, if you need to fill properties instead of calling the constructor, this IoC will allow you to do it, as in the delegate that you give, you can create the required object, fill its properties (manually or by using the IoC container again) and finally return it.

Note: In the sample where I've shown the use of a ServiceLocator, I used it like a static class. But if you receive the service locator as a parameter, by its interface, you have a solution that's better than using [Attributes]. Surely, you will have a reference to the assembly where the interface is declared, but you will be able to replace one IoC container (if you really want to) by simply creating an adapter to the new IoC container, so you can concentrate the change to one place. But by using an attribute, you will need a way to tell the new IoC container how to use that attribute (if that's possible) or you will need to make the appropriate changes at every place where the attribute was used.

Many Registrations "At Once"

Many IoC containers use fluent methods to register all the types that match a specific criteria from an assembly.

Well, this class doesn't have any fluent methods but it is possible to add them as extension methods. And in fact, it is possible to eager-load all the needed types or to lazy-load them.

In any case, with the IoC container as is, you should write your code to search for types in an assembly. But if you want eager loading, you will probably do something like a foreach over all the types in an assembly and, if they are valid, you will immediately register them. If you want lazy loading, you will only add a handler to the ResolveGet event, which will try to find the right delegate for a type only when it is asked.

In fact, the "solution" provided in the sample to load external assemblies can be changed to look for types on the actual assembly by removing the I from the interface names. This way we could easily register all the implementations for the interfaces used in the sample with a single "registration".

It is important to note that many IoC containers have only eager-loading solutions. But now imagine that, for some reason, all the libraries a company has are in the same folder and you don't know a specific rule to filter the right ones, but you know that when you ask for an implementation, it is always in a library named as the namespace of the interface. An eager loading solution may end-up loading all the assemblies, while the lazy loading will only load the MyNamespace when I ask for a MyNamespace.ISomething. If I only use items from the MyNamespace, then I will only load a single assembly. Much better, isn't it?

Recursivety

The actual solution doesn't support recursive references. It will end-up causing a StackOverflowException.

In fact, it is impossible to an object A reference an object B from constructor while the object B references the object A from constructor. In such an event, at least one of the objects should receive the reference to the other by a property set. So, the right approach will be to create A, create B, and only then fill the references from one to the other.

And doing that is possible, but while registering the objects, it's up to you to create both the objects before filling the properties. To the final user, they can ask for A and receive A with a B that references back the A, but there's nothing helping you do that automatically. Unfortunately, this is a real limitation. Probably, it is possible to solve it by writing a better RegisterCallingConstructor method (or, in fact, a RegisterCallingConstructorAndFillingProperties, or whatever you name it), and here's another reason I wanted such method in a separate class. The main class is less susceptible to changes without such a method.

Lifetime Management

The actual implementation doesn't keep track of the created objects, so it doesn't have a lifetime management for the created objects. But as happens with multi-threading and AOP, it is possible to create another class that uses this IoC container to create the objects and then store the created objects on its own, also having extra methods to tell when those objects should die (and so, forcing "referenced objects" that aren't in use anymore to die/be disposed).

Such solution would also be able to deal with "contexts", effectively returning the same instance of objects of a given type even when they are transient. That is, it would be possible to do something like:

C#
using(iocContainerWithLifetimeManagement.CreateContext())
{
  var a1 = iocContainerWithLifetimeManagement.Get<A>();
  var a2 = iocContainerWithLifetimeManagement.Get<A>();
}

And both a1 and a2 could be the same instance, even if they are written as transient. Of course, outside the context, their transient nature will be kept and a good container can know how to work with transient objects bound to the context and transient objects not bound to the context but, again, this is not provided now.

Named Registrations

In the book, Dependency Injection with Unity (page 33), there's an example like this:

C#
container
  .RegisterType<ISurveyAnswerStore, SurveyAnswerStore>(
    new InjectionFactory((c, t, s) => new SurveyAnswerStore(
      container.Resolve<ITenantStore>(),
      container.Resolve<ISurveyAnswerContainerFactory>(),
      container.Resolve<IMessageQueue<SurveyAnswerStoredMessage>>(
      "Standard"),
      container.Resolve<IMessageQueue<SurveyAnswerStoredMessage>>(
      "Premium"),
      container.Resolve<IBlobContainer<List<string>>>())));

The important part is the use of the "Standard" and "Premium" parameters given to the Resolve() method.

In fact, my very first solution, which used strings to find the delegates, was prepared to support this situation while the actual one, that uses types, is not. Maybe to support it, I should go back to use string registrations and I could use the Type.FullName as the default registration, while a registration with a name could use something like Type.FullName + ":" + the user given name.

So, my expandable solution really has a limitation by one of the decisions I did just in the beginning. I can, of course, change that easily, but I will be changing the code I don't want to change anymore. But to defend myself, I should say that I consider the named registrations a bad practice.

If you can search for a registration of a type with a different name, then why not have a new type? That way, you will be able search for the IMessageQueue<T> or for the IPremiumMessageQueue<T>, which can even have new methods if needed and will have the advantage of being refactor friendly.

Conclusion

As you may have just seen from the comparison, this IoC container is different than the most popular ones, especially by not using terms like "transient" and "singleton" in the registration.

As I see it, it doesn't have real limitations but it is not feature complete. Personally, I don't see a reason to use other solutions, as I think many of them have blocking constraints (like requiring attributes on the classes or simply being incapable of loading external assemblies when needed) even when they have some of those extra features.

And, in my particular case, I know that I can add many of those missing features when needed. I am not doing that right now because my purpose was to show how a simple solution can be really expandable, thanks to the right interfaces and events. Adding the other features will only make the article more complex than needed. Maybe in the future, I can add those features and present separate articles to explain how each one of those work.

Well, I hope that you give this IoC container a try if you like using IoC containers (or if you want to start using them now). And even if you don't use it, I hope it is good to understand how an IoC container works and to understand how to make "expandable components" in general.

Extra - Final Object versus Aggregated Object

I started the article saying that 'The heart of an IoC container is the mapping between a "request" and a "result generator"', but then I gave an entire solution that uses the Type as the input and also as the type of object that should be created.

This gives the possibility to call Get giving a generic type and having the guarantee that the result will be of that type.

But that's a limitation related to how many IoC containers work: They expect the input type to be the same as the output type. But there are many situations that don't work like that.

In fact, the entire concept of data-templates used in WPF is in such a situation: We already have the data, and we want the "visual representation" of such data. Our input type is the type of the data, but our result is the template that shows such data.

It is not hard to implement such a solution either, but it is important to note that such a solution will be less typed than the actual one when we expect the input type and the output to be the same. So, this solution is not really limited, it has a different purpose. But I think that I will leave such an alternative solution as an exercise for the readers.

History

  • 5th December, 2013: Initial version

License

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


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions

 
GeneralRe: what about MEF Pin
Tomaz Koritnik20-Mar-18 1:48
Tomaz Koritnik20-Mar-18 1:48 
GeneralRe: what about MEF Pin
Paulo Zemek20-Mar-18 7:56
mvaPaulo Zemek20-Mar-18 7:56 
GeneralRe: what about MEF Pin
Tomaz Koritnik20-Mar-18 23:43
Tomaz Koritnik20-Mar-18 23:43 
Questionwow Pin
BigMax6-Jan-14 2:26
professionalBigMax6-Jan-14 2:26 
AnswerRe: wow Pin
Paulo Zemek6-Jan-14 3:10
mvaPaulo Zemek6-Jan-14 3:10 
GeneralAs usual Pin
Sergey Morenko4-Jan-14 0:49
professionalSergey Morenko4-Jan-14 0:49 
GeneralRe: As usual Pin
Paulo Zemek4-Jan-14 5:01
mvaPaulo Zemek4-Jan-14 5:01 
GeneralMy vote of 5 Pin
Monjurul Habib17-Dec-13 5:19
professionalMonjurul Habib17-Dec-13 5:19 
nice work!
GeneralRe: My vote of 5 Pin
Paulo Zemek17-Dec-13 5:21
mvaPaulo Zemek17-Dec-13 5:21 
GeneralMy vote of 5 Pin
S. M. Ahasan Habib8-Dec-13 20:55
professionalS. M. Ahasan Habib8-Dec-13 20:55 
GeneralRe: My vote of 5 Pin
Paulo Zemek9-Dec-13 3:08
mvaPaulo Zemek9-Dec-13 3:08 
GeneralMy vote of 5 Pin
Volynsky Alex7-Dec-13 11:46
professionalVolynsky Alex7-Dec-13 11:46 
GeneralRe: My vote of 5 Pin
Paulo Zemek7-Dec-13 11:56
mvaPaulo Zemek7-Dec-13 11:56 
GeneralRe: My vote of 5 Pin
Volynsky Alex7-Dec-13 12:00
professionalVolynsky Alex7-Dec-13 12:00 
GeneralLet's not forget to check our dictionary Pin
Master.Man19806-Dec-13 4:17
Master.Man19806-Dec-13 4:17 
GeneralRe: Let's not forget to check our dictionary Pin
Paulo Zemek6-Dec-13 4:40
mvaPaulo Zemek6-Dec-13 4:40 
GeneralRe: Let's not forget to check our dictionary Pin
Master.Man19806-Dec-13 4:46
Master.Man19806-Dec-13 4:46 
GeneralRe: Let's not forget to check our dictionary Pin
Paulo Zemek6-Dec-13 4:52
mvaPaulo Zemek6-Dec-13 4:52 
GeneralRe: Let's not forget to check our dictionary Pin
Master.Man19806-Dec-13 7:37
Master.Man19806-Dec-13 7:37 
GeneralRe: Let's not forget to check our dictionary Pin
Paulo Zemek6-Dec-13 8:23
mvaPaulo Zemek6-Dec-13 8:23 
GeneralRe: Let's not forget to check our dictionary Pin
Master.Man198010-Dec-13 7:11
Master.Man198010-Dec-13 7:11 
GeneralRe: Let's not forget to check our dictionary Pin
Paulo Zemek10-Dec-13 7:30
mvaPaulo Zemek10-Dec-13 7:30 
GeneralRe: Let's not forget to check our dictionary Pin
Master.Man198010-Dec-13 8:33
Master.Man198010-Dec-13 8:33 
GeneralRe: Let's not forget to check our dictionary Pin
Paulo Zemek10-Dec-13 14:46
mvaPaulo Zemek10-Dec-13 14:46 
GeneralMy vote of 3 Pin
Master.Man19806-Dec-13 4:15
Master.Man19806-Dec-13 4:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.