As @HazardouS identifies, @Grbinho's answer is hard-coded. Borrowing from this answer to inheritance of direct routing and from @HazardouS, I wrote this object
public class InheritableDirectRouteProvider : DefaultDirectRouteProvider {}
Then overrode the following methods, hoping RoutePrefixAttribute would get inherited:
protected override IReadOnlyList<IDirectRouteFactory> GetControllerRouteFactories(HttpControllerDescriptor controllerDescriptor)
{
// Inherit route attributes decorated on base class controller
// GOTCHA: RoutePrefixAttribute doesn't show up here, even though we were expecting it to.
// Am keeping this here anyways, but am implementing an ugly fix by overriding GetRoutePrefix
return controllerDescriptor.GetCustomAttributes<IDirectRouteFactory>(true);
}
protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
// Inherit route attributes decorated on base class controller's actions
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(true);
}
Sadly, per the gotcha comment, RoutePrefixAttribute doesn't show up in the factory list. I didn't dig into why, in case anyone wants to research a little deeper into this.
So I kept those methods for future compatibility, and overrode the GetRoutePrefix method as follows:
protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor)
{
// Get the calling controller's route prefix
var routePrefix = base.GetRoutePrefix(controllerDescriptor);
// Iterate through each of the calling controller's base classes that inherit from HttpController
var baseControllerType = controllerDescriptor.ControllerType.BaseType;
while(typeof(IHttpController).IsAssignableFrom(baseControllerType))
{
// Get the base controller's route prefix, if it exists
// GOTCHA: There are two RoutePrefixAttributes... System.Web.Http.RoutePrefixAttribute and System.Web.Mvc.RoutePrefixAttribute!
// Depending on your controller implementation, either one or the other might be used... checking against typeof(RoutePrefixAttribute)
// without identifying which one will sometimes succeed, sometimes fail.
// Since this implementation is generic, I'm handling both cases. Preference would be to extend System.Web.Mvc and System.Web.Http
var baseRoutePrefix = Attribute.GetCustomAttribute(baseControllerType, typeof(System.Web.Http.RoutePrefixAttribute))
?? Attribute.GetCustomAttribute(baseControllerType, typeof(System.Web.Mvc.RoutePrefixAttribute));
if (baseRoutePrefix != null)
{
// A trailing slash is added by the system. Only add it if we're prefixing an existing string
var trailingSlash = string.IsNullOrEmpty(routePrefix) ? "" : "/";
// Prepend the base controller's prefix
routePrefix = ((RoutePrefixAttribute)baseRoutePrefix).Prefix + trailingSlash + routePrefix;
}
// Traverse up the base hierarchy to check for all inherited prefixes
baseControllerType = baseControllerType.BaseType;
}
return routePrefix;
}
Notes:
- Attribute.GetCustomAttributes(Assembly,Type,bool) method
includes an "inherit" boolean... but it's ignored for this method
signature. ARG! Because if it worked, we could have dropped the
reflection loop... which takes us to the next point:
- This traverses up the inheritance hierarchy with reflection. Not ideal
because of the O(n) calls through reflection, but required for my
needs. You can get rid of the loop if you only have 1 or 2 levels
of inheritance.
- Per the GOTCHA in the code, RoutePrefixAttribute
is declared in System.Web.Http and in System.Web.Mvc. They both
inherit directly from Attribute, and they both implement their own
IRoutePrefix interface (i.e.
System.Web.Http.RoutePrefixAttribute<--System.Web.Http.IRoutePrefix
and
System.Web.Mvc.RoutePrefixAttribute<--System.Web.Mvc.IRoutePrefix).
The end result is that the library used to declare your controller
(web.mvc or web.http) is the library whose RoutePrefixAttribute is
assigned. This makes sense, of course, but I lost 2 hours
refactoring code that was actually legit because my test case
implicitly checked for System.Web.Http.RoutePrefixAttribute but the
controller was declared with System.Web.Mvc... Hence the explicit namespace in the code.