如何在不使用AttributeRouting在Route属性上指定名称的情况下生成WebApi2 URL?

时间:2015-02-06 15:43:28

标签: c# .net asp.net-web-api asp.net-web-api-routing attributerouting

我已将ASP.NET MVC5应用程序配置为使用AttributeRouting for WebApi:

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

我有一个ApiController如下:

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

我想为我的WebApi控制器操作生成一个URL,而不必指定显式的路由名称。

根据this page on CodePlex,所有MVC路由都有一个不同的名称,即使没有指定。

  

如果没有指定的路由名称,Web API将生成一个   默认路由名称。如果只有一个属性路由   特定控制器上的动作名称,路径名称将采用   表单“ControllerName.ActionName”。如果有多个属性   在该控制器上使用相同的操作名称,后缀将添加到   区分路线:“Customer.Get1”,“Customer.Get2”。

On ASP.NET,它没有确切说明默认命名约定是什么,但它确实表明每个路由都有一个名称。

  

在Web API中,每个路由都有一个名称。路由名称非常有用   生成链接,以便您可以在HTTP响应中包含链接。

基于这些资源和answer by StackOverflow user Karhgath,我被认为以下会产生一个指向我的WebApi路线的URL:

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

然而,这会产生错误:

  

在路线中找不到名为“Subjects.Search”的路线   集合。

我根据我在StackOverflow上找到的其他答案尝试了一些其他变体,没有成功。

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

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

事实上,即使在属性中提供路由名称,也只能使用:

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

将“Search.Subjects”指定为Route属性中的路径名称。

我不想被迫为我的路线指定唯一名称。

如何生成WebApi控制器操作的URL而无需在Route属性中明确指定路由名称?

CodePlex的默认路由命名方案是否可能已更改或记录错误?

有没有人对使用AttributeRouting设置的路由检索URL的正确方法有所了解?

3 个答案:

答案 0 :(得分:11)

通过检查Web Api的angular.module('mainApp', ['ui.mask', "ngRoute", "cfp.hotkeys"]) .controller('ndcController', ["$scope", "$location", "$timeout", "fieldService", "entityService", "$filter", "authorizedUserService", "$q", "gerimedService","hotkeys", function ($scope, $location, $timeout, fieldService, entityService, $filter, authorizedUserService, $q, gerimedService,hotkeys) { hotkeys.add({ combo: 'ctrl+up', description: 'This one goes to 11', callback: function () { var test = ""; } }); }]); 以及强类型表达式来解决路径,我能够生成WebApi2 URL而无需在IApiExplorer属性上指定Name使用属性路由。

我已经创建了一个帮助扩展,它允许我在MVC razor中使用Route强类型表达式。这非常适合在视图中解析我的MVC控制器的URI。

UrlHelper

我现在有一个视图,我试图使用knockout将一些数据发布到我的web api,并且需要能够做这样的事情

<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" })) {...}    

这样我就不必硬编码我的网址(魔术字符串)

我目前用于获取Web API网址的扩展方法的实现在以下类中定义。

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

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

RouteValueDictionary

诀窍是获取行动的路线并使用它来生成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;
    }
}

所以现在如果我有以下api控制器

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

我测试时到目前为止大部分工作

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

我得到@section Scripts { <script type="text/javascript"> var url = '@(Url.HttpRouteUrl<TestsApiController>(c => c.Get(1,2)))'; alert(url); </script> } ,这是我想要的,我认为可以满足您的要求。

请注意,对于包含/api/tests/1/2的路由属性的操作,它也会默认返回UrlHelper。

答案 1 :(得分:4)

  

根据CodePlex上的这个页面,所有MVC路由都有一个不同的名称,即使它没有指定。

Codeplex上的文档适用于WebApi 2.0 beta,看起来事情已经发生了变化。

我已经调试了属性路由,看起来WebApi为所有操作创建单一路由,而没有指定名为RouteName的{​​{1}}。

您可以在MS_attributerouteWebApi字段中找到它:

_routeCollection._namedMap

此集合还填充了命名路由,其路由名称是通过属性显式指定的。

使用GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap 生成网址时,会在Url.Route("RouteName", null);字段中搜索路由名称:

_routeCollection

它只会找到路由属性指定的路由。或者当然是VirtualPathData virtualPath1 = this._routeCollection.GetVirtualPath(requestContext, name, values1);

  

我不想被迫为我的路线指定一个唯一的名称。

不幸的是,如果没有明确指定路由名,就无法为WebApi操作生成URL。

  

事实上,即使在属性中提供路由名称,也只能使用config.Routes.MapHttpRoute

是的,这是因为API路由和MVC路由使用不同的集合来存储路由并具有不同的内部实现。

答案 2 :(得分:0)

首先,如果你想访问一条路线,那么你肯定需要一个唯一的标识符,就像我们在普通的c#编程中使用的任何其他变量一样。

因此,如果为每条路线定义一个唯一的名称对您来说是一个令人头疼的问题,但我认为您仍然必须使用它,因为它提供的好处要好得多。

好处:考虑一种情况,您希望将路由更改为新值,但无论您在何处使用它,都需要在应用程序中更改该值。 在这种情况下,它会有所帮助。

以下是从路由名称生成链接的代码示例。

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

请阅读此link

是的,您需要定义此路由名称才能以您想要访问的方式访问它们。您想要的基于约定的链接生成目前不可用。

我想在这里添加的另一件事是,如果这对你来说真的非常重要,那么我们可以编写自己的帮助器方法,它将采用两个参数{ControllerName}和{ActionName},并将返回路由值一些逻辑。

如果你真的认为它值得这样做,请告诉我们。