The Relationship Zoo
Dependency Injection, unsurprisingly, is all about the relationships between components.
Components rarely exist or perform any useful task alone. A system that achieves a business function may use tens, hundreds or even thousands of different components, all working in collaboration to get a job done.
Early IoC containers understood one kind of relationship: “A needs a B.”
interface B { }
class A
{
public A(B b) { ... }
}
More complex relationships, like “A creates instances of B” required the author of component A to deal directly with the container to get things done, by calling container.Resolve<B>()
, for example.
Since a majority of relationships are, in fact, simple direct dependencies, this was an acceptable point from which to start developing IoC containers as a technology. Recently, more powerful, declarative ways of specifying “higher-order” dependencies have emerged. In this article we’ll look at a selection of them, using Autofac 2 as an example implementation.
I was first introduced to the term “higher-order dependencies” by Mircea Trofin at Microsoft. I’m not sure I’m using it here in precisely the same sense. The .NET Composition Primitives, which Mircea was instrumental in creating, have some elegant mechanisms for handling them - in a future article I hope to cover this in more detail.
A Catalog of Relationships
It turns out that there are a lot of different ways that one component may relate to another.
Here’s a partial list to fill in the question mark above:
- A needs a B
- A needs a B at some point in the future
- A needs a B until some point in the future
- A needs to create instances of B
- and provides parameters of types X and Y
- A needs all the kinds of B
- A needs to know X about B before using it
- …
This list is chosen carefully, because each of these relationships can be expressed declaratively through simple adapter types.
Relationship | Adapter Type | Meaning |
---|---|---|
A needs a B | None | Dependency |
A needs a B at some point in the future | Lazy<B> |
Delayed instantiation |
A needs a B until some point in the future | Owned<B> |
Controlled lifetime |
A needs to create instances of B | Func<B> |
Dynamic instantiation |
A provides parameters of types X and Y to B | Func<X,Y,B> |
Parameterisation |
A needs all the kinds of B | IEnumerable<B> |
Enumeration |
A needs to know X about B before using it | Meta<B,X> |
Metadata interrogation |
Some of the types above will be familiar: Lazy<T>
, Func<T>
and IEnumerable<T>
are all .NET BCL types, and are used here with their standard semantics.
Owned<T>
and Meta<T>
are introduced by Autofac, and fill some of the gaps in the BCL. They’re very simple types that could be replicated in an application that wanted to use them independently.
Lazy
A lazy dependency is not instantiated until its first use. This appears where the dependency is infrequently used, or expensive to construct.
class A
{
Lazy<B> _b;
public A(Lazy<B> b) { _b = b }
public void M()
{
// The component implementing B is created the
// first time M() is called
_b.Value.DoSomething();
}
}
Owned
An owned dependency can be released by the owner when it is no longer required. Owned dependencies usually correspond to some unit of work performed by the dependent component.
class A
{
Owned<B> _b;
public A(Owned<B> b) { _b = b }
public void M()
{
// _b is used for some task
_b.Value.DoSomething();
// Here _b is no longer needed, so
// it is released
_b.Dispose();
}
}
In an IoC container, there’s often a subtle difference between releasing and disposing a component: releasing an owned component goes further than disposing the component itself. Any of the dependencies of the component will also be disposed. Releasing a shared component is usually a no-op, as other components will continue to use its services.
Func and Func<X,Y,T>
The ‘factory’ adapters imply creation of individual instances of the dependency. The parameterised versions allow initialisation data to be passed to the implementing component.
class A
{
Func<int, string, B> _b;
public A(Func<int, string, B> b) { _b = b }
public void M()
{
var b = _b(42, "http://hel.owr.ld");
b.DoSomething();
}
}
IEnumerable
Dependencies of an enumerable type provide multiple implementations of the same service (interface).
class A
{
IEnumerable<B> _bs;
public A(IEnumerable<B> bs) { _bs = bs }
public void M()
{
foreach (var b in bs)
b.DoSomething();
}
}
Meta<T,M>
Metadata is data-about-data. Requesting metadata with a dependency allows the dependent component to get infomation about the provider of a service without invoking it.
class A
{
Meta<B,BMetadata> _b;
public A(Meta<B,BMetadata> b) { _b = b }
public void M()
{
if (_b.Metadata.SomeValue == "yes")
_b.Value.DoSomething();
}
}
The Relationship Zoo
If you’ve read this far, you’re probably desperate for an example that doesn’t use the components A and B. I feel your pain! The truth is, while these dependencies are useful in their own right, really interesting dependencies often combine multiple adapters!
Choosing a File Viewer
Metadata is excellent when there are multiple implementations of a service, and one must be selected to perform a task. The selection criterion mightn’t be inherent in the component itself, so properties make awkward choices for representing this data.
Let’s say we have a number of file viewers, and, via configuration, we associate each viewer with the file types that it should be used to view.
interface IViewer
{
void View(string filename);
}
interface IViewerMetadata
{
IEnumerable<string> FileTypes { get; }
}
class Browser
{
IEnumerable<Meta<IViewer, IViewerMetadata>> _viewers;
public Browser(IEnumerable<Meta<IViewer, IViewerMetadata>> viewers)
{
_viewers = viewers;
}
public void OnViewFile(string filename)
{
var ext = Path.GetExtension(filename);
var viewer = _viewers.Single(v => v.Metadata.FileTypes.Contains(ext));
viewer.Value.View(filename);
}
}
In this example, IEnumerable<T>
and Meta<T>
are combined to express the two features of the relationship between Browser
and IViewer
.
Mixing in dynamic creation, controlled lifetime or lazy initialization would often also make sense in this kind of scenario.
You can see how metadata is associated with an Autofac component here, or examine one of the many examples based on the MEF APIs here.
Creating and Releasing a Message Handler
Another common combination is that of dynamic instantiation and ownership of the instances. A message pump may create, use, then release a handler for each incoming message:
interface IMessageHandler
{
void Handle(Message message);
}
class MessagePump
{
Func<Owned<IMessageHandler>> _handlerFactory;
public MessagePump(Func<Owned<IMessageHandler>> handlerFactory)
{
_handlerFactory = handlerFactory;
}
public void Go()
{
while(true)
{
var message = NextMessage();
using (var handler = _handlerFactory())
{
handler.Value.Handle(message);
}
}
}
}
Many instances, with dynamic creation and controlled lifetime and parameters and and and whoah!
Yes, IEnumerable<Meta<Func<X,Y,Owned<T>>,M>>
is in fact a valid relationship type. Glad you asked?
Common combinations can be eaiser to work with when they are baked into a single class. In MEF, for example, Meta<Lazy<T>>
is represented as Lazy<T,M>
. Likewise Func<Owned<T>>
is represented as PartCreator<T>
. You can plug these composite adapter types into Autofac via a custom registration source if you need them.
Custom delegate types can also ease angle-bracket-eyestrain if you use the parameterised factories a lot.
The core of Autofac 2 doesn’t have any special knowledge of any of these adapter types. They’re all plugged in as IRegistrationSource implementations, meaning that you are free to change or extend the set of adapter types as your needs dictate.
Muahahahahhahahaahhh!!!!
I heard that maniac laugh just then! Let me just slip in the disclaimer now: just because you can model almost any conceivable relationship this way, doesn’t mean that you should. Where a simple direct dependency will suffice, don’t gouge out your eyes on six layers of nested angle brackets, or invest in new adapter types. Like all sharp tools, use relationship types with discretion.
At the same time, there’s practically no excuse to use IContainer
or IComponentContext
in your components anymore.
Conclusions and a Suggestion
You might not be using Autofac today, and might not have any desire to. That’s okay! Anything new and useful you see here will no doubt make its way back into the container you prefer - this is the magic of being part of an Open Source community.
If you do use Autofac, or are curious, download the .NET 4.0 version from the project site.
Update: see also the IIndex<K,V>
relationship type.
A note to container developers…
This section remains for historical curiosity.
Although this is an Autofac-centric article, it draws on the combined experience of many passionate people who are driving .NET composition technology forward.
I’m not sure yet that we share a common vision of the “v.Next” IoC container - one thing we do seem to agree on is that it is harmful to have the container abstraction leak through into components. Perhaps in the future, IoC containers will be completely transparent, but we have some way yet to go.
Common Service Locator (CSL) was a step forward in interoperability. A good range of applications and frameworks that need dynamic or lazy instantiation no longer have to depend on a specific IoC container; I think this is one way we’ve maintained diversity and a healthy ecosystem.
CSL is a stopgap though - a service locator that could handle all of the scenarios above would be a monster of an interface indeed! CSL also breaks the declarative nature of components that use it.
I’d be very interested to hear thoughts on a new project, Common Context Adapters, which would include types like Owned<T>
and Meta<T,M>
to fill gaps in the BCL while it evolves. It would also provide baseline documentation on how wiring of Func<T>
etc. should work.
Common Context Adapters would play a similar role to CSL by isolating applications and frameworks from the specifics of an IoC container, but in a way that is compatible with declarative dependency wiring and inversion of control.
I’m prepared to put effort into making this happen if there is enough interest.