Search code examples
c#asp.net-mvcasp.net-mvc-4asp.net-web-apiasp.net-web-api-routing

Resolve WebApi action Url from within MVC View using MVC.UrlHelper


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 as follows.

private const string HttpRouteKey = "httproute";
public static string HttpRouteUrl<TController>(this UrlHelper urlHelper, Expression<Action<TController>> action)
   where TController : System.Web.Http.ApiController {
    RouteValueDictionary routeValues = InternalExpressionHelper.GetRouteValues(action);
    if (!routeValues.ContainsKey(HttpRouteKey)) {
        routeValues.Add(HttpRouteKey, true);
    }
    var scheme = urlHelper.RequestContext.HttpContext.Request.Url.Scheme;
    var route = urlHelper.Action(null, null, rvd, scheme);
    //I've also tried
    //var route = urlHelper.HttpRouteUrl(null, rvd);
    return route;
}

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

Problem is that my route always returns null. Previous questions on SO and blogs found have indicated that by including the httproute in the dictionary would allow for the ability to get WebApi Urls from MVC.UrlHelper route collection.

So far none of the accepted answers seem to be working for me as I am using attribute routing without route name.


Solution

  • Seeing as when generating links to Web API routes one must always require a RouteName for this to work, I decided to apply a convention.

    The helper extension with determine the RouteName by combining the controller name and action name.

    var routeName = string.Join("_", controllerName, actionName);
    

    Now because when extraction information from the expression of which controller name and action name are included, they were also being shown in the url. not very pretty.

    /api/tests/1/2?controller=TestsApi&amp;action=Get
    

    so they were removed from the route values.

    routeValues.Remove("controller");
    routeValues.Remove("action");
    

    The update extension method now looks like this

    private const string HttpRouteKey = "httproute";
    public static string HttpRouteUrl<TController>(this UrlHelper urlHelper, Expression<Action<TController>> action)
       where TController : System.Web.Http.Controllers.IHttpController {
        var routeValues = InternalExpressionHelper.GetRouteValues(action);
        if (!routeValues.ContainsKey(HttpRouteKey)) {
            routeValues.Add(HttpRouteKey, true);
        }
    
        var controllerName = routeValues["controller"] as string;
        routeValues.Remove("controller");
        var actionName = routeValues["action"] as string;
        routeValues.Remove("action");
    
        var routeName = string.Join("_", controllerName, actionName);
    
        var url = urlHelper.HttpRouteUrl(routeName, routeValues);
    
        return url;
    }
    

    And now I just need to make sure that if I want my action to be accessible for url/link generation from MVC or Web API, a RouteName must be applied that follows the convention "{controller}_{action}"

    [RoutePrefix("api/tests")]
    [AllowAnonymous]
    public class TestsApiController : WebApiControllerBase {
        [HttpGet]
        [Route("{lat:double:range(-90,90)}/{lng:double:range(-180,180)}", Name = "TestsApi_Get")]
        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.