How can I generate a WebApi2 URL without specifying a Name on the Route attribute with AttributeRouting? How can I generate a WebApi2 URL without specifying a Name on the Route attribute with AttributeRouting? asp.net asp.net

How can I generate a WebApi2 URL without specifying a Name on the Route attribute with AttributeRouting?


Using a work around to find the route via inspection of Web Api's IApiExplorer along with strongly typed expressions I was able to generate a WebApi2 URL without specifying a Name on the Route attribute with attribute routing.

I've created a helper extension which allows me to have strongly typed expressions with UrlHelper in MVC razor. This works very well for resolving URIs for my MVC Controllers from with in views.

<a href="@(Url.Action<HomeController>(c=>c.Index()))">Home</a><li>@(Html.ActionLink<AccountController>("Sign in", c => c.Signin(null)))</li><li>@(Html.ActionLink<AccountController>("Create an account", c => c.Signup(), htmlAttributes: null))</li>@using (Html.BeginForm<ToolsController>(c => c.Track(null), FormMethod.Get, htmlAttributes: new { @class = "navbar-form", role = "search" })) {...}    

I now have a view where I am trying to use knockout to post some data to my web api and need to be able to do something like this

var targetUrl = '@(Url.HttpRouteUrl<TestsApiController>(c => c.TestAction(null)))';

so that I don't have to hard code my urls (Magic strings)

My current implementation of my extension method for getting the web API url is defined in the following class.

