Declarative Context Adapters in Autofac 2
The previous post on this blog showed how Autofac 2 can provide Lazy<T>
dependencies whenever T
is registered.
Other types that Autofac provides via the same mechanism are:
-
Func<T>
,Func<T,U>
&c., to create components on-the-fly; -
Owned<T>
to manually control the lifetime of a component; -
IEnumerable<T>
to get all implementers of a service; and -
Instances of a particular generic type, e.g.
IRepository<Customer>
, when the open generic type, e.g.Repository<T>
, is registered.
Each of these can be thought of as a _declarative __context adapter. _Each of these types supports a scenario involving some kind of interaction between a component and the container that hosts it. Instead of having the component call the container directly, the component expresses requirements declaratively using these little adapter types, and the container provides the implementation.
Don’t worry if it is a stretch to imagine how each of these works. For today, we’ll examine the implementation of Lazy<T>
, and that should give you enough background to figure out the others if you want to.
IRegistrationSource: a better Service Locator?
Context adapters like these are provided to Autofac via IRegistrationSource
s.
public interface IRegistrationSource
{
IEnumerable<IComponentRegistration> RegistrationsFor(
Service service,
Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor);
}
A registration source is much like a Service Locator: when the container needs to provide service
, it queries the registration source to get any available implementations.
The registration source can use the passed-in registrationAccessor
to get information about other components registered with the container.
At first glance this seems to be a Service Locator plain and simple, however there is an important distinction. The IComponentRegistration
’s returned from RegistrationsFor()
tell the container how to make components supporting the service. They are not instances of the services themselves.
This means that among other benefits, a registration source is called only once for any particular service (e.g. Lazy
) no matter how many times the service is resolved. The performance overhead, compared with using a Service Locator this way, is very low.
LazyRegistrationSource
If you’re using Autofac on .NET 4.0, you can use LazyRegistrationSource
(by registering LazyDependencyModule
) to get support for the context adapter System.Lazy<T>
.
LazyRegistrationSource
is a very simple implementation of IRegistrationSource
.
class LazyRegistrationSource : IRegistrationSource
{
public IEnumerable<IComponentRegistration> RegistrationsFor(
Service service,
Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
{
var swt = service as IServiceWithType;
if (swt == null ||
!swt.ServiceType.IsGenericType ||
swt.ServiceType.GetGenericTypeDefinition() != typeof(Lazy<>))
return Enumerable.Empty<IComponentRegistration>();
var valueType = swt.ServiceType.GetGenericArguments()[0];
var valueService = swt.ChangeType(valueType);
var registrationCreator = CreateLazyRegistrationMethod.MakeGenericMethod(valueType);
return registrationAccessor(valueService)
.Select(v => registrationCreator.Invoke(null, new object[] { service, v }))
.Cast<IComponentRegistration>();
}
static readonly MethodInfo CreateLazyRegistrationMethod = typeof(LazyRegistrationSource)
.GetMethod("CreateLazyRegistration", BindingFlags.Static | BindingFlags.NonPublic);
static IComponentRegistration CreateLazyRegistration<T>(
Service providedService,
IComponentRegistration valueRegistration)
{
var rb = RegistrationBuilder.ForDelegate(
(c, p) => {
var context = c.Resolve<IComponentContext;>();
return new Lazy<T>(() => (T)c.Resolve(valueRegistration, p));
})
.As(providedService);
return RegistrationBuilder.CreateRegistration(rb);
}
}
Let’s break this down.
RegistrationsFor()
The whole purpose of RegistrationsFor()
is to map a service to components that can provide it:
public IEnumerable<IComponentRegistration> RegistrationsFor(
Service service,
Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
{
In LazyRegistrationSource
we’re looking for services that are based on the Lazy<T>
type.
So, the first part of RegistrationsFor
checks to see if the service fits this description, and if not, the method returns an empty list of components, since it can’t provide anything.
var swt = service as IServiceWithType;
if (swt == null ||
!swt.ServiceType.IsGenericType ||
swt.ServiceType.GetGenericTypeDefinition() != typeof(Lazy<>))
return Enumerable.Empty<IComponentRegistration>();
If we get past this test, then we know we’re dealing with Lazy<T>
. Since we provide Lazy<T>
on top of T
, we extract T
and construct a Service
with it so that we can find implementations.
var valueType = swt.ServiceType.GetGenericArguments()[0];
var valueService = swt.ChangeType(valueType);
Lazy<T>
is a generic type, but we’re deep in the land of reflection here. Rather than clog our code up with tonnes of reflective goop, we use a generic method parameterised on T
to switch over into strongly-typed code to create a registration for Lazy<T>
.
var registrationCreator = CreateLazyRegistrationMethod.MakeGenericMethod(valueType);
We’ll come back to the implementation of CreateLazyRegistration
in just a second.
Once we have a method that can create a IComponentRegistration
for Lazy<T>
based on a component registration for T
, we use registrationAccessor
to find all the T
registrations, and use our function to map them to a result.
return registrationAccessor(valueService)
.Select(v => registrationCreator.Invoke(null, new object[] { service, v }))
.Cast<IComponentRegistration>();
Now to the implemenation behind registrationCreator
.
CreateLazyRegistration
This method takes an IComponentRegistration
for T
and wraps it in an IComponentRegistration
for Lazy<T>
. The providedService
parameter is the service that our registration is going to provie to the container (some kind of service based on Lazy<T>
.)
static IComponentRegistration CreateLazyRegistration<T>(
Service providedService,
IComponentRegistration valueRegistration)
{
Autofac 2 provides some helpers for creating registrations. The syntax below is the implementaiton equivalent of calling Register((c, p) => ...).As(providedService)
on a ContainerBuilder
.
var rb = RegistrationBuilder.ForDelegate(
(c, p) => {
var context = c.Resolve<IComponentContext>();
return new Lazy<T>(() => (T)c.Resolve(valueRegistration, p));
})
.As(providedService);
return RegistrationBuilder.CreateRegistration(rb);
Some noteworthy points stick out:
-
The
context
is re-resolved fromc
, since we need the underlying context and not the temporary one used for the current operation; -
We’re using a new
Resolve()
overload that takes anIComponentRegistration
; and -
We forward the parameters
p
on to the value resolve call, so that parameters passed when resolvingLazy<T>
will be available toT
.
Wrapping Up
This isn’t the kind of code you’ll write day-to-day when using Autofac 2. What’s exciting though, is how easy it can be to write sophisticated IoC-driven applications without having infrastructure concerns scattered through your code.
Next time you hear the question “but how do I do X without calling the container directly?” remember that there’s probably a declarative context adapter for that, based on IRegistrationSource
.