Generic controllers in ASP.Net Core
From my archive - originally published on 28 November 2016
ASP.Net Core ignores generic controllers by default, so you have to add support for them yourself at start up. This is done by manipulating application parts, a mechanism which allow you to discover and load MVC features at runtime.
It’s worth pointing out that approach can give rise to a kind of generic data repository that I regard as a fairly lazy anti-pattern. A generic solution may reduce the amount of code but it often provides too much of a generalisation that does not define a meaningful contract. That said, it can be a useful approach to addressing some uncommon generic use cases, such as front-line import APIs that just ingest data for validation and processing elsewhere.
Identifying the entities
At application start-up the feature provider will need a list of the entity classes that can be passed to the generic controller. You will need a mechanism for creating a list of TypeInfo objects for each entity class that you want to support.
The easiest way of doing this is just to maintain a list of classes, i.e.
public static class IncludedEntities{ public static IReadOnlyList<TypeInfo> Types = new List<TypeInfo> { typeof(Animals).GetTypeInfo(), typeof(Insects).GetTypeInfo(), };}
For example, if we create the following attribute and use it to mark a simple entity class:
/// <summary>/// This is just a marker attribute used to allow us to identifier which entities to expose in the API/// </summary>[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]public class ApiEntityAttribute : Attribute{}// These are two example entities that will be supported by the generic controller [ApiEntityAttribute]public class Animals { } [ApiEntityAttribute]public class Insects { }
public static class IncludedEntities{ public static IReadOnlyList<TypeInfo> Types; static IncludedEntities() { var assembly = typeof(IncludedEntities).GetTypeInfo().Assembly; var typeList = new List<TypeInfo>(); foreach (Type type in assembly.GetTypes()) { if (type.GetCustomAttributes(typeof(ApiEntityAttribute), true).Length > 0) { typeList.Add(type.GetTypeInfo()); } } Types = typeList; }}
Create the generic controller
In creating a generic controller we want any URL routes to default to the name of the entity class rather than the name of the controller. This can be done by creating a class attribute that implements the IControllerModelConvention interface as shown below:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]public class GenericControllerNameAttribute : Attribute, IControllerModelConvention{ public void Apply(ControllerModel controller) { if (controller.ControllerType.GetGenericTypeDefinition() == typeof(GenericController<>)) { var entityType = controller.ControllerType.GenericTypeArguments[0]; controller.ControllerName = entityType.Name; } }}
[Route("[controller]")][GenericControllerNameAttribute]public class GenericController<T> : Controller{ [HttpGet] public IActionResult IndexAsync() { return Content($"GET from a {typeof(T).Name} controller."); } [HttpPost] public IActionResult Create([FromBody] IEnumerable<T> items) { return Content($"POST to a {typeof(T).Name} controller."); }}
Adding the feature provider
The feature provider runs at start up and it runs after the regular ControllerTypeProvider class. It looks through the collection of TypeInfo objects we have created for all the model classes and adds them in as supported types for our generic controller.
public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>{ public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature) { // Get the list of entities that we want to support for the generic controller foreach (var entityType in IncludedEntities.Types) { var typeName = entityType.Name + "Controller"; // Check to see if there is a "real" controller for this class if (!feature.Controllers.Any(t => t.Name == typeName)) { // Create a generic controller for this type var controllerType = typeof(GenericController<>).MakeGenericType(entityType.AsType()).GetTypeInfo(); feature.Controllers.Add(controllerType); } } }}
public void ConfigureServices(IServiceCollection services){ services.AddMvc().ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new GenericControllerFeatureProvider())); }