Compile-Time Lifetime Matching by Mark Seemann
When using hand-coded object composition, the compiler can help you match service lifetimes.
In my previous post, you learned how easy it is to accidentally misconfigure a DI Container to produce Captive Dependencies, which are dependencies that are being kept around after they should have been released. This can lead to subtle or catastrophic bugs.
This problem is associated with DI Containers, because Container registration APIs let you register services out of order, and with any particular lifestyle you'd like:
var builder = new ContainerBuilder(); builder.RegisterType<ProductService>().SingleInstance(); builder.RegisterType<CommerceContext>().InstancePerDependency(); builder.RegisterType<SqlProductRepository>().As<IProductRepository>() .InstancePerDependency(); var container = builder.Build();
In this Autofac example, CommerceContext is registered before SqlProductRepository, even though SqlProductRepository is a 'higher-level' service, but ProductService is registered first, and it's even 'higher-level' than SqlProductRepository. A DI Container doesn't care; it'll figure it out.
The compiler doesn't care if the various lifetime configurations make sense. As you learned in my previous article, this particular configuration combination doesn't make sense, but the compiler can't help you.
Compiler assistance #
The overall message in my Poka-yoke Design article series is that you can often design your types in such a way that they are less forgiving of programming mistakes; this enables the compiler to give you feedback faster than you could otherwise have gotten feedback.
If, instead of using a DI Container, you'd simply hand-code the required object composition (also called Poor Man's DI in my book, but now called Pure DI), the compiler will make it much harder for you to mismatch object lifetimes. Not impossible, but more difficult.
As an example, consider a web-based Composition Root. Here, the particular IHttpControllerActivator interface belongs to ASP.NET Web API, but it could be any Composition Root:
public class SomeCompositionRoot : IHttpControllerActivator { // Singleton-scoped services are declared here... private readonly SomeThreadSafeService singleton; public SomeCompositionRoot() { // ... and (Singleton-scoped services) are initialised here. this.singleton = new SomeThreadSafeService(); } public IHttpController Create( HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) { // Per-Request-scoped services are declared and initialized here var perRequestService = new SomeThreadUnsafeService(); if(controllerType == typeof(FooController)) { // Transient services are created and directly injected into // FooController here: return new FooController( new SomeServiceThatMustBeTransient(), new SomeServiceThatMustBeTransient()); } if(controllerType == typeof(BarController)) { // Transient service is created and directly injected into // BarController here, but Per-Request-scoped services or // Singleton-scoped services can be used too. return new BarController( this.singleton, perRequestService, perRequestService, new SomeServiceThatMustBeTransient()); } throw new ArgumentException("Unexpected type!", "controllerType"); } }
Notice the following:
- There's only going to be a single instance of the SomeCompositionRoot class around, so any object you assign to a
readonly
field is effectively going to be a Singleton. - The Create method is invoked for each request, so if you create objects at the beginning of the Create method, you can reuse them as much as you'd like, but only within that single request. This means that even if you have a service that isn't thread-safe, it's safe to create it at this time. In the example, the BarController depends on two arguments where the Per-Request Service fits, and the instance can be reused. This may seem contrived, but isn't at all if SomeThreadUnsafeService implements more that one (Role) interface.
- If you need to make a service truly Transient (i.e. it must not be reused at all), you can create it within the constructor of its client. You see an example of this when returning the FooController instance: this example is contrived, but it makes the point: for some unfathomable reason, FooController needs two instances of the same type, but the SomeServiceThatMustBeTransient class must never be shared. It's actually quite rare to have this requirement, but it's easy enough to meet it, if you encounter it.
Commerce example #
In the previous article, you saw how easy it is to misconfigure a ProductService, because you'd like it to be a Singleton. When you hand-code the composition, it becomes much easier to spot the mistake. You may start like this:
public class CommerceCompositionRoot : IHttpControllerActivator { private readonly ProductService productService; public CommerceCompositionRoot() { this.productService = new ProductService(); } public IHttpController Create( HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) { // Implementation follows here... } }
Fortunately, that doesn't even compile, because ProductService doesn't have a parameterless constructor. With a DI Container, you could define ProductService as a Singleton without a compilation error:
var container = new StandardKernel(); container.Bind<ProductService>().ToSelf().InSingletonScope();
If you attempt to do the same with hand-coded composition, it doesn't compile. This is an excellent example of Poka-Yoke Design: design your system in such a way that the compiler can give you as much feedback as possible.
Intellisense will tell you that ProductService has dependencies, so your next step may be this:
public CommerceCompositionRoot() { this.productService = new ProductService( new SqlProductRepository( new CommerceContext())); // Alarm bell! }
This will compile, but at this point, an alarm bell should go off. You know that you mustn't share CommerceContext across threads, but you're currently creating a single instance. Now it's much clearer that you're on your way to doing something wrong. In the end, you realise, simply by trial and error, that you can't make any part of the ProductService sub-graph a class field, because the leaf node (CommerceContext) isn't thread-safe.
Armed with that knowledge, the next step is to create the entire object graph in the Create method, because that's the only safe implementation left:
public IHttpController Create( HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) { if(controllerType == typeof(HomeController)) { return new HomeController( new ProductService( new SqlProductRepository( new CommerceContext()))); } // Handle other controller types here... throw new ArgumentException("Unexpected type!", "controllerType"); }
In this example, you create the object graph in a single statement, theoretically giving all services the Transient lifestyle. In practice, there's no difference between the Per Request and the Transient lifestyle as long as there's only a single instance of each service for each object graph.
Concluding remarks #
Some time ago, I wrote an article on when to use a DI Container. In that article, I attempted to explain how going from Pure DI (hand-coded composition) to a DI Container meant loss of compile-time safety, but I may have made an insufficient job of providing enough examples of this effect. The Captive Dependency configuration error, and this article together, describe one such effect: with Pure DI, lifetime matching is compiler-assisted, but if you refactor to use a DI Container, you lose the compiler's help.
Since I wrote the article on when to use a DI Container, I've only strengthened my preference for Pure DI. Unless I'm writing a very complex code base that could benefit from Convention over Configuration, I don't use a DI Container, but since I explicitly architect my systems to be non-complex these days, I haven't used a DI Container in production code for more than 1½ years.
Comments
I don't think it's a problem with the container, but a problem with the registrations. I use a Autofac as my DI Container registration, and I always have a root application lifetime scope, and a separate scope for each request. If the product service is registered in the root scope as single instance, it will throw a
DependencyResolutionException
In this case, I would have the ProductService registered in the root scope as single instance, and the other types in the request scope.
If
ProductService
is resolved, aDependencyResolutionException
is thrown, and the app is unusable - "fail fast" is followed. To fix the issue, the registration needs to be moved to to the request scope.Here's an example of a safe MVC Controller Factory using Autofac.
Sorry for the lack of code formatting - I'm not sure what you use to format code
Steve, thank you for writing. Indeed, you can make a DI Container detect the Captive Dependency error at run-time. I pointed that out in the defining article about the Captive Dependency problem, and as qujck points out in the comments, Simple Injector has this feature, too.
The point with the present article is that, instead of waiting until run-time, you get a chance to learn about potential lifetime mismatches already at design-time. In C#, F#, and other compiled languages, you can't perform a run-time test until you've compiled. While I'm all for fail fast, I don't think it'd be failing fast enough, if you can catch the problem at compile time, but then deliberately wait until run-time.
Another concern is to go for the simplest thing that could possibly work. Why use a complex piece of code like your AutofacControllerFactory above, instead of just writing the code directly? It's no secret that I'm not a big fan of the Lifetime Scope idiom, and your code provides an excellent example of how complicated it is. You may have omitted this for the sake of the example, but that code isn't thread-safe; in order to make it thread-safe, you'd need to make it even more complicated.
You probably know how to make it thread-safe, as do I, so this isn't an attempt at pointing fingers. The point I'm attempting to make is that, by using a DI Container, you
Thanks for the speedy response, Mark - I can't keep up. I think an issue I have with poor-man's DI is that I'm yet to see it in anything more than a trivial example. Indeed, the only time I have seen it used in a professional context is a 300 line file, 280 lines of which have the 'new' keyword in it, with plenty of repetition.
Do you know of any medium sized code bases around that use it to good effect? I'm thinking an application with at least 100 types, I'd like to see how the complexity of the graph is managed.
To answer your question, here's the advantages I see of using a container and lifetime scopes.
Clearer lifetimes: Your statement that the compiler is detecting the captive dependency isn't quite correct - it's still the developer doing that at design time. They have to see that
new CommerceContext()
is not a smart thing to do at application start, and move it accordingly. The compiler has nothing to do with that - either way, the check is happening at coding time. Whether that's while typingnew CommerceContext()
or when typingbuilder.Register<CommerceContext>()
, it's the same thing.I'd argue that the code that registers
CommerceContext
in an application scope is a much clearer alarm bell. After fixing the issue, you'll end up with the registration appearing in aRegisterRequestScopedStuff()
method, which is a much better way to tell future developers to be careful about this guy in the future.Simplicity: I would argue that the Autofac controller factory is simpler than the poor mans one. Using poor man style, you have a
switch
(or bunch ofif
statements) on the controller type, and need keep track of correct lifetimes in a deeply-nested object graph. I think a (thread safe) dictionary and disposal is significantly simpler that those things - at the very least, has fewer branches - and provides great access points to define expected lifetimes of objects. It probably seems more complicated because there's only a few types, mine has no syntax highlighting (very important for readability!) and I've documented which bits are app-wide and which are request-wide lifetimes, via method names and registration types.Speed of development: I find the overall development speed is faster using a container, as you don't have to micromanage the dependencies. While you do get slower feedback times on dependency failures, you have far fewer failures overall. It's been several months since I've seen a
DependencyResolutionException
. On the flip side, the javascript development I've done (which doesn't use a container) often has a missing a dependency or 2 - which would be equivalent to a compile error in a strongly typed language.What's more, I can write my classes and tests without having to worry about composition until it's time to run the application. To be fair, this is also achieved with good domain/application separation - since the app failing to compile does not prevent the tests from running - but I still like to write tests for my application project.
Disposables: As you mentioned, my simple example was not thread safe, due to having to store the lifetime scope for disposal when the controller is released. The only reason I need to store that is so Autofac can clean up any IDisposable dependencies I may have, and trivially at that - how do you do this with poor man's DI, while keeping it simple?
If I can wire up Autofac in my application in 10 minutes, have the computer do all the heavy lifting, while making it clearer to myself and future people what I want the lifetimes of things to be, why would I want to manage a dependency graph myself?
Before we continue this discussion, I think it's important to establish how you use a DI Container. If you refer to my article on the benefits of using a DI Container, which approach are you using?
I'd say I sit somewhere between convention and explicit register, but I guess I disagree about the "pointless" value for it, and place less importance on the value of strong/weak typing. As I said, I very rarely have the dependency exceptions be thrown anyway. In practice, I have a class of services that are wired up by convention (type name ends in "Factory" or "Controller", for example), and explicitly register others. No hard and fast rules about it.
That makes the discussion a little less clear-cut, because you are getting some of the benefits out of Convention over Configuration, but perhaps not as much as you could... Depending on how you put the balance between these two, I would agree with you that using a DI Container is beneficial.
My point isn't that there are no benefits from using a DI Container, but that there are also serious disadvantages. The benefits should outweigh the disadvantages, and that is, in my experience, far from given that they do. YMMV.
Do I know of any medium-sized code bases that use Pure DI to good effect? Perhaps... I don't know what a 'medium-sized' code base is to you. In any case, while I may know of such code bases, I know of none where the source code is public.
300-odd lines of code for composition sounds like a lot, but as I have previously demonstrated, using Explicit Register will only increase the line count.
Another criticism of manual composition is that every time you change something, you'll need to edit the composition code. That's true, but this is equally as true for Explicit Register. The difference is that with manual composition, you learn about this at compile-time, while with Explicit Register, you don't learn about changes until run-time. This, in isolation, is a clear win for manual composition.
Now, if you move to Convention over Configuration, this particular criticism of using a DI Container disappears again, but I never claimed anything else.