How do you inherit route prefixes at the controller class level in WebApi?

后端 未结 5 1393
粉色の甜心
粉色の甜心 2021-01-11 10:49

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

相关标签:
5条回答
  • 2021-01-11 11:29

    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.

    0 讨论(0)
  • 2021-01-11 11:43

    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.
    0 讨论(0)
  • 2021-01-11 11:49

    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) { ... }
    }
    
    0 讨论(0)
  • 2021-01-11 11:51

    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());
    
    0 讨论(0)
  • 2021-01-11 11:56

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

    [Route("account/[Controller]")]
    
    0 讨论(0)
提交回复
热议问题