Nick,
Overview (you can always tell when a forum post is too long when it's
broken into sections)
Over the past few days we have been starting down the path of letting
Autofac take care of component disposal for us. Prior to Autofac we
had to explicitly handle disposal of components and resources. As we
have proceeded down this path we have found a few “gotchas”.
One thing we found very curious was that the container’s scope wasn’t
ambient whereas a typical “scope” class such as TransactionScope was
ambient. A normal usage of TransactionScope might look like the
following:
// Main
using (var scope = new TransactionScope())
{
// call to UserRepository
// some work in UserRepository
using (var repositoryScope = new TransactionScope())
{
// call to UserDAO
// some work inside of UserDAO
using (var daoScope = new TransactionScope())
{
// DB activity
}
}
}
The above scenario might be a bit extreme and somewhat unrealistic,
but the idea is that each component wants and needs to control its
little slice of the universe and that in this case each component on
its level is very concerned about ensuring the atomicity of the work
performed. In the above scenario TransactionScope was “aware” of the
parent scope and was able to coordinate activity with the parent in a
consistent and predictable fashion.
The Problem
In the Autofac forum and wiki as well as sprinkled across the internet
are a few overly simplistic examples dealing with nesting of scopes
via the container’s CreateInnerContainer method. Typically those
examples looked like the following:
using (var root = builder.Build())
{
// do some work
using (var inner = root.CreateInnerContainer())
{
// get a container-scoped component
var dbConn = inner.Resolve<IDbConnection>();
}
}
These examples work well in order to grasp the general concept of
container scoping but they do create some confusion. Specifically,
the following does not appear to be possible:
var builder = new ContainerBuilder();
builder.Register(c => new SqlConnection("connection string here"))
.As<IDbConnection>()
.ContainerScoped();
builder.Register(c => (Func<IDbConnection>)c.Resolve<IDbConnection>())
.As<Func<IDbConnection>>() // registers a callback to create
connection.
.FactoryScoped();
using (var root = builder.Build())
{
var connectionFactory = root.Resolve<Func<IDbConnection>>();
using (var inner = root.CreateInnerContainer())
{
// Despite executin in an inner container, the delegate
factory below
// will resolve to the container that was used to get a
reference
// to the factory. This is because of “closure” over
// registration parameter "c" which points to root container.
var connection = connectionFactory();
}
}
Granted, the above example is also overly simplistic also and may well
provoke a “Why did he do it that way?” For the sake of argument,
let’s just suppose that that’s the way it is.
The above example shows the potentially negative side effect of being
“inside” an inner container but having components resolve through the
outer container. The subtle consequence is that in order to create
components I must hold a direct reference to the container.
A Possible Solution
One possible solution would be to use thread local storage (or
HttpContext.Current.Items) to store a reference to the inner-most
container for the currently executing thread. If Autofac held this
reference internally/privately (on the thread store) it could easily
perform the resolvethrough the inner-most container without
difficulty. Using this technique the following scenario would easily
be possible:
The “connectionFactory” delegate is invoked (see above code) which
then causes the “c.Resolve<IDbConnection>()” to be invoked. During
the current container’s c.Resolve, probably during one of the very
first steps, the thread store is inspected for a reference to an inner
container which, if found, the current container container would then
delegate the request to the inner-most container found.
Afterthoughts
This whole situation comes about because in order to effectively use
an inner container you have to hold a reference to the container. As
we understand it the container shouldn’t be passed around (http://
code.google.com/p/autofac/wiki/BestPractices). This being the case,
it seems a bit of a hack to do all of the setup all possible Units of
Work in the program’s Main method or Global.asax.cs. Furthermore the
“closure” formed around the Func<> callback always references the
container that was used when the Func<> was resolved – regardless of
where the Func<> is invoked.
Jonathan