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

帅比萌擦擦* 提交于 2019-12-03 16:16:31

问题


I've configured my ASP.NET MVC5 application to use AttributeRouting for WebApi:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
    }
}

I have an ApiController as follows:

[RoutePrefix("api/v1/subjects")]
public class SubjectsController : ApiController
{
    [Route("search")]
    [HttpPost]
    public SearchResultsViewModel Search(SearchCriteriaViewModel criteria)
    {
        //...
    }
}

I would like to generate a URL to my WebApi controller action without having to specify an explicit route name.

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

In the absence of a specified route name, Web API will generate a default route name. If there is only one attribute route for the action name on a particular controller, the route name will take the form "ControllerName.ActionName". If there are multiple attributes with the same action name on that controller, a suffix gets added to differentiate between the routes: "Customer.Get1", "Customer.Get2".

On ASP.NET, it doesn't say exactly what is the default naming convention, but it does indicate that every route has a name.

In Web API, every route has a name. Route names are useful for generating links, so that you can include a link in an HTTP response.

Based on these resources, and an answer by StackOverflow user Karhgath, I was led to believe that the following would produce a URL to my WebApi route:

@(Url.RouteUrl("Subjects.Search"))

However, this produces an error:

A route named 'Subjects.Search' could not be found in the route collection.

I've tried a few other variants based on other answers I found on StackOverflow, none with success.

@(Url.Action("Search", "Subjects", new { httproute = "" }))

@(Url.HttpRouteUrl("Search.Subjects", new {}))

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

@(Url.HttpRouteUrl("Search.Subjects", new {}))

Where "Search.Subjects" is specified as the route name in the Route attribute.

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

How can I generate a URL to my WebApi controller action without having to explicitly specify a route name in the Route attribute?

Is it possible that the default route naming scheme has changed or is documented incorrectly at CodePlex?

Does anyone have some insight on the proper way to retrieve a URL for a route that has been setup with AttributeRouting?


回答1:


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.




回答2:


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.




回答3:


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.



来源:https://stackoverflow.com/questions/28369419/how-can-i-generate-a-webapi2-url-without-specifying-a-name-on-the-route-attribut

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!