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.
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&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.