How do you inherit route prefixes at the controller class level in WebApi?
I had a similar requirement. What i did was:
public class CustomDirectRouteProvider : DefaultDirectRouteProvider{ protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor) { var routePrefix = base.GetRoutePrefix(controllerDescriptor); var controllerBaseType = controllerDescriptor.ControllerType.BaseType; if (controllerBaseType == typeof(BaseController)) { //TODO: Check for extra slashes routePrefix = "api/{tenantid}/" + routePrefix; } return routePrefix; }}
Where BaseController
is the one defining what is the prefix. Now normal prefixes work and you can add your own. When configuring routes, call
config.MapHttpAttributeRoutes(new CustomDirectRouteProvider());
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) methodincludes an "inherit" boolean... but it's ignored for this methodsignature. ARG! Because if it worked, we could have dropped thereflection loop... which takes us to the next point:
- This traverses up the inheritance hierarchy with reflection. Not idealbecause of the O(n) calls through reflection, but required for myneeds. You can get rid of the loop if you only have 1 or 2 levelsof inheritance.
- Per the GOTCHA in the code, RoutePrefixAttributeis declared in System.Web.Http and in System.Web.Mvc. They bothinherit directly from Attribute, and they both implement their ownIRoutePrefix interface (i.e.System.Web.Http.RoutePrefixAttribute<--System.Web.Http.IRoutePrefixandSystem.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 isassigned. This makes sense, of course, but I lost 2 hoursrefactoring code that was actually legit because my test caseimplicitly checked for System.Web.Http.RoutePrefixAttribute but thecontroller was declared with System.Web.Mvc... Hence the explicit namespace in the code.
Tried this in ASP.NET Web Api 2.2 (should/might also work in MVC):
public class InheritedRoutePrefixDirectRouteProvider : DefaultDirectRouteProvider{ protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor) { var sb = new StringBuilder(base.GetRoutePrefix(controllerDescriptor)); var baseType = controllerDescriptor.ControllerType.BaseType; for (var t = baseType; typeof(ApiController).IsAssignableFrom(t); t = t.BaseType) { var a = (t as MemberInfo).GetCustomAttribute<RoutePrefixAttribute>(false); if (a != null) { sb.Insert(0, $"{a.Prefix}{(sb.Length > 0 ? "/": "")}"); } } return sb.ToString(); }}
It links the route prefixes together in the controller inheritance chain.