Search code examples
c#design-patternsdependency-injectioninversion-of-controlservice-locator

DI composition root: how does it ensure compile-time resolution checking


I've read a couple of articles my Mark Seeman on Dependency Injection, specifically the reasons for avoiding the Service Locator pattern:

Basic idea behind Service Locator being problematic is that it can fail at runtime:

public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

var orderProcessor = new OrderProcessor();

// following line fails at compile time if you 
// forget to register all necessary services - and
// you don't have a way of knowing which services it needs

orderProcessor.Process(someOrder);

But this means that the Composition Root must not only resolve all dependencies at startup, but actually instantiate the entire object graph, or otherwise we still wouldn't know it all the necessary dependencies have been registered:

private static void Main(string[] args)
{
    var container = new WindsorContainer();
    container.Kernel.Resolver.AddSubResolver(
        new CollectionResolver(container.Kernel));

    // Register
    container.Register(
        Component.For<IParser>()
            .ImplementedBy<WineInformationParser>(),
        Component.For<IParser>()
            .ImplementedBy<HelpParser>(),
        Component.For<IParseService>()
            .ImplementedBy<CoalescingParserSelector>(),
        Component.For<IWineRepository>()
            .ImplementedBy<SqlWineRepository>(),
        Component.For<IMessageWriter>()
            .ImplementedBy<ConsoleMessageWriter>());

    // Everything must be resolved AND instantiated here
    var ps = container.Resolve<IParseService>();
    ps.Parse(args).CreateCommand().Execute();

    // Release
    container.Release(ps);
    container.Dispose();
}

How feasible is this in a real world application? Does this really mean you are not supposed to instantiate anything anywhere outside the constructor?

(Additional info)

Say you have a service which is supposed to handle incoming connections from multiple measurement devices of some kind (different connection types, protocols, or different versions of the same protocol). Whenever you get a new connection, service is supposed to construct a "pipeline" from the input port, through fifo buffers, to many parsers specific to that device type, ending with multiple consumers for various parsed messages.

Composing these object graphs in advance is something that doesn't seem possible on application startup. Even if it can be delayed, I still don't see how it's possible to get an early(-er) indication that object graph construction will fail.

This seems to be the main problem with service locators, and I don't see how to avoid it:

In short, the problem with Service Locator is that it hides a class' dependencies, causing run-time errors instead of compile-time errors, as well as making the code more difficult to maintain because it becomes unclear when you would be introducing a breaking change.


Solution

  • But this means that the Composition Root must not only resolve all dependencies at startup, but actually instantiate the entire object graph

    If you apply Pure DI (i.e. applying the Dependency Injection pattern, but without a DI container) you'll get compile-time support out of the box. With a DI container you will have to do these checks at runtime, but that doesn't mean that you have to do this during start-up, although I would say it is preferred. So if checking the container's configuration at startup doesn't cause performance issues, you should do this. Otherwise you can move this verification step to a unit test.

    How feasible is this in a real world application?

    This is completely feasible. I build big applications with often hundreds to over a thousand services registered in the container and I always verify (and diagnose) my container's configuration, which prevents many common configuration mistakes that are very easy to make and very hard to track down.

    Does this really mean you are not supposed to instantiate anything anywhere outside the constructor?

    It is your composition root that is responsible for creating your services; that doesn't mean per se that all services should be created during startup, because you can delay the creation of parts of your object graph until runtime. Still however, my preferred way of working is to make all registered services singleton (one instance for the during of the application). This makes it really easy (and cheap) to create all services during application startup, and forces you into a more strict model where SOLID violations and other DI bad practices popup much sooner.