DI Containers aren't type-safe. What are the alternatives?

In April 2020 I published an article called Unit bias against collections. My goal with the article was to point out a common cognitive bias. I just happened to use .NET's built-in DI Container as an example because I'd recently encountered a piece of documentation that exhibited the kind of bias I wanted to write about.

This lead to a discussion about the mental model of the DI Container:

"Yeah, I wasn't explicit and clarified in a second tweet. I didn't mean in the services example, but in general where it helps if the reader's mental model of the code has 1 item from the collection, because that's how it is in real life. SingleOrDefault would enforce this."

The point made by Bogdan Galiceanu highlights the incongruity between the container's API and the mental model required to work with it.

IServiceCollection recap #

The API in case belongs to IServiceCollection, which is little more than a collection of ServiceDescriptor objects. Each ServiceDescriptor describes a service, as the name implies.

Given an IServiceCollection you can, for example, register an IReservationsRepository instance:

var connStr = Configuration.GetConnectionString("Restaurant");
services.AddSingleton<IReservationsRepository>(sp =>
{
    var logger =
        sp.GetService<ILogger<LoggingReservationsRepository>>();
    var postOffice = sp.GetService<IPostOffice>();
    return new EmailingReservationsRepository(
        postOffice,
        new LoggingReservationsRepository(
            logger,
            new SqlReservationsRepository(connStr)));
});

This adds a ServiceDescriptor entry to the collection. (Code examples are from Code That Fits in Your Head.)

Later, you can remove and replace the service for test purposes:

internal sealed class SelfHostedApi : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.RemoveAll<IReservationsRepository>();
            services.AddSingleton<IReservationsRepository>(
                new FakeDatabase());
        });
    }
}

Here I use RemoveAll, even though I 'know' there's only one service of that type. Bogdan Galiceanu's argument, if I understand it correctly, is that it'd be more honest to use SingleOrDefault, since we 'know' that there's only one such service.

I don't bring this up to bash on either Bogdan Galiceanu or the IServiceCollection API, but this exchange of ideas provided another example that DI Containers aren't as helpful as you'd think. While they do provide some services, they require significant mental overhead. You have to 'know' that this service has only one instance, while another service may have two implementations, and so on. As the size of both code base and team grows, keeping all such knowledge in your head becomes increasingly difficult.

The promise of object-orientation was always that you shouldn't have to remember implementation details.

Particularly with statically typed programming languages you should be able to surface such knowledge as static type information. What would a more honest, statically typed DI Container look like?

Statically typed containers #

Over a series of articles I'll explore how a statically typed DI Container might look:

  • Type-level DI Container prototype
  • A type-safe DI Container C# example
  • Nested type-safe DI Containers
  • A type-safe DI Container as a functor
  • A type-safe DI Container as a tuple
  • Type-safe DI Containers are redundant

The first of these articles show a Haskell prototype, while the rest of the articles use C#. If you don't care about Haskell, you can skip the first article.

As the title of the last article implies, this exploration only concludes that type-safe DI Containers are isomorphic to Pure DI. I consider Pure DI the simplest of these approaches, suggesting that there's no point in type-safe DI Containers of the kinds shown here.

Conclusion #

Some people like DI Containers. I don't, because they take away type-safety without providing much benefit to warrant the trade-off. A commonly suggested benefit of DI Containers is lifetime management, but you can trivially implement type-safe lifetime management with Pure DI. I don't find that argument compelling.

This article series examines if it's possible to create a 'better' DI Container by making it more type-safe, but I ultimately conclude that there's no point in doing so.

Next: Type-level DI Container prototype.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 10 January 2022 06:41:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 10 January 2022 06:41:00 UTC