15

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 to solve the issue of inheriting action level route attributes, but not route prefixes defined at the class level. http://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-22#ARI

I would like to do something like this:

[RoutePrefix("account")]
public abstract class AccountControllerBase : ControllerBase { }

[RoutePrefix("facebook")]
public class FacebookController : AccountControllerBase
{
    [Route("foo")]
    public async Task<string> GetAsync() { ... }
}

[RoutePrefix("google")]
public class GoogleController : AccountControllerBase
{
    [Route("bar")]
    public async Task<string> GetAsync() { ... }
}

I would like the account route prefix to be inherited, so when defining the Facebook and Google controllers, I get routes:

~/account/facebook/foo
~/account/google/bar

Currently, routes are getting defined without the account portion from the base class.

Scott Lin
  • 1,532
  • 1
  • 18
  • 28
  • possible duplicate of [.NET WebAPI Attribute Routing and inheritance](http://stackoverflow.com/questions/19989023/net-webapi-attribute-routing-and-inheritance) – Stilgar Dec 14 '14 at 02:32
  • 8
    Not a duplicate. That question is asking about the RoutePrefix on the controller implementing the base controller. I'm asking about putting the RoutePrefix on the base controller. Please read again. Thanks. – Scott Lin Dec 14 '14 at 02:50

5 Answers5

16

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());
Grbinho
  • 408
  • 4
  • 12
  • @kurifodo worked for me. please accept answer if it worked for you too :) – Tomasz Pluskiewicz Feb 25 '16 at 10:22
  • 3
    This solution hard codes the desired prefix within the provider. This is not scalable. The OP wants inheritance based prefixes. – hazardous Jun 27 '16 at 13:28
  • 1
    `if (controllerBaseType == typeof(BaseApiController)) { var baseRoutePrefixObject = controllerBaseType.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(RoutePrefixAttribute))?.ConstructorArguments.FirstOrDefault().Value; String baseRoutePrefix = baseRoutePrefixObject as String; routePrefix = $"{baseRoutePrefix ?? String.Empty}/{routePrefix}"; }` for excluding hardcode – Xordal Feb 28 '18 at 11:51
9

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:

  1. 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:
  2. 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.
  3. 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.
Community
  • 1
  • 1
HeyZiko
  • 1,660
  • 15
  • 28
  • This is a great answer, the only caveat is, what if system.web.mvc is not referenced at all? Is there a way to make this even more generic? – James Blake Dec 09 '16 at 14:26
  • @JamesBlake If you're not including the System.Web.Mvc library in the project, the simplest approach would be to cull the line that references it. Other than that, you could use something like [this](http://stackoverflow.com/a/22784842/1876622) to reflect on what assemblies are loaded... but that's overkill IMO – HeyZiko Dec 09 '16 at 16:51
  • Worked for me. Just need to add config.MapHttpAttributeRoutes(new InheritableDirectRouteProvider()); to the WebApiConfig. – Mideus Apr 10 '20 at 14:59
0

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.

  • Could you show me an example ? I've copied you code to my project but it doesn't work. My controller structure is the same as the question's – Redplane Jun 06 '18 at 06:10
0

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) { ... }
}
Dustin C
  • 215
  • 6
  • 15
-1

Maybe it is late, but I think this base controller attribute will make it work:

[Route("account/[Controller]")]
MyNameIsCaleb
  • 4,409
  • 1
  • 13
  • 31
  • 1
    You may want to add more information on **why** you believe that would work with potentially links to the documentation or your answer may be flagged as Low Quality. Here are some guidelines for [How do I write a good answer?](https://stackoverflow.com/help/how-to-answer). This provided answer may be correct, but it could benefit from an explanation. Code only answers are not considered "good" answers. From [review](https://stackoverflow.com/review). – MyNameIsCaleb Sep 22 '19 at 17:42
  • Here is the Documentation mentioning this pattern: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.1#token-replacement-in-route-templates-controller-action-area – HeikoG Apr 15 '20 at 08:02