Search code examples
c#asp.net-mvc-5asp.net-web-api-routing

web api 2 attribute based route generation


I've been messing around with MVC5 and WebApi2. At one point it seems there was a convention based auto-name for RouteAttributes -- "ControllerName.ActionName". I have a large api with many ApiControllers and a custom routing defined using attributes. I can use the urls directly and it works well, and ApiExplorer does just fine with it.

Then I get to the point where I need to generate links and for some fields in my dto objects as update urls. I've tried calling:

Url.Link("", new { controller = "...", action = "...", [other data...] })

but it uses the default global route defined which is not usable.

Is there no way to generate links for attribute based routes that do not have a name defined using UrlHelper.Link?

Any input would be appreciated, thanks.


Solution

  • Using the algorithms described here, I opted to using ApiExplorer to fetch out the routes that match a given set of values.

    Example of usage:

    [RoutePrefix( "api/v2/test" )]
    public class EntityController : ApiController {
        [Route( "" )]
        public IEnumerable<Entity> GetAll() {
            // ...
        }
    
        [Route( "{id:int}" )]
        public Entity Get( int id ) {
            // ...
        }
    
        // ... stuff
    
        [HttpGet]
        [Route( "{id:int}/children" )]
        public IEnumerable[Child] Children( int id ) {
            // ...
        }
    }
    
    ///
    /// elsewhere
    ///
    
    // outputs: api/v2/test/5
    request.HttpRouteUrl( HttpMethod.Get, new {
        controller = "entity",
        id = 5
    } )
    
    // outputs: api/v2/test/5/children
    request.HttpRouteUrl( HttpMethod.Get, new {
        controller = "entity",
        action = "children",
        id = 5
    } )
    

    Here's the implementation:

    public static class HttpRouteUrlExtension {
        private const string HttpRouteKey = "httproute";
    
        private static readonly Type[] SimpleTypes = new[] {
            typeof (DateTime), 
            typeof (Decimal), 
            typeof (Guid), 
            typeof (string), 
            typeof (TimeSpan)
        };
    
        public static string HttpRouteUrl( this HttpRequestMessage request, HttpMethod method, object routeValues ) {
            return HttpRouteUrl( request, method, new HttpRouteValueDictionary( routeValues ) );
        }
    
        public static string HttpRouteUrl( this HttpRequestMessage request, HttpMethod method, IDictionary<string, object> routeValues ) {
            if ( routeValues == null ) {
                throw new ArgumentNullException( "routeValues" );
            }
    
            if ( !routeValues.ContainsKey( "controller" ) ) {
                throw new ArgumentException( "'controller' key must be provided", "routeValues" );
            }
    
            routeValues = new HttpRouteValueDictionary( routeValues );
            if ( !routeValues.ContainsKey( HttpRouteKey ) ) {
                routeValues.Add( HttpRouteKey, true );
            }
    
            string controllerName = routeValues[ "controller" ].ToString();
            routeValues.Remove( "controller" );
    
            string actionName = string.Empty;
            if ( routeValues.ContainsKey( "action" ) ) {
                actionName = routeValues[ "action" ].ToString();
                routeValues.Remove( "action" );
            }
    
            IHttpRoute[] matchedRoutes = request.GetConfiguration().Services
                                                .GetApiExplorer().ApiDescriptions
                                                .Where( x => x.ActionDescriptor.ControllerDescriptor.ControllerName.Equals( controllerName, StringComparison.OrdinalIgnoreCase ) )
                                                .Where( x => x.ActionDescriptor.SupportedHttpMethods.Contains( method ) )
                                                .Where( x => string.IsNullOrEmpty( actionName ) || x.ActionDescriptor.ActionName.Equals( actionName, StringComparison.OrdinalIgnoreCase ) )
                                                .Select( x => new {
                                                    route = x.Route,
                                                    matches = x.ActionDescriptor.GetParameters()
                                                               .Count( p => ( !p.IsOptional ) &&
                                                                       ( p.ParameterType.IsPrimitive || SimpleTypes.Contains( p.ParameterType ) ) &&
                                                                       ( routeValues.ContainsKey( p.ParameterName ) ) &&
                                                                       ( routeValues[ p.ParameterName ].GetType() == p.ParameterType ) )
                                                } )
                                                .Where(x => x.matches > 0)
                                                .OrderBy( x => x.route.DataTokens[ "order" ] )
                                                .ThenBy( x => x.route.DataTokens[ "precedence" ] )
                                                .ThenByDescending( x => x.matches )
                                                .Select( x => x.route )
                                                .ToArray();
    
            if ( matchedRoutes.Length > 0 ) {
                IHttpVirtualPathData pathData = matchedRoutes[ 0 ].GetVirtualPath( request, routeValues );
    
                if ( pathData != null ) {
                    return new Uri( new Uri( httpRequestMessage.RequestUri.GetLeftPart( UriPartial.Authority ) ), pathData.VirtualPath ).AbsoluteUri;
                }
            }
    
            return null;
        }
    }