Skip to content

Designing a Delegate Injection Container

In my last post I proposed using delegates instead of interfaces to declare dependencies for injection. While delegates are limited to a single function call, this is often sufficient for service dependencies. In addition, this is not a wholesale replacement for traditional IoC, since at the end of the day, you have to have some class instances that provide the methods bound by the delegates and want to return instances require those delegates, so our container will still need to resolve class instances.

The main benefit of using delegates instead of interfaces is that delegates do not impose any requirements on the providing class, and thereby dependencies can be defined by what the client needs rather than what a service provides.

Mapping Delegate Dependencies

To illustrate this mapping, let's bring back the classes defined in the last post:

public class MessageQueue
{
 public void Enqueue(string recipient, string message) { ... }
 public string TryDequeue(string recipient) { ... }
}

public class Producer : IProducer
{
 public delegate void EnqueueDelegate(string recipient, string message);
 public Producer(EnqueueDelegate dispatcher) { ... }
}

public class Consumer : IConsumer
{
 public delegate string TryDequeueDelegate(string recipient);
 public Consumer(TryDequeueDelegate inbox) { ... }
}

What we need is a way to map a delegate to a method:

EnqueueDelegate => MessageQueue.Enqueue
TryDequeueDelegate => MessageQueue.TryDequeue

Aside from the fact that the above is not a legal C# syntax, the above has the implicit assumption that we can resolve a canonical instance of MessageQueue, since MessageQueue.Enqueue really refers to a method on an instance of MessageQueue. I.e. our container must function like a regular IoC container so we can resolve that instance.

The above solves the scenario of a mapping one delegate to one implementation. In addition, we'd probably want the flexibility to map a particular implementation to a particular client class, such that:

Producer =>
  EnqueueDelegate => MessageQueue.Enqueue
Consumer =>
  TryDequeueDelegate => MessageQueue.TryDequeue

Using Expressions to capture delegate mappings

The usage scenarios described are simple enough to understand. If our container were initialized using strings or some external configuration file (Xml, custom DSL, etc.), the actual reflection required for the injection of mappings isn't too complex either. However, I abhor defining typed mappings without type-safety. This is isn't so much about making mistakes in spelling, etc. that the compiler can't catch. It is mostly about being able to navigate the mappings and the dependencies they describe and the ability to refactor while keeping the mappings in sync.

It would be great if we could just say:

Note: I'm purposely using a syntax that mimics Autofac, since the implementation later on will be done as a hack on top of Autofac

builder.Define<Producer.EnqueueDelegate>.As<MessageQueue.Enqueue>();

That looks fine and could even work in the face of polymorphism, since knowing the signature of Producer.EnqueueDelegate we could reflect the proper overload of MessageQueue.Enqueue. However, C# has no syntax for getting at the MethodInfo of a method via Generics (it is not a type). There isn't even an equivalent to typeof(T) for members, the reason for which was well explained by Eric Lippert in In Foof We Trust: A Dialogue. The only way to get MethodInfo relies on string based reflection.

Fortunately, C#3.0 introduced a syntax that allows us to capture method calls as expression trees that we can decompose programatically. This lets us to express our method call like this:

MessageQueue messageQueue = new MessageQueue();
Expression<Producer.EnqueueDelegate> expr = (a,b) => messageQueue.Enqueue(a,b);

This expression conveniently infers the type of a and b. As a sidenote, Producer.EnqueueDelegate does not mean that EnqueueDelegate is a member of Producer. It's just a syntax artifact of nested declarations in C#, which in this case conveniently makes the delegate look attached to the class.

Unfortunately, there we can't just include MessageQueue in the parameter list of the lambda. If we were to include it in the argument list, it could not be inferred, and if we were to define MessageQueue explicitly as a lambda parameter, then we'd be forced to declare all arguments. We want to express the above and only explicitly define MessageQueue. To accomplish this we need to create a composite expression that previously was told about MessageQueue:

Expression<Func<MessageQueue, Expression<Producer.EnqueueDelegate>>> expr
  = x => (a,b)=> messageQueue.Enqueue(a,b);

Now we have enough syntactic sugar to describe our two registration scenarios in terms of the container builder. First, the global registration of the delegate against an implementation:

builder.Define<Consumer.TryDequeueDelegate>()
  .As<MessageQueue>(x => a => x.TryDequeue(a));
builder.Define<Producer.EnqueueDelegate>()
  .As<MessageQueue>(x => (a, b) => x.Enqueue(a, b));

And alternately, the registration of delegates and their implementation in the context of a particular class:

builder.Register<Consumer>().As<IConsumer>()
  .With<Consumer.TryDequeueDelegate>()
  .As<MessageQueue>(x => a => x.TryDequeue(a));

builder.Register<Producer>().As<IProducer>()
  .With<Producer.EnqueueDelegate>()
  .As<MessageQueue>(x => (a, b) => x.Enqueue(a, b));

Next time, I'll go over the implementation of the above to get it working as an injection framework.