Wednesday, August 08, 2007

Injection Doctor

We use in project X a slightly wrapped PicoContainer. The implementation of several service are registered in the container explicitely. Sometimes people forget to register it, and unfortunately the container fails with an "UnsatisfiedConstructor" error. The error appears only at run-time (we don't have test-coverage for container configuration), and it's misleading: if we need a class A which needs B,
and we forgot to register B, we get the error on class A (which is, per se, OK).

The solution (proposed) is to instantiate recursively all the services, and to build for each (failed) instantiation a histogram: the more points we get for a service, the "more" needed it is, the probability to
have there the "injection-problem" will be bigger. For the previous example A will get 1 point and B 2 points.

And here is the source:


public class Memoize {

private readonly Func _func;

public Memoize(Func func) {

_func = func;

}

private IDictionary _cache = new Dictionary();

public R Call(T value) {

R result;

if (_cache.ContainsKey(value)) {

result = _cache[value];

} else {

result = _func(value);

_cache[value] = result;

}

return result;

}

}


class InjectionExplorer {

private Func<IEnumerable<Type>, Type> _greediest_ctor_params;

public InjectionExplorer() {

_greediest_ctor_params = new Memoize<IEnumerable<Type>, Type>(GreediestConstructorParametersWorker).Call;

}

public IEnumerable<Type> RequiredTypes(Type t) {

return _greediest_ctor_params(t);

}

private ConstructorInfo Greediest(Type t) {

ConstructorInfo greediest = null;

int greediest_param_length = -1;

foreach (ConstructorInfo ctor in t.GetConstructors()) {

if (ctor.GetParameters().Length > greediest_param_length) {

greediest = ctor;

greediest_param_length = ctor.GetParameters().Length;

}

}

if (greediest == null) { throw new Exception("No ctor was found"); }

return greediest;

}

private IEnumerable<Type> GreediestConstructorParametersWorker(Type t) {

ConstructorInfo ctor = Greediest(t);

return Enumerables.Do.Map<Type, ParameterInfo>(ctor.GetParameters(), delegate(ParameterInfo info) { return info.ParameterType; });

}

}

class InjectionDoctor {

private readonly IEnumerable<Type> _types;

private readonly Func<object, Type> _instantiate;

private readonly InjectionExplorer _explorer;

private readonly IDictionary<Type, int> _failed_instantiations;

public InjectionDoctor(IEnumerable<Type> types, Func<object, Type> instantiate) {

_types = types;

_instantiate = instantiate;

_explorer = new InjectionExplorer();

_failed_instantiations = new Dictionary<Type, int>();

}

public void Examine() {

Enumerables.Do.ForEach(_types, TryInstantiate);

}

public void PrettyPrint() {

string print_format = "{0} -> {1}";

Console.WriteLine(print_format, "Type", "Failed Instatiation Count");

foreach (KeyValuePair<Type, int> failed_instantiation in SortFailedInstatiationsByFailedCount()) {

Console.WriteLine(print_format, failed_instantiation.Key, failed_instantiation.Value);

}

}

private IEnumerable<KeyValuePair<Type,int>> SortFailedInstatiationsByFailedCount() {

List<KeyValuePair<Type, int>> pairs = new List<KeyValuePair<Type, int>>();

foreach (KeyValuePair<Type, int> failed_instantiation in _failed_instantiations) {

pairs.Add(failed_instantiation);

}

pairs.Sort(delegate(KeyValuePair<Type, int> x, KeyValuePair<Type, int> y) { return y.Value.CompareTo(x.Value); });

return pairs;

}

private Type Instantiable(Type candidate) {

if (candidate.IsArray) {

return Instantiable(candidate.GetElementType());

}

return candidate;

}

private void TryInstantiate(Type candidate) {

Type instantiable = Instantiable(candidate);

if (HasFailedInstatiations(instantiable)) {

CountFailedInstantiation(instantiable);

return;

}

try {

_instantiate(instantiable);

} catch (Exception) {

CountFailedInstantiation(instantiable);

TryInstatiateRequiredTypes(instantiable);

}

}

private void TryInstatiateRequiredTypes(Type candidate) {

Enumerables.Do.ForEach(_explorer.RequiredTypes(candidate), TryInstantiate);

}

private bool HasFailedInstatiations(Type candidate) {

return _failed_instantiations.ContainsKey(candidate);

}

private void CountFailedInstantiation(Type candidate) {

if (! HasFailedInstatiations(candidate)) { _failed_instantiations[candidate] = 0; }

_failed_instantiations[candidate] += 1;

}

public int FailedInstatiations() {

if (! HasFailedInstatiations(typeof(T))) { return 0; }

return _failed_instantiations[typeof (T)];

}

}



Unit-Test sample:

class B {

private readonly C _c;

public B(C c) {

_c = c;

throw new Exception("poof");

}

}

class C {

public C() {

throw new Exception("poof");

}

}

class D {

}

class E {

private readonly C[] _cs;

public E(C[] cs) {

_cs = cs;

}

}

class A {

private readonly B _b;

private readonly D _d;

public A(B b, D d) {

_b = b;

_d = d;

}

}

[TestClass]

public class InjectionDoctorSpecifications {

private DefaultPicoContainer _container;

private InjectionDoctor _doctor;

[TestInitialize]

public void BeforeEachTest() {

_container = new DefaultPicoContainer();

}

private void Register(IEnumerable<Type> types) {

foreach (Type t in types) {

_container.RegisterComponent(new CachingComponentAdapter(new ConstructorInjectionComponentAdapter(t)));

}

}

[TestMethod]

public void should_detect_instantiation_failures() {

IEnumerable<Type> types = new Type[] {typeof (A), typeof (B), typeof (C), typeof (D), typeof(E)};

Register(types);

_doctor = new InjectionDoctor(types, _container.GetComponentInstanceOfType);

_doctor.Examine();

//_doctor.PrettyPrint();

Assert.AreEqual(1, _doctor.FailedInstatiations<A>());

Assert.AreEqual(2, _doctor.FailedInstatiations<B>());

Assert.AreEqual(3, _doctor.FailedInstatiations<C>());

Assert.AreEqual(0, _doctor.FailedInstatiations<D>());

Assert.AreEqual(1, _doctor.FailedInstatiations<E>());

}

}



The output of the PrettyPrint looks like:
Type -> Failed Instatiation Count
Tests.C -> 3
Tests.B -> 2
Tests.E -> 1
Tests.A -> 1

Additionally we could add a method bool InjectionDoctor.HaveAllInstantionsSucceded()
ps. The Blogger Code Formatter is not playing nice with the generics: the InjectionDoctor takes as parameter in constructor a function f(Type) -> object. In this way we make the InjectionDoctor container-agnostic.
pps. source-code on request.

No comments: