I was recently working on a project, where I had a need to inherit routes from a generic base Web API controller. This is not supported by Web API out of the box, but can be enabled with a tiny configuration tweak. Let’s have a look.
The problem with inheriting attribute routes π
If you look at the definition of the RouteAttribute in ASP.NET Web API, you will see that it’s marked as an “inheritable” attribute. As such, it’s reasonable to assume that if you use that attribute on a base controller, it will be respected in a child controller you create off the base one.
However, in reality, that is not the case, and that’s due to the internal logic in DefaultDirectRouteProvider - which is the default implementation of the way how Web API discovers attribute routes.
We discussed this class (and the entire extensibility point, as the direct route provider can be replaced) before - for example when implementing a centralized route prefix for Web API.
So if this is your generic Web API code, it will not work out of the box:
public abstract class GenericController<TEntity> : ApiController where TEntity : class, IMyEntityDefinition, new()
{
private readonly IGenericRepository<TEntity> _repo;
protected GenericController(IGenericRepository<TEntity> repo)
{
_repo = repo;
}
[Route("{id:int}")]
public virtual async Task<IHttpActionResult> Get(int id)
{
var result = await _repo.FindAsync(id);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
}
[RoutePrefix("api/items")]public class ItemController : GenericController<Item>
{
public GenericController(IGenericRepository<Item> repo) : base(repo)
{}
}
Ignoring the implementation details of the repository pattern here, assuming all your dependency injection is configured already - with the above controller, trying to hit api/items/{id} is going to produce 404.
The solution for inheriting attribute routes π
One of the methods that this default direct route provider exposes as overrideable, is the one shown below. It is responsible for extracting route attributes from an action descriptor:
protected virtual IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
// Ignore the Route attributes from inherited actions.
ReflectedHttpActionDescriptor reflectedActionDescriptor = actionDescriptor as ReflectedHttpActionDescriptor;
if (reflectedActionDescriptor != null &&
reflectedActionDescriptor.MethodInfo != null &&
reflectedActionDescriptor.MethodInfo.DeclaringType != actionDescriptor.ControllerDescriptor.ControllerType)
{
return null;
}
Collection<IDirectRouteFactory> newFactories = actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(inherit: false);
Collection<IHttpRouteInfoProvider> oldProviders = actionDescriptor.GetCustomAttributes<IHttpRouteInfoProvider>(inherit: false);
List<IDirectRouteFactory> combined = new List<IDirectRouteFactory>();
combined.AddRange(newFactories);
foreach (IHttpRouteInfoProvider oldProvider in oldProviders)
{
if (oldProvider is IDirectRouteFactory)
{
continue;
}
combined.Add(new RouteInfoDirectRouteFactory(oldProvider));
}
return combined;
}
Without going into too much details about this code - it’s clearly visible that it specifically ignores inherited route attributes (route attributes implement IDirectRouteFactory interface).
So in order to make our initial sample generic controller work, we need to override the above method and read all inherited routes. This is extremely simple and is shown below:
public class InheritanceDirectRouteProvider : DefaultDirectRouteProvider
{
protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(true);
}
}
This can now be registered at the application startup against your HttpConfiguration - which is shown in the next snippet as an extension method + OWIN Startup class.
public static class HttpConfigurationExtensions
{
public static void MapInheritedAttributeRoutes(this HttpConfiguration config)
{
config.MapHttpAttributeRoutes(new InheritanceDirectRouteProvider());
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
config.MapInheritedAttributeRoutes();
app.UseWebApi(config);
}
}
And that’s it!