Build yourself a portable Dependency Injection container

By Rob on Tuesday, September 30, 2008

1 Comment

Filed Under: .Net

It’s been too long since I promised to post this. But it’s worth it, I promise you. If you just want to grab the code, it’s at the bottom of this post.

What we have here is another do it yourself dependency injection container. I release it to you, dear world, free of charge, warranty or care.

Pros:

  • You can paste the code straight into your project – no DLL dependencies (easy to deploy, get approved, source control)
  • It won’t bulk up your silverlight application
  • It performs super-fast if you’re using it to resolve singletons
  • It can do lots of the things that the “real” DI containers can do (named parameters, value parameters, lifestyle choices)
  • It’s got a fluent interface
Cons
  • There’s a bunch of stuff that the real containers can do that mine can’t (consider this DIY container a “gateway drug”). That said, I seem to have covered three of Oren’s four basic requirements, and i think it’s pretty easy to use…
  • If you are doing a lot of transient resolves (say per each page request) you will find reflection is just too slow.
  • This version depends on LINQ and lambdas, so it’s VS 2008 only.

the tests:

[TestClass]
public class DemoContainerTest
{
    [TestMethod]
    public void NamedRegistration()
    {
        Container c = new Container();
        c.Register<IMathNode, Zero>("zero");
        IMathNode m = c.Resolve<IMathNode>("zero");
        Assert.AreEqual(0, m.Calculate());
    }

    [TestMethod]
    public void AnonymousRegistration()
    {
        Container c = new Container();
        c.Register<IMathNode, Zero>();
        IMathNode m = c.Resolve<IMathNode>();
        Assert.AreEqual(0, m.Calculate());
    }

    [TestMethod]
    public void AnonymousSubDependency()
    {
        Container c = new Container();
        c.Register<IMathNode, Zero>();
        c.Register<IFormatter, MathFormatter>();
        IFormatter m = c.Resolve<IFormatter>();
        Assert.AreEqual("$0.00", m.Format("C2"));
    }

    [TestMethod]
    public void WithValue()
    {
        Container c = new Container();
        c.Register<IMathNode, Number>("five").WithValue("number", 5);
        int i = c.Resolve<IMathNode>("five").Calculate();
        Assert.AreEqual(5, i);
    }

    [TestMethod]
    public void NamedSubDependency()
    {
        Container c = new Container();
        c.Register<IMathNode, Number>("five").WithValue("number", 5);
        c.Register<IMathNode, Number>("six").WithValue("number", 6);
        c.Register<IMathNode, Add>("add").WithDependency("m1", "five").WithDependency("m2", "six");
        int i = c.Resolve<IMathNode>("add").Calculate();
        Assert.AreEqual(11, i);
    }

    [TestMethod]
    public void NamedSubDependencyOutOfOrder()
    {

        Container c = new Container();
        c.Register<IMathNode, Add>("add").WithDependency("m1", "five").WithDependency("m2", "six");
        c.Register<IMathNode, Number>("five").WithValue("number", 5);
        c.Register<IMathNode, Number>("six").WithValue("number", 6);
        int i = c.Resolve<IMathNode>("add").Calculate();
        Assert.AreEqual(11, i);
    }

    [TestMethod]
    public void Singleton()
    {
        Container c = new Container();
        c.Register<IMathNode, Zero>().AsSingleton();
        Assert.AreSame(c.Resolve<IMathNode>(), c.Resolve<IMathNode>());
    }

    [TestMethod]
    public void NonSingleton()
    {
        Container c = new Container();
        c.Register<IMathNode, Zero>();
        Assert.AreNotSame(c.Resolve<IMathNode>(), c.Resolve<IMathNode>());
    }

    public interface IFormatter
    {
        string Format(string format);
    }

    public class MathFormatter : IFormatter
    {
        private readonly IMathNode math;

        public MathFormatter(IMathNode math)
        {
            this.math = math;
        }

        public string Format(string format)
        {
            return math.Calculate().ToString(format);
        }
    }

    public interface IMathNode
    {
        int Calculate();
    }

    public class Zero : IMathNode
    {
        public int Calculate()
        {
            return 0;
        }
    }

    public class Number : IMathNode
    {
        private int number;

        public Number(int number)
        {
            this.number = number;
        }

        public int Calculate()
        {
            return number;
        }
    }

    public class Add : IMathNode
    {
        private IMathNode m1, m2;

        public Add(IMathNode m1, IMathNode m2)
        {
            this.m1 = m1;
            this.m2 = m2;
        }

        public int Calculate()
        {
            return m1.Calculate() + m2.Calculate();
        }
    }

}

The container:

The way this bad boy works is by storing dictionary of services as “Func<object>”s. These are keyed by name, which is usually provided by the Resolve method. If the parameterless overload of resolve is used, then the service name is looked up in the dictionary serviceNames. This simply stores the first  ever registration of a service type’s name. If the nameless Register method is used, then a random name is generated.

Whenever you register a component, you get back a “dependency manager” object. That object lets you specify further configuration on your component via a fluent interface. It contains most of the logic for resolving an object, and manages the parent container’s Func<object> for resolving that dependency.

public class Container
{
    protected readonly Dictionary<string, Func<object>> services = new Dictionary<string, Func<object>>();
    protected readonly Dictionary<Type, string> serviceNames = new Dictionary<Type, string>();

    public DependencyManager Register<S, C>() where C : S
    {
        return Register<S, C>(Guid.NewGuid().ToString());
    }

    public DependencyManager Register<S, C>(string name) where C : S
    {
        if (!serviceNames.ContainsKey(typeof(S)))
        {
            serviceNames[typeof(S)] = name;
        }
        return new DependencyManager(this, name, typeof(C));
    }

    public T Resolve<T>(string name) where T : class
    {
        return (T)services[name]();
    }

    public T Resolve<T>() where T : class
    {
        return Resolve<T>(serviceNames[typeof(T)]);
    }

    public class DependencyManager
    {
        private readonly Container container;
        private readonly Dictionary<string, Func<object>> args;
        private readonly string name;

        internal DependencyManager(Container container, string name, Type type)
        {
            this.container = container;
            this.name = name;

            ConstructorInfo c = type.GetConstructors().First();
            args = c.GetParameters()
                .ToDictionary<ParameterInfo, string, Func<object>>(
                x => x.Name,
                x => (() => container.services[container.serviceNames[x.ParameterType]]())
                );

            container.services[name] = () => c.Invoke(args.Values.Select(x => x()).ToArray());
        }

        public DependencyManager AsSingleton()
        {
            object value = null;
            Func<object> service = container.services[name];
            container.services[name] = () => value ?? (value = service());
            return this;
        }

        public DependencyManager WithDependency(string parameter, string component)
        {
            args[parameter] = () => container.services[component]();
            return this;
        }

        public DependencyManager WithValue(string parameter, object value)
        {
            args[parameter] = () => value;
            return this;
        }
    }
}

One Comment for this post

[...] always provide a simple design-time subclass that will populate everything you want. While Dependency Injection is awesome, we aren’t all the architects on our projects, and you might get handed a [...]

Posted onDecember 18th, 2009 at 7:33 pm

Leave a comment

Name (required) Comment
Mail (required)
Website