Discovering Generic Controllers in ASP.NET Core

What happens by default

During the controller discovery process, your open generic Controller<T> class will be among the candidate types. But the default implementation of the IApplicationFeatureProvider<ControllerFeature> interface, DefaultControllerTypeProvider, will eliminate your Controller<T> because it rules out any class with open generic parameters.

Why overriding IsController() doesn’t work

Replacing the default implementation of the IApplicationFeatureProvider<ControllerFeature> interface, in order to override DefaultControllerTypeProvider.IsController(), will not work. Because you don’t actually want the discovery process to accept your open generic controller (Controller<T>) as a valid controller. It is not a valid controller per se, and the controller factory wouldn’t know how to instantiate it anyway, because it wouldn’t know what T is supposed to be.

What needs to be done

1. Generate closed controller types

Before the controller discovery process even starts, you need to generate closed generic types from your open generic controller, using reflection. Here, with two sample entity types, named Account and Contact:

Type[] entityTypes = new[] { typeof(Account), typeof(Contact) };
TypeInfo[] closedControllerTypes = entityTypes
    .Select(et => typeof(Controller<>).MakeGenericType(et))
    .Select(cct => cct.GetTypeInfo())
    .ToArray();

We now have closed TypeInfos for Controller<Account> and Controller<Contact>.

2. Add them to an application part and register it

Application parts are usually wrapped around CLR assemblies, but we can implement a custom application part providing a collection of types generated at runtime. We simply need to have it implement the IApplicationPartTypeProvider interface. Therefore, our runtime-generated controller types will enter the controller discovery process like any other built-in type would.

The custom application part:

public class GenericControllerApplicationPart : ApplicationPart, IApplicationPartTypeProvider
{
    public GenericControllerApplicationPart(IEnumerable<TypeInfo> typeInfos)
    {
        Types = typeInfos;
    }

    public override string Name => "GenericController";
    public IEnumerable<TypeInfo> Types { get; }
}

Registration in MVC services (Startup.cs):

services.AddMvc()
    .ConfigureApplicationPartManager(apm =>
        apm.ApplicationParts.Add(new GenericControllerApplicationPart(closedControllerTypes)));

As long as your controller derives from the built-in Controller class, there is no actual need to override the IsController method of the ControllerFeatureProvider. Because your generic controller inherits the [Controller] attribute from ControllerBase, it will be accepted as a controller in the discovery process regardless of its somewhat bizarre name (“Controller`1”).

3. Override the controller name in the application model

Nevertheless, “Controller`1” is not a good name for routing purposes. You want each of your closed generic controllers to have independent RouteValues. Here, we will replace the name of the controller with that of the entity type, to match what would happen with two independent “AccountController” and “ContactController” types.

The model convention attribute:

public class GenericControllerAttribute : Attribute, IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        Type entityType = controller.ControllerType.GetGenericArguments()[0];

        controller.ControllerName = entityType.Name;
    }
}

Applied to the controller class:

[GenericController]
public class Controller<T> : Controller
{
}

Conclusion

This solution stays close to the overall ASP.NET Core architecture and, among other things, you will keep full visibility of your controllers through the API Explorer (think “Swagger”).

It has been tested successfully with both conventional and attribute-based routing.

Leave a Comment