Rob West

Constrained Open Generics and Dependency Injection

Published 15 Oct 2020

Jimmy Bogard, author of those stalwart NuGet packages Automapper and MediatR, recently blogged about an upcoming change that he championed in the behaviour of the .NET Core Dependency Injection container. The change related to constrained open generics, a pattern that I had not even considered, which opens up some interesting possibilities. It led me to a (slightly hacky) solution to a design problem I was having with SimpleDatastore.

The Problem

You can go read Jimmy's blog post for an in-depth explanation, but I'll quickly summarise it here. He uses validation as a way to demonstrate the issue, so we start with a generic interface:

public interface IValidator<in T> {
    ValidationResult Validate(T instance);
}

This has a contravariant type parameter, denoted by the in modifier. Generic variance is way more confusing that it should be because if the nomenclature used - you have to worry when Jon Skeet sets it up by saying it is easier to show than to describe! Put as simply as I can, in this case it means the type is not being returned and this allows you to use more derived types. Jimmy gives this example:

public class UserInfoValidator : IValidator<IContainUserInfo> {
    public ValidationResult Validate(IContainUserInfo instance) {
    }
}

Where many DTOs can then implement IContainsUserInfo and use this single validator. Notice that we have closed the generic type. This is the way I would normally do things, but the downside to this is that we cannot have any additional dependencies that are based on the type. The interesting thing that Jimmy does is to keep the generic open and use a constraint instead so that he can do something like this:

public class UserInfoValidator<T> : IValidator<T>
    where T : IContainUserInfo
{
    private IEnumerable<IPermission<IContainUserInfo>> _permissions;

    public UserInfoValidator(IEnumerable<IPermission<T>> permissions)
        => _permissions = permissions;

    public ValidationResult Validate(T instance) {
        // I can still use T's members of IContainUserInfo
    }
}

Jimmy's problem is that the .NET Core Dependency Injection container does not respect these constraints, opening up the possibility of an exception at runtime. His immediate suggestion was accepted for the .NET 5 release and involves a try catch. Ugly, but no worse than what already happens under the hood. His longer term proposal is the addition of a Type.CanMakeGenericType method using the Tester-Doer pattern.

Hacking A Solution With Open Generics

Leaving generics open is interesting to me because I am adding support for JSON to SimpleDatastore. The original version utilised XML to persist POCOs, but I wanted an opportunity to play with the new System.Text.Json and see how it performed.

The original interfaces such as the IDocumentProvider used XML related entities:

public interface IDocumentProvider<T> where T : PersistentObject
{
    Task<XDocument> GetDocumentAsync();
    XDocument GetDocument();
    Task SaveDocumentAsync(XDocument document);
    void SaveDocument(XDocument document);
}

and so the obvious way to abstract away from XML was to introduce an additional generic parameter:

public interface IDocumentProvider<T, TDocument> where T : PersistentObject
{
    Task<TDocument> GetDocumentAsync();
    TDocument GetDocument();
    Task SaveDocumentAsync(TDocument document);
    void SaveDocument(TDocument document);
}

My initial attempt to implement these interfaces involved closing the TDocument generic type parameter:

public class DocumentProviderXml<T> : IDocumentProvider<T, XDocument>
    where T : PersistentObject
{
    // implementation
}

However, the .NET Core Dependency Injection container is not sophisticated enough to cope with this and throws an exception because interface and implementation do not have the same generic arity. Annoying, as this meant potentially creating divergent interfaces for each storage format which is ugly. The lightbulb moment came from realising that I could leave the generic open:

public class DocumentProviderXml<T, TDocument> : IDocumentProvider<T, XDocument>
    where T : PersistentObject
    where TDocument : XDocument
{
    // implementation
}

Now in this case I am able to supply a type constraint of XDocument, although this isn't possible for JsonDocument or JsonElement as they are sealed and there are no relevant interfaces to use. It's a hack anyway, as I'm ignoring the generic type parameter within the implementation, the parameter's only role is to ensure the calling code uses the correct type, and that is only necessary to prevent the .NET Core Dependency Injection container from blowing up as the interface and implementation have the same arity. I know it is nasty, but I can't see a better way without ditching support for the default container, and I don't want to do that.

Once we have .NET 5 I will certainly be adopting the "proper" constrained generic pattern as Jimmy set out as a powerful way to apply cross-cutting concerns. It will certainly give me cause to stop and think before I close a generic type parameter in future.

© 2023 Rob West. All Rights Reserved. Built using Kontent and Gatsby.