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.
To get a list of controllers in RC2, just get ApplicationPartManager from DependencyInjection and do this:
Short Answer
Implement
IApplicationFeatureProvider<ControllerFeature>
.Question and Answer
The
ControllerFeatureProvider
is responsible for that.Do that within
ControllerFeatureProvider.IsController(TypeInfo typeInfo)
.Example
MyControllerFeatureProvider.cs
Register it during startup.
Here is some example output.
And here is a demo on GitHub. Best of luck.
Edit - Adding Versions
.NET Version
NuGet.Config
.NET CLI
Restore, Build, and Run
Edit - Notes on RC1 vs RC2
This might not be possible is RC1, because
DefaultControllerTypeProvider.IsController()
is marked asinternal
.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 theIApplicationFeatureProvider<ControllerFeature>
interface,DefaultControllerTypeProvider
, will eliminate yourController<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 overrideDefaultControllerTypeProvider.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 whatT
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
andContact
:We now have closed
TypeInfos
forController<Account>
andController<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:
Registration in MVC services (
Startup.cs
):As long as your controller derives from the built-in
Controller
class, there is no actual need to override theIsController
method of theControllerFeatureProvider
. Because your generic controller inherits the[Controller]
attribute fromControllerBase
, 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:
Applied to the controller class:
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.
Application Feature Providers examine application parts and provide features for those parts. There are built-in feature providers for the following MVC features:
Feature providers inherit from IApplicationFeatureProvider, where T is the type of the feature. You can implement your own feature providers for any of MVC's feature types listed above. The order of feature providers in the ApplicationPartManager.FeatureProviders collection can be important, since later providers can react to actions taken by previous providers.
By default, ASP.NET Core MVC ignores generic controllers (for example, SomeController). This sample uses a controller feature provider that runs after the default provider and adds generic controller instances for a specified list of types (defined in EntityTypes.Types):
The entity types:
The feature provider is added in Startup:
By default, the generic controller names used for routing would be of the form GenericController`1[Widget] instead of Widget. The following attribute is used to modify the name to correspond to the generic type used by the controller:
using Microsoft.AspNetCore.Mvc.ApplicationModels; using System;
The GenericController class:
Sample: Generic controller feature