I’m going to discuss a problem that can arise when using widely diffused Dependency Containers in a huge code bases with complex type hierarchies, and a different perspective on how to deal with it that doesn’t involve complex, verbose and error-prone dependency configurations.
(Describing what Dependency Injection is and what is a Dependency Container is beyond the scope of this article; we can just address the unaware reader to some external resources ( and )
Before going deep in the topic, I have to admit my strong background of static type checked languages, C++ particularly, and my general adversion to runtime exception for failed assertions, like failed runtime type casts: if I use a compiler and I need some particular interface in a method or class, I like to provide it objects of the least type requested, not a vague abstract interface to be down-casted at runtime; even more I feel this a kind of necessity in huge projects with a constant evolution and a really long life time, during which my memory can fail, whereas compiler will not.
After that mandatory introduction, let’s describe the scenario.
The main picture is a MVC 3 application using a custom dependency resolver, specifically StructureMap .
The very object of the failure is a data context, conceptually a database connection, that must exists in a single instance for every HTTP connection.
This data context is the leaf of a stratified hierarchy, composed by a tree of interfaces implemented by a correspondent tree of abstract and concrete classes.
In different parts of the library this context may (and is) referenced by any of its differents forms, either interface or class.
With StructureMap, we are forced to define different mappings for every form our leaf class can appear within our library, HTTP-context-scoped or not, leading to automatic construction of more than one instance (and also forcing us to remember or discover by failure every form of the class needed by our current application).
Obviously we can add overcome this approach by defining specific injection parameters, for each and every needed dependent class constructor, to use the specific instance provided by the container itself – my head aches just picturing that.
But what if the container would have been smarter and able to understand that, without an explicit and specific type mapping, a mapping from the root to the leaf of the hierarchy is suitable for every inner node / intermediate class ?
So we ended up in implementing this kind of logic and a simple Dependency Container, totally suitable for our needs, since we don’t need specific values; this container is so “smart” it is usually configured with two lines of code in our non-trivial applications.
A basic part of the container is a simple algorithm bound to System.Type as extension method, able to tell us if a type Derived is-a type Base, with the property that every type T is-a itself.
|
|
static class TypeExtensions
{
// tells if a type extends another one; a type does extends itself
public static Boolean Extends(this Type t, Type baseType)
{
return (selfOrBaseType != null && selfOrBaseType.IsAssignableFrom(t));
}
// tells if an object's type extends another type
public static Boolean IsA(this Type t, Type selfOrBaseType)
{
return (o != null && o.GetType().Extends(selfOrBaseType));
}
} |
Relying on this simple check method, we can define a special type of Dictionary able to find an implied mapping, rather than just a key association. This dictionary contains associations from requested to provided types, the latter associated with factory methods – this adds a degree of freedom to our creation process and is consistent with a cachable fallback to a default factory method. Note that since TryGetValue is not marked as virtual, we can’t refer to this class by one of its ancestor types (no dynamic binding allowed).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
/// <summary>
/// Type registry: when asked for a type T, it can answer with a type T'' registered for a type T'
/// such that T is-a T' and T'' is-a T, if not stated otherwise
/// </summary>
/// <example>B extends A implements I; register B bound to I; ask for A; get B</example>
class LambdaRegistry : Dictionary<Type, Tuple<Type, Func<Object>>>
{
/// <summary>
/// succeed when a direct or implied bond exists
/// </summary>
public bool TryGetValue(Type t, out Func<Object> result)
{
// look for a specific binding
Pair<Type, Func<Object>> rt = null;
if (base.TryGetValue(t, out rt)) {
result = rt.Second;
return true;
}
// look for an implied binding
foreach (var x in this) {
if (t.Extends(x.Key) && x.Value.Item1.Extends(t)) {
result = x.Value.Item2;
return true;
}
}
// fail
result = null;
return false;
}
} |
At this point, we are ready to realize the backbone of our Dependency Container: in our library it’s obviously a bit more complex since it can deal with singletons, named and scoped instances, various flavours type and factory method registrations, etc.
The basic instantiation algorithm however is realized by a pair of mutually recursive methods; the entry point is GetInstanceOrCreate, that looks up in an instance of the registry defined above to look for a suitable factory method; if no entry is found, it falls back to a default factory method, named BuildInstance that relies on reflection to build an object instance. BuildInstance, in turn, uses GetInstanceOrCreate to obtain instances of parameters for non-empty constructors.
Error handling has completely been omitted for code clarity.
Note that using the factory method as a default fallback allows us to just omit trivial type bindings and safelly use the InstanceBuilder class throughout the code without relying on per-library or any kind of subsequent runtime configurations – this has been our choice.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
class InstanceBuilder
{
/// <summary>
/// registry of type mappings: what type to create (values.first) when something is asked for (keys), using which factory (values.second)
/// </summary>
private LambdaRegistry _typeLambdaRegistry = new LambdaRegistry();
/// <summary>
/// default factory method
/// </summary>
private object BuildInstance(Type t)
{
foreach (var ctor in t.GetConstructors()) {
try {
var args = new List<object>();
foreach (var p in ctor.GetParameters()) {
var pDef = p.RawDefaultValue;
if (pDef != DBNull.Value) {
args.Add(pDef);
} else {
args.Add(this.GetInstanceOrCreate(p.ParameterType));
}
}
return ctor.Invoke(args.ToArray());
} catch (Exception e) {
// try with next constructor
continue;
}
}
// no constructor succeded, fail
throw new InvalidOperationException("Unable to create an instance of type " + t);
}
public object GetInstanceOrCreate(Type type)
{
// search for a suitable registration for type
if (_typeLambdaRegistry.TryGetValue(type, out factoryMethod)) {
return factoryMethod.Invoke();
}
// if type is not registered anywhere, just try to build it directly
return this.BuildInstance(type);
}
} |