3

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.

2 Answers 2

1

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;
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Also, I might remove it as an extension on HttpRequestMessage, and use GlobalConfiguration within the method instead..
Don't. GlobalConfiguration is only available when using IIS hosting, not in Owin.
1

What about Route Names? Perhaps you can expose those in your DTOs like you could in a view.

Controller:

[Route("menu", Name = "mainmenu")]
public ActionResult MainMenu() { ... }`

View:

<a href="@Url.RouteUrl("mainmenu")">Main menu</a>

4 Comments

That would work, the issue is with the custom api routes. It's a handful and kind of tedious to specify a name for each and every one. I was wondering if there was a way to get around that.
Hmm, reflection comes to mind. Have you looked at Tim Scott's solution? Perhaps there's a fit or gets you on the right track.
I'll take a look at it tomorrow at work. I already use ApiExplorer and T4 to generate javascript for api helper methods on the client. I might go that direction with this as well. I don't mind reflection as long as its maintainable in the end.
I'll assume this worked once upon a time, but as of MVC5, with attribute routing, I cannot seem to generate a route to a webapi2 action with @Url.RouteUrl(routeName) even after specifying the route name in the RouteAttribute. It simply provides a blank string.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.