Discovering Generic Controllers in ASP.NET Core

Brent Arias picture Brent Arias · Apr 17, 2016 · Viewed 16.5k times · Source

I am trying to create a generic controller like this:

[Route("api/[controller]")]
public class OrdersController<T> : Controller where T : IOrder
{
    [HttpPost("{orderType}")]
    public async Task<IActionResult> Create(
        [FromBody] Order<T> order)
    {
       //....
    }
}

I intend for the {orderType} URI segment variable to control the generic type of the controller. I'm experimenting with both a custom IControllerFactory and IControllerActivator, but nothing is working. Every time I try to send a request, I get a 404 response. The code for my custom controller factory (and activator) is never executed.

Evidently the problem is that ASP.NET Core expects valid controllers to end with the suffix "Controller", but my generic controller instead has the (reflection based) suffix "Controller`1". Thus the attribute-based routes it declares are going unnoticed.

In ASP.NET MVC, at least in its early days, the DefaultControllerFactory was responsible for discovering all the available controllers. It tested for the "Controller" suffix:

The MVC framework provides a default controller factory (aptly named DefaultControllerFactory) that will search through all the assemblies in an appdomain looking for all types that implement IController and whose name ends with "Controller."

Apparently, in ASP.NET Core, the controller factory no longer has this responsibility. As I stated earlier, my custom controller factory executes for "normal" controllers, but is never invoked for generic controllers. So there is something else, earlier in the evaluation process, which governs the discovery of controllers.

Does anyone know what "service" interface is responsible for that discovery? I don't know the customization interface or "hook" point.

And does anyone know of a way to make ASP.NET Core "dump" the names of all the controllers it discovered? It would be great to write a unit test that verifies that any custom controller discovery I expect is indeed working.

Incidentally, if there is a "hook" which allows generic controller names to be discovered, it implies that route substitutions must also be normalized:

[Route("api/[controller]")]
public class OrdersController<T> : Controller { }

Regardless of what value for T is given, the [controller] name must remain a simple base-generic name. Using the above code as an example, the [controller] value would be "Orders". It would not be "Orders`1" or "OrdersOfSomething".

Note

This problem could also be solved by explicitly declaring the closed-generic types, instead of generating them at run time:

public class VanityOrdersController : OrdersController<Vanity> { }
public class ExistingOrdersController : OrdersController<Existing> { }

The above works, but it produces URI paths that I don't like:

~/api/VanityOrders
~/api/ExistingOrders

What I had actually wanted was this:

~/api/Orders/Vanity
~/api/Orders/Existing

Another adjustment gets me the URI's I'm looking for:

[Route("api/Orders/Vanity", Name ="VanityLink")]
public class VanityOrdersController : OrdersController<Vanity> { }
[Route("api/Orders/Existing", Name = "ExistingLink")]
public class ExistingOrdersController : OrdersController<Existing> { }

However, although this appears to work, it does not really answer my question. I would like to use my generic controller directly at run-time, rather than indirectly (via manual coding) at compile-time. Fundamentally, this means I need ASP.NET Core to be able to "see" or "discover" my generic controller, despite the fact that its run-time reflection name does not end with the expected "Controller" suffix.

Answer

Shaun Luttin picture Shaun Luttin · Apr 18, 2016

Short Answer

Implement IApplicationFeatureProvider<ControllerFeature>.

Question and Answer

Does anyone know what "service" interface is responsible for [discovering all available controllers]?

The ControllerFeatureProvider is responsible for that.

And does anyone know of a way to make ASP.NET Core "dump" the names of all the controllers it discovered?

Do that within ControllerFeatureProvider.IsController(TypeInfo typeInfo).

Example

MyControllerFeatureProvider.cs

using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Controllers;

namespace CustomControllerNames 
{
    public class MyControllerFeatureProvider : ControllerFeatureProvider 
    {
        protected override bool IsController(TypeInfo typeInfo)
        {
            var isController = base.IsController(typeInfo);

            if (!isController)
            {
                string[] validEndings = new[] { "Foobar", "Controller`1" };

                isController = validEndings.Any(x => 
                    typeInfo.Name.EndsWith(x, StringComparison.OrdinalIgnoreCase));
            }

            Console.WriteLine($"{typeInfo.Name} IsController: {isController}.");

            return isController;
        }
    }
}

Register it during startup.

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvcCore()
        .ConfigureApplicationPartManager(manager => 
        {
            manager.FeatureProviders.Add(new MyControllerFeatureProvider());
        });
}

Here is some example output.

MyControllerFeatureProvider IsController: False.
OrdersFoobar IsController: True.
OrdersFoobarController`1 IsController: True.
Program IsController: False.
<>c__DisplayClass0_0 IsController: False.
<>c IsController: False.

And here is a demo on GitHub. Best of luck.

Edit - Adding Versions

.NET Version

> dnvm install "1.0.0-rc2-20221" -runtime coreclr -architecture x64 -os win -unstable

NuGet.Config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear/>
    <add key="AspNetCore" 
         value="https://www.myget.org/F/aspnetvnext/api/v3/index.json" />  
  </packageSources>
</configuration>

.NET CLI

> dotnet --info
.NET Command Line Tools (1.0.0-rc2-002429)

Product Information:
 Version:     1.0.0-rc2-002429
 Commit Sha:  612088cfa8

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.10586
 OS Platform: Windows
 RID:         win10-x64

Restore, Build, and Run

> dotnet restore
> dotnet build
> dotnet run

Edit - Notes on RC1 vs RC2

This might not be possible is RC1, because DefaultControllerTypeProvider.IsController() is marked as internal.