public static class GenericUrlActionHelper {    /// <summary>    /// Generates a fully qualified URL to an action method     /// </summary>    public static string Action<TController>(this UrlHelper urlHelper, Expression<Action<TController>> action)       where TController : Controller {        RouteValueDictionary rvd = InternalExpressionHelper.GetRouteValues(action);        return urlHelper.Action(null, null, rvd);    }    public const string HttpAttributeRouteWebApiKey = "__RouteName";    public static string HttpRouteUrl<TController>(this UrlHelper urlHelper, Expression<Action<TController>> expression)       where TController : System.Web.Http.Controllers.IHttpController {        var routeValues = expression.GetRouteValues();        var httpRouteKey = System.Web.Http.Routing.HttpRoute.HttpRouteKey;        if (!routeValues.ContainsKey(httpRouteKey)) {            routeValues.Add(httpRouteKey, true);        }        var url = string.Empty;        if (routeValues.ContainsKey(HttpAttributeRouteWebApiKey)) {            var routeName = routeValues[HttpAttributeRouteWebApiKey] as string;            routeValues.Remove(HttpAttributeRouteWebApiKey);            routeValues.Remove("controller");            routeValues.Remove("action");            url = urlHelper.HttpRouteUrl(routeName, routeValues);        } else {            var path = resolvePath<TController>(routeValues, expression);            var root = getRootPath(urlHelper);            url = root + path;        }        return url;    }    private static string resolvePath<TController>(RouteValueDictionary routeValues, Expression<Action<TController>> expression) where TController : Http.Controllers.IHttpController {        var controllerName = routeValues["controller"] as string;        var actionName = routeValues["action"] as string;        routeValues.Remove("controller");        routeValues.Remove("action");        var method = expression.AsMethodCallExpression().Method;        var configuration = System.Web.Http.GlobalConfiguration.Configuration;        var apiDescription = configuration.Services.GetApiExplorer().ApiDescriptions           .FirstOrDefault(c =>               c.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(TController)               && c.ActionDescriptor.ControllerDescriptor.ControllerType.GetMethod(actionName) == method               && c.ActionDescriptor.ActionName == actionName           );        var route = apiDescription.Route;        var routeData = new HttpRouteData(route, new HttpRouteValueDictionary(routeValues));        var request = new System.Net.Http.HttpRequestMessage();        request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration;        request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;        var virtualPathData = route.GetVirtualPath(request, routeValues);        var path = virtualPathData.VirtualPath;        return path;    }    private static string getRootPath(UrlHelper urlHelper) {        var request = urlHelper.RequestContext.HttpContext.Request;        var scheme = request.Url.Scheme;        var server = request.Headers["Host"] ?? string.Format("{0}:{1}", request.Url.Host, request.Url.Port);        var host = string.Format("{0}://{1}", scheme, server);        var root = host + ToAbsolute("~");        return root;    }    static string ToAbsolute(string virtualPath) {        return VirtualPathUtility.ToAbsolute(virtualPath);    }}

InternalExpressionHelper.GetRouteValues inspects the expression and generates a RouteValueDictionary that will be used to generate the url.

static class InternalExpressionHelper {    /// <summary>    /// Extract route values from strongly typed expression    /// </summary>    public static RouteValueDictionary GetRouteValues<TController>(        this Expression<Action<TController>> expression,        RouteValueDictionary routeValues = null) {        if (expression == null) {            throw new ArgumentNullException("expression");        }        routeValues = routeValues ?? new RouteValueDictionary();        var controllerType = ensureController<TController>();        routeValues["controller"] = ensureControllerName(controllerType); ;        var methodCallExpression = AsMethodCallExpression<TController>(expression);        routeValues["action"] = methodCallExpression.Method.Name;        //Add parameter values from expression to dictionary        var parameters = buildParameterValuesFromExpression(methodCallExpression);        if (parameters != null) {            foreach (KeyValuePair<string, object> parameter in parameters) {                routeValues.Add(parameter.Key, parameter.Value);            }        }        //Try to extract route attribute name if present on an api controller.        if (typeof(System.Web.Http.Controllers.IHttpController).IsAssignableFrom(controllerType)) {            var routeAttribute = methodCallExpression.Method.GetCustomAttribute<System.Web.Http.RouteAttribute>(false);            if (routeAttribute != null && routeAttribute.Name != null) {                routeValues[GenericUrlActionHelper.HttpAttributeRouteWebApiKey] = routeAttribute.Name;            }        }        return routeValues;    }    private static string ensureControllerName(Type controllerType) {        var controllerName = controllerType.Name;        if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) {            throw new ArgumentException("Action target must end in controller", "action");        }        controllerName = controllerName.Remove(controllerName.Length - 10, 10);        if (controllerName.Length == 0) {            throw new ArgumentException("Action cannot route to controller", "action");        }        return controllerName;    }    internal static MethodCallExpression AsMethodCallExpression<TController>(this Expression<Action<TController>> expression) {        var methodCallExpression = expression.Body as MethodCallExpression;        if (methodCallExpression == null)            throw new InvalidOperationException("Expression must be a method call.");        if (methodCallExpression.Object != expression.Parameters[0])            throw new InvalidOperationException("Method call must target lambda argument.");        return methodCallExpression;    }    private static Type ensureController<TController>() {        var controllerType = typeof(TController);        bool isController = controllerType != null               && controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)               && !controllerType.IsAbstract               && (                    typeof(IController).IsAssignableFrom(controllerType)                    || typeof(System.Web.Http.Controllers.IHttpController).IsAssignableFrom(controllerType)                  );        if (!isController) {            throw new InvalidOperationException("Action target is an invalid controller.");        }        return controllerType;    }    private static RouteValueDictionary buildParameterValuesFromExpression(MethodCallExpression methodCallExpression) {        RouteValueDictionary result = new RouteValueDictionary();        ParameterInfo[] parameters = methodCallExpression.Method.GetParameters();        if (parameters.Length > 0) {            for (int i = 0; i < parameters.Length; i++) {                object value;                var expressionArgument = methodCallExpression.Arguments[i];                if (expressionArgument.NodeType == ExpressionType.Constant) {                    // If argument is a constant expression, just get the value                    value = (expressionArgument as ConstantExpression).Value;                } else {                    try {                        // Otherwise, convert the argument subexpression to type object,                        // make a lambda out of it, compile it, and invoke it to get the value                        var convertExpression = Expression.Convert(expressionArgument, typeof(object));                        value = Expression.Lambda<Func<object>>(convertExpression).Compile().Invoke();                    } catch {                        // ?????                        value = String.Empty;                    }                }                result.Add(parameters[i].Name, value);            }        }        return result;    }}

The trick was to get the route to the action and use that to generate the URL.

private static string resolvePath<TController>(RouteValueDictionary routeValues, Expression<Action<TController>> expression) where TController : Http.Controllers.IHttpController {    var controllerName = routeValues["controller"] as string;    var actionName = routeValues["action"] as string;    routeValues.Remove("controller");    routeValues.Remove("action");    var method = expression.AsMethodCallExpression().Method;    var configuration = System.Web.Http.GlobalConfiguration.Configuration;    var apiDescription = configuration.Services.GetApiExplorer().ApiDescriptions       .FirstOrDefault(c =>           c.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(TController)           && c.ActionDescriptor.ControllerDescriptor.ControllerType.GetMethod(actionName) == method           && c.ActionDescriptor.ActionName == actionName       );    var route = apiDescription.Route;    var routeData = new HttpRouteData(route, new HttpRouteValueDictionary(routeValues));    var request = new System.Net.Http.HttpRequestMessage();    request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration;    request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;    var virtualPathData = route.GetVirtualPath(request, routeValues);    var path = virtualPathData.VirtualPath;    return path;}

So now if for example I have the following api controller

[RoutePrefix("api/tests")][AllowAnonymous]public class TestsApiController : WebApiControllerBase {    [HttpGet]    [Route("{lat:double:range(-90,90)}/{lng:double:range(-180,180)}")]    public object Get(double lat, double lng) {        return new { lat = lat, lng = lng };    }}

Works for the most part so far when I test it

@section Scripts {    <script type="text/javascript">        var url = '@(Url.HttpRouteUrl<TestsApiController>(c => c.Get(1,2)))';        alert(url);    </script>}

I get /api/tests/1/2, which is what I wanted and what I believe would satisfy your requirements.

Note that it will also default back to the UrlHelper for actions with route attributes that have the Name.


According to this page on CodePlex, all MVC routes have a distinct name, even if it is not specified.

Docs on codeplex is for WebApi 2.0 beta and looks like things have changed since that.

I have debugded attribute routes and it looks like WebApi create single route for all actions without specified RouteName with the name MS_attributerouteWebApi.

You can find it in _routeCollection._namedMap field:

GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap

This collection is also populated with named routes for which route name was specified explicitly via attribute.

When you generate URL with Url.Route("RouteName", null); it searches for route names in _routeCollection field:

VirtualPathData virtualPath1 =    this._routeCollection.GetVirtualPath(requestContext, name, values1);

And it will find only routes specified with route attributes there. Or with config.Routes.MapHttpRoute of course.

I don't want to be forced to specify a unique name for my routes.

Unfortunately, there is no way to generate URL for WebApi action without specifying route name explicitly.

In fact, even providing a Route name in the attribute only seems to work with Url.HttpRouteUrl

Yes, and that is because API routes and MVC routes use different collections to store routes and have different internal implementation.


Very first thing is, if you want to access a route then definitely you need a unique identifier for that just like any other variable we use in normal c# programming.

Hence if defining a unique name for each route is a headache for you, but still I think you will have to with it because the benefit its providing is much better.

Benefit: Think of a scenario where you want to change your route to a new value but it will require you to change that value across the applciation wherever you have used it.In this scenario, it will be helpful.

Following is the code sample to generate link from route name.

public class BooksController : ApiController{    [Route("api/books/{id}", Name="GetBookById")]    public BookDto GetBook(int id)     {        // Implementation not shown...    }    [Route("api/books")]    public HttpResponseMessage Post(Book book)    {        // Validate and add book to database (not shown)        var response = Request.CreateResponse(HttpStatusCode.Created);        // Generate a link to the new book and set the Location header in the response.        string uri = **Url.Link("GetBookById", new { id = book.BookId });**        response.Headers.Location = new Uri(uri);        return response;    }}

Please read this link

And yes you are gonna need to define this routing name in order to access them with the ease you want to access. The convention based link generation you want is currently not available.

One more thing I would like to add here is, if this is really very concerning issue for you then we can write out own helper methods which will take two parameters {ControllerName} and {ActionName} and will return the route value using some logic.

Let us know if you really think that its worthy to do that.