Saturday, January 17, 2009

Dependency injection in .NET CF 1.0

Recently I got involved in building an application for mobile devices. The problem is that they are old Pocket PC 2002 devices, so the only option to write managed code is to use also old .NET CF 1.0. Don’t even get me started on how limited it is and how much pain it is to work with VS2003 – do you remember the times when you had to add namespace imports by hand? Without ReSharper it’s almost unbearable.

Anyway I’m responsible for the design of the solution. I thought it would be nice to have DI container, but there are some constraints on the mobile platform:

  • Performance – There’s no possibility to use reflection extensively without running into performance problems.
  • Limited BCL - .NET CF 1.0 base class library is only a small subset the .NET 1.1 BCL, not to mention .NET 2.0

Taking this into account I thought it would be best to implement a really simple solution myself. The features of my simple container are:

  • Mapping interfaces to implementing types
  • Registering types both as singleton and as transient instance
  • Registering existing instances
  • Resolving both mapped interfaces and unmapped types
  • Injecting dependencies into constructors (recursively)
  • Fluent registration syntax
  • Reflection usage reduced to a minimum

This set of functionalities should allow me to work comfortably with dependency injection and keep the container as simple as possible. Here is the code, first the container interface:

public interface IContainer
{
    IContainer Register(Type mapFrom, Type mapTo);
    IContainer RegisterSingleton(Type mapFrom, Type mapTo);
    IContainer RegisterSingleton(Type mapFrom, object toInstance);
    object Resolve(Type type);
}

Register method allows you to register a mapping of a transient objects (instantiated on every resolve). RegisterSingleton is overloaded and allows you to register either a type mapping or type to existing instance mapping as a singleton (instantiated only once). Resolve returns instance of requested type – either mapped or not.

Now TypeItem class used to store type mappings (in a Hashtable):

public class TypeItem
{
    private Type targetType;

    public Type TargetType { get { return targetType; } }

    private bool sigleton;

    public bool Singleton { get { return sigleton; } }

    public TypeItem(Type targetType, bool singleton)
    {
        this.targetType = targetType;
        this.sigleton = singleton;
    }
}

And the container itself:

public class Container : IContainer
{
    private Hashtable typeMap = new Hashtable();
    private Hashtable singletonCache = new Hashtable();
    private ArrayList targetTypeList = new ArrayList();

    #region IContainer Members

    public IContainer Register(Type mapFrom, Type mapTo)
    {
        CheckRegistrationConditions(mapFrom, mapTo, true);

        TypeItem item = new TypeItem(mapTo, false);
        AddItem(mapFrom, item);
        return this;
    }

    public IContainer RegisterSingleton(Type mapFrom, Type mapTo)
    {
        CheckRegistrationConditions(mapFrom, mapTo, true);

        TypeItem item = new TypeItem(mapTo, true);
        AddItem(mapFrom, item);
        return this;
    }

    public IContainer RegisterSingleton(Type mapFrom, object toInstance)
    {
        CheckRegistrationConditions(mapFrom, null, false);
        Guard.AgainstNullArgument(toInstance, "toInstance");

        TypeItem item = new TypeItem(toInstance.GetType(), true);
        if(targetTypeList.Contains(item.TargetType))
        {
            throw new ArgumentException(
                "Cannot register singleton because another singleton mapping " + 
                "uses the type of the instance",
                "toInstance");
        }
        AddItem(mapFrom, item);
        singletonCache.Add(item.TargetType, toInstance);
        return this;
    }

    public object Resolve(Type type)
    {
        Guard.AgainstNullArgument(type, "type");

        if (typeMap.ContainsKey(type))
        {
            // mapped type
            TypeItem item = typeMap[type] as TypeItem;

            if (item.Singleton && singletonCache.ContainsKey(item.TargetType))
                return singletonCache[item.TargetType];

            object instance = BuildInstance(item.TargetType);

            if (item.Singleton)
                singletonCache.Add(item.TargetType, instance);

            return instance;
        }
        else
        {
            // unmapped type
            if (!type.IsClass || !type.IsPublic)
                throw new TypeResolveException(
                    "The type " + type.Name + 
                    " is neither mapped nor can it be instantiated");

            return BuildInstance(type);
        }
    }

    #endregion

    private void CheckRegistrationConditions(Type mapFrom, Type mapTo, bool mapToValid)
    {
        Guard.AgainstNullArgument(mapFrom, "mapFrom");
        if (mapToValid)
        {
            Guard.AgainstNullArgument(mapTo, "mapTo");
            if (!mapTo.IsClass || !mapTo.IsPublic)
                throw new ArgumentException("The type " + mapTo.Name + 
                    " cannot be instantiated");
        }

        if (typeMap.ContainsKey(mapFrom))
            throw new ArgumentException("Provided type is already mapped", "mapFrom");
    }

    private void AddItem(Type mapFrom, TypeItem item)
    {
        typeMap.Add(mapFrom, item);
        targetTypeList.Add(item.TargetType);
    }

    private object BuildInstance(Type type)
    {
        ConstructorInfo[] constructors = type.GetConstructors(
            BindingFlags.Public | BindingFlags.Instance);

        if (constructors.Length == 0)
            throw new TypeResolveException("The type " + type.Name +
                " has no public constructors");

        ParameterInfo[] parameters = constructors[0].GetParameters();
        object[] paramValues = new object[parameters.Length];

        for (int i = 0; i < parameters.Length; i++)
        {
            paramValues[i] = Resolve(parameters[i].ParameterType);
        }

        return constructors[0].Invoke(paramValues);
    }

}

As you might noticed, the BuildInstance method just takes the first public constructor returned by GetConstructors and uses it to create instance. This is enough for my scenario, but you might want to mark injection constructor with a custom attribute or select constructor basing on some criteria (ex. most resolvable parameters).

There are also some tests for the container, but I won’t be posting them here.

DotNetKicks Image

No comments:

Share