Constrained Open Generics Support Merged in .NET Core DI Container

Continuing the trend of extremely long blog titles...years ago I opened a pull request for supporting constrained open generics in the built-in Microsoft.Extensions.DependencyInjection container. And another. And another....and another. But I did not give up hope - after porting through a couple repo moves and consolidations, this feature is now merged and will get released with .NET 5 (huzzah).

So what is this mouthful going to get you? Well, for users of MediatR, quite a lot. Something I see quite a lot when building generic-oriented code are declarations of IFoo<T> where you have a method accepting that T. For example, Fluent Validation defines an IValidator<T> interface that is (roughly):

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

Notice the contravariance here - we can use more derived types in the T parameter. I often do this in validation when I have many different screens that share some common trait, or validation:

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

All DTOs that then want this common validation only need to implement the IContainUserInfo interface.

This is all well and good, but there is a slight problem here. My type parameter has lost its original type information, because I've closed the IValidator interface, so if I want to plug in additional dependencies based on that type, it's gone.

Constraining instead of closing the generic type preserves the generic parameter:

public class UserInfoValidator<T> : IValidator<T>
    where T : IContainUserInfo
{
    public ValidationResult Validate(T instance) {
        // I can still use T's members of IContainUserInfo
    }
}

What's important here is that because I still have the type parameter, I can now include dependencies here that still can be based on the type T. For example, I could do something like:

public class UserInfoValidator<T> : IValidator<T>
    where T : IContainUserInfo
{
    public UserInfoValidator(IEnumerable<IPermission<T>> permissions)
        => _permissions = permissions;

In this example, I depend on another collection of permission dependencies to validate my user information. However, because my generic type is still open, at runtime, only the IPermission<T> instances that satisfy T will be satisfied and supplied.

There's a gap in .NET to make this sort of check much easier, and I've opened an API proposal to help. Please vote if this proposal interests you!

Constrained generics are a powerful tool to apply cross-cutting concerns to generics in dependency injection, giving us a "DI-enabled" sort of pattern matching. The pull request that merged provides a simple fix (don't blow up).

What Took So Long?

Great question, especially since the change is so small (adding a try-catch), but it exposed a challenge that the MS team has to worry about. The built-in container is a conforming container - that is, it provides some default behaviors that ASP.NET Core requires, and if you want to replace that container, you need to conform to those behaviors.

This is a challenge for the DI container library authors, who have built their own libraries over the years, independently, and now need to conform to the needs and design of the MS.Ext.DI container. Conversely, external library authors (such as myself) want to build on top of the default DI container without needing to provide a bunch of library-specific extensions (MediatR.Autofac, MediatR.Lamar, MediatR.TinyIoc etc).

But what happens if you want to depend on a feature that:

  • Exists in most or all popular containers
  • Does not exist in the conforming container (MS.Ext.DI)
  • Is not needed by ASP.NET Core (the primary user)

That's where constrained open generics sat. It's not needed by the primary users (various .NET hosts) but is needed, or wanted, by a large number of people that use these .NET hosts built on top of the conforming container.

So, it's a risk - so what I wound up needing to do was:

  • Show that the majority (or all) of the containers support this behavior
  • Document this behavior explicitly with a set of specification tests

These specification tests run against the built-in container plus 8 popular ones (Autofac, Unity, Lamar etc.)

With this in place, I could show the expected behavior of this feature in DI containers, and prove that yes indeed, all the listed containers do support this feature.

And with that, and a little try..catch, this feature will drop in .NET 5.