Note, I\'ve read about the new routing features as part of WebApi 2.2 to allow for inheritance of routes. This does not seem to solve my particular issue, however. It seems
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.
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:
I just ran into this same issue on a .NET Core 3.0 app (seems to be a new feature in MVC 6 so it won't work for MVC 5 and previous versions but may still be helpful for anyone else that stumbles across this problem). I don't have enough rep to make a comment on @EmilioRojo's answer but he is correct. Here is some more information from the Microsoft Docs to help people that come across the same issue.
Token replacement in route templates ([controller], [action], [area]) For convenience, attribute routes support token replacement by enclosing a token in square-braces ([, ]). The tokens [action], [area], and [controller] are replaced with the values of the action name, area name, and controller name from the action where the route is defined. In the following example, the actions match URL paths as described in the comments:
[Route("[controller]/[action]")]
public class ProductsController : Controller
{
[HttpGet] // Matches '/Products/List'
public IActionResult List() {
// ...
}
[HttpGet("{id}")] // Matches '/Products/Edit/{id}'
public IActionResult Edit(int id) {
// ...
}
}
Attribute routes can also be combined with inheritance. This is particularly powerful combined with token replacement.
[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }
public class ProductsController : MyBaseController
{
[HttpGet] // Matches '/api/Products'
public IActionResult List() { ... }
[HttpPut("{id}")] // Matches '/api/Products/{id}'
public IActionResult Edit(int id) { ... }
}
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());
Maybe it is late, but I think this base controller attribute will make it work:
[Route("account/[Controller]")]