A type-safe DI Container as a functor by Mark Seemann
Decoupling the registry of services from the container. An ongoing C# example.
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, nested 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.
In the previous article, you saw how you can use container nesting to implement a type-safe container of arbitrary arity. You only need these generic containers:
public sealed class Container public sealed class Container<T1> public sealed class Container<T1, T2>
You can achieve arities higher than two by nesting containers. For example, you can register three services by nesting one container inside another: Container<T1, Container<T2, T3>>
. Higher arities require more nesting. See the previous article for an example with five services.
Decoupling the container from the registry #
How did I get the idea of nesting the containers?
It started as I was examining the constructor of the CompositionRoot
class:
private readonly Container<IConfiguration, Container<Container<Container<IRestaurantDatabase, IPostOffice>, IClock>, IReservationsRepository>> container; public CompositionRoot(Container<IConfiguration, Container<Container<Container<IRestaurantDatabase, IPostOffice>, IClock>, IReservationsRepository>> container) { this.container = container; }
Those are, indeed, wide lines of code. No 80x24 box here. If you ignore all the container nesting, you can tell that the container contains five services:
IConfiguration
IRestaurantDatabase
IPostOffice
IClock
IReservationsRepository
CompositionRoot
:
IRestaurantDatabase
IClock
IReservationsRepository
IConfiguration
and IPostOffice
services are only present because they were required to create some of the other services. CompositionRoot
never directly uses these two services. In other words, there's a leaky abstraction somewhere.
It'd be more in spirit with the Dependency Inversion Principle (DIP) and the Interface Segregation Principle (ISP) if the constructor required only the services it needs.
On the other hand, it's easiest to create the container from its constituent elements if you can pass already-registered services along when registering other services:
var container = Container.Empty .Register(Configuration) .Register(CompositionRoot.CreateRestaurantDatabase) .Register(CompositionRoot.CreatePostOffice) .Register(CompositionRoot.CreateClock()) .Register((conf, cont) => CompositionRoot.CreateRepository(conf, cont.Item1.Item2)); var compositionRoot = new CompositionRoot(container); services.AddSingleton<IControllerActivator>(compositionRoot);
I realised that the container needed a way to remove services from the public API. And it hit me that it'd be easier if I separated the container from its registry of services. If I did that, I could define a single generic Container<T>
and even make it a functor. The payload could be a custom Parameter Object, but could also just be a tuple of services.
From Thinking with Types I knew that you can reduce all algebraic data types to canonical representations of Eithers and tuples.
A triple, for example, can be modelled as a Tuple<T1, Tuple<T2, T3>>
or Tuple<Tuple<T1, T2>, T3>
, a quadruple as e.g. Tuple<Tuple<T1, T2>, Tuple<T3, T4>>
, etcetera.
Connecting those dots, I also realised that as an intermediary step I could nest the containers. It's really unnecessary, though, so let's proceed to simplify the code.
A Container functor #
Instead of a Container<T1, T2>
in addition to a Container<T1>
, you only need the single-arity container:
public sealed class Container<T> { public Container(T item) { Item = item; } public T Item { get; } // More members to follow...
Apart from overriding Equals
and GetHashCode
, the only member is Select
:
public Container<TResult> Select<TResult>(Func<T, TResult> selector) { if (selector is null) throw new ArgumentNullException(nameof(selector)); return new Container<TResult>(selector(Item)); }
This is a straightforward Select
implementation, making Container<T>
a functor.
Usage #
You can use the Select
method to incrementally build the desired container:
var container = Container.Empty.Register(Configuration) .Select(conf => (conf, rdb: CompositionRoot.CreateRestaurantDatabase(conf))) .Select(t => (t.conf, t.rdb, po: CompositionRoot.CreatePostOffice(t.conf, t.rdb))) .Select(t => (t.conf, t.rdb, t.po, clock: CompositionRoot.CreateClock())) .Select(t => (t.conf, t.rdb, t.po, t.clock, repo: CompositionRoot.CreateRepository(t.conf, t.po))); var compositionRoot = new CompositionRoot(container); services.AddSingleton<IControllerActivator>(compositionRoot);
The syntax is awkward, since you need to pass a tuple along to the next step, in which you need to access each of the tuple's elements, only to create a bigger tuple, and so on.
While it does get the job done, we can do better.
Query syntax #
Instead of using method-call syntax to chain all these service registrations together, you can take advantage of C#'s query syntax which lights up for any functor:
var container = from conf in Container.Empty.Register(Configuration) let rdb = CompositionRoot.CreateRestaurantDatabase(conf) let po = CompositionRoot.CreatePostOffice(conf, rdb) let clock = CompositionRoot.CreateClock() let repo = CompositionRoot.CreateRepository(conf, po) select (conf, rdb, po, clock, repo); var compositionRoot = new CompositionRoot(container); services.AddSingleton<IControllerActivator>(compositionRoot);
In both cases, the container
object has the type Container<(IConfiguration conf, IRestaurantDatabase rdb, IPostOffice po, IClock clock, IReservationsRepository repo)>
, which is still quite the mouthful. Now, however, we can do something about it.
DIP and ISP applied #
The CompositionRoot
class only needs three services, so its constructor should ask for those, and no more:
private readonly Container<(IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo)> container; public CompositionRoot( Container<(IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo)> container) { this.container = container; }
With that injected container
it can implement Create
like this:
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.Item.rdb, container.Item.repo); else if (t == typeof(HomeController)) return new HomeController(container.Item.rdb); else if (t == typeof(ReservationsController)) return new ReservationsController( container.Item.clock, container.Item.rdb, container.Item.repo); else if (t == typeof(RestaurantsController)) return new RestaurantsController(container.Item.rdb); else if (t == typeof(ScheduleController)) return new ScheduleController( container.Item.rdb, container.Item.repo, AccessControlList.FromUser(context.HttpContext.User)); else throw new ArgumentException( $"Unexpected controller type: {t}.", nameof(context)); }
That's more readable, although the intermediary Item
object doesn't seem to do much work...
You can create a container
with the desired type like this:
Container<(IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo)> container = from conf in Container.Empty.Register(Configuration) let rdb = CompositionRoot.CreateRestaurantDatabase(conf) let po = CompositionRoot.CreatePostOffice(conf, rdb) let clock = CompositionRoot.CreateClock() let repo = CompositionRoot.CreateRepository(conf, po) select (rdb, clock, repo); var compositionRoot = new CompositionRoot(container); services.AddSingleton<IControllerActivator>(compositionRoot);
Notice that the only difference compared to earlier is that the select
expression only involves the three required services: rdb
, clock
, and repo
.
Conclusion #
This seems cleaner than before, but perhaps you're left with a nagging doubt: Why is the Container<T>
class even required? What value does its Item
property provide?
And perhaps another question is also in order: Does the Select
method shown even define a lawful functor?
Read on.