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.
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;
}
}