How to address the arity problem with type-safe DI Container prototypes.

This article is part of a series called Type-safe DI composition. In the previous article, you saw a C# example of a type-safe DI Container. In case it's not clear from the article that introduces the series, there's really no point to any of this. My motivation for writing the article is that readers sometimes ask me about topics such as DI Containers versus type safety, or DI Containers in functional programming. The goal of these articles is to make it painfully clear why I find such ideas moot.

N+1 arity #

The previous article suggested a series of generic containers in order to support type-safe Dependency Injection (DI) composition. For example, to support five services, you need five generic containers:

public sealed class Container
public sealed class Container<T1>
public sealed class Container<T1T2>
public sealed class Container<T1T2T3>
public sealed class Container<T1T2T3T4>
public sealed class Container<T1T2T3T4T5>

As the above listing suggests, there's also an (effectively redundant) empty, non-generic Container class. Thus, in order to support five services, you need 5 + 1 = 6 Container classes. In order to support ten services, you'll need eleven classes, and so on.

While these classes are all boilerplate and completely generic, you may still consider this a design flaw. If so, there's a workaround.

Nested containers #

The key to avoid the n + 1 arity problem is to nest the containers. First, we can delete Container<T1T2T3>, Container<T1T2T3T4>, and Container<T1T2T3T4T5>, while leaving Container and Container<T1> alone.

Container<T1T2> needs a few changes to its Register methods.

public Container<T1, Container<T2, T3>> Register<T3>(T3 item3)
{
    return new Container<T1, Container<T2, T3>>(
        Item1,
        new Container<T2, T3>(
            Item2,
            item3));
}

Instead of returning a Container<T1, T2, T3>, this version of Register returns a Container<T1, Container<T2, T3>>. Notice how Item2 is a new container. A Container<T2, T3> is nested inside an outer container whose Item1 remains of the type T1, but whose Item2 is of the type Container<T2, T3>.

The other Register overload follows suit:

public Container<T1, Container<T2, T3>> Register<T3>(Func<T1, T2, T3> create)
{
    if (create is null)
        throw new ArgumentNullException(nameof(create));
 
    var item3 = create(Item1, Item2);
    return Register(item3);
}

The only change to this method, compared to the previous article, is to the return type.

Usage #

Since the input parameter types didn't change, composition still looks much the same:

var container = Container.Empty
    .Register(Configuration)
    .Register(CompositionRoot.CreateRestaurantDatabase)
    .Register(CompositionRoot.CreatePostOffice)
    .Register(CompositionRoot.CreateClock())
    .Register((confcont) =>
        CompositionRoot.CreateRepository(conf, cont.Item1.Item2));
var compositionRoot = new CompositionRoot(container);
services.AddSingleton<IControllerActivator>(compositionRoot);

Only the last Register call is different. Instead of a lambda expression taking four arguments ((c, _, po, __)), this one only takes two: (conf, cont). conf is an IConfiguration object, while cont is a nested container of the type Container<Container<IRestaurantDatabase, IPostOffice>, IClock>.

Recall that the CreateRepository method has this signature:

public static IReservationsRepository CreateRepository(
    IConfiguration configuration,
    IPostOffice postOffice)

In order to produce the required IPostOffice object, the lambda expression must first read Item1, which has the type Container<IRestaurantDatabase, IPostOffice>. It can then read that container's Item2 to get the IPostOffice.

Not particularly readable, but type-safe.

The entire container object passed into CompositionRoot has the type Container<IConfiguration, Container<Container<Container<IRestaurantDatabase, IPostOffice>, IClock>, IReservationsRepository>>.

Equivalently, the CompositionRoot's Create method has to train-wreck its way to each service:

public object Create(ControllerContext context)
{
    if (context is null)
        throw new ArgumentNullException(nameof(context));
 
    var t = context.ActionDescriptor.ControllerTypeInfo;
 
    if (t == typeof(CalendarController))
        return new CalendarController(
            container.Item2.Item1.Item1.Item1,
            container.Item2.Item2);
    else if (t == typeof(HomeController))
        return new HomeController(container.Item2.Item1.Item1.Item1);
    else if (t == typeof(ReservationsController))
        return new ReservationsController(
            container.Item2.Item1.Item2,
            container.Item2.Item1.Item1.Item1,
            container.Item2.Item2);
    else if (t == typeof(RestaurantsController))
        return new RestaurantsController(container.Item2.Item1.Item1.Item1);
    else if (t == typeof(ScheduleController))
        return new ScheduleController(
            container.Item2.Item1.Item1.Item1,
            container.Item2.Item2,
            AccessControlList.FromUser(context.HttpContext.User));
    else
        throw new ArgumentException(
            $"Unexpected controller type: {t}.",
            nameof(context));
}

Notice how most of the services depend on container.Item2.Item1.Item1.Item1. If you hover over that code in an IDE, you'll see that this is an IRestaurantDatabase service. Again, type-safe, but hardly readable.

Conclusion #

You can address the n + 1 arity problem by nesting generic containers inside each other. How did I think of this solution? And can we simplify things even more?

Read on.

Next: A type-safe DI Container as a functor.



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, 07 February 2022 07:01:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 07 February 2022 07:01:00 UTC