A type-safe DI Container as a tuple by Mark Seemann
Tuples are DI Containers. With example code in C#.
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 as a functor. 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.
The previous article demonstrated how you can view a type-safe DI Container as a functor. The payload is a registry of services. You could introduce a specialised Parameter Object with each service given a name, but really, since a DI container is little more than a dictionary keyed by type, if you give each element of a tuple a unique type, then a tuple of services is all you need.
Identity #
In the previous article, I asked the question: Does Container<T>
and its Select
method form a lawful functor? In other words, does it obey the functor laws?
In the article, I just left the question dangling, so you might wonder if there's something fishy going on. In a sense, there is.
The Container functor is simply the Identity functor in disguise. Just imagine renaming Container<T>
to Identity<T>
and this should be clear. Since the Identity functor is lawful, the Container functor is also lawful.
On the other hand, the Identity functor is redundant. It doesn't offer any additional capabilities over its payload. Anything you can do with the Identity functor's payload, you can do directly to the payload. An even stronger statement is that the Identity functor is isomorphic to its underlying payload. This is true also for Container<T>
, which is isomorphic to T
. This is clear from the definition of the class:
public sealed class Container<T> { public Container(T item) { Item = item; } public T Item { get; } // More members to follow...
You can convert any T
to a Container<T>
by calling the constructor, and you can convert any Container<T>
to a T
value by reading the Item
property.
Then why have the Container at all?
Tuple as a DI Container #
In the previous article, we left off with a container defined as Container<(IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo)>
. It follows that the type of the Container's Item
property is (IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo)
- a tuple with three elements.
This code example makes use of C# tuple types, which enables you to give each element in the tuple a name. For the tuple in question, the first element is called rdb
, the next element clock
, and the third repo
. This makes the code that accesses these elements more readable, but is structurally unimportant. If you work in a language without this tuple feature, it doesn't change the conclusion in this article series.
Instead of relying on Container<(IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo)>
, the CompositionRoot
class can rely on the unwrapped tuple:
private readonly (IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo) container; public CompositionRoot( (IRestaurantDatabase rdb, IClock clock, IReservationsRepository repo) container) { this.container = container; }
Its Create
method directly accesses the tuple elements to create Controllers:
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.rdb, container.repo); else if (t == typeof(HomeController)) return new HomeController(container.rdb); else if (t == typeof(ReservationsController)) return new ReservationsController( container.clock, container.rdb, container.repo); else if (t == typeof(RestaurantsController)) return new RestaurantsController(container.rdb); else if (t == typeof(ScheduleController)) return new ScheduleController( container.rdb, container.repo, AccessControlList.FromUser(context.HttpContext.User)); else throw new ArgumentException( $"Unexpected controller type: {t}.", nameof(context)); }
Notice that instead of container.Item.rdb
, container.Item.repo
, and container.Item.clock
, there's no redundant Item
property. The Create
method can directly access container.rdb
, container.repo
, and container.clock
.
Registration #
Not only did it become easier to resolve services from the Container (i.e. the tuple) - registering services is also simpler:
var rdb = CompositionRoot.CreateRestaurantDatabase(Configuration); var po = CompositionRoot.CreatePostOffice(Configuration, rdb); var clock = CompositionRoot.CreateClock(); var repo = CompositionRoot.CreateRepository(Configuration, po); var container = (rdb, clock, repo); var compositionRoot = new CompositionRoot(container); services.AddSingleton<IControllerActivator>(compositionRoot);
Just create the services and put them in a tuple of the desired shape. No query syntax or other compiler tricks are required. Just write normal code.
Conclusion #
The Container<T>
class is just the Identity functor in disguise. It's redundant, so you can delete it and instead use a tuple, a record type, or a Parameter Object as your service registry.
Are we done now, or can we simplify things even further? Read on.