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

cru*_*ush 21 c# asp.net asp.net-web-api attributerouting asp.net-web-api-routing

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

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
    }
}
Run Code Online (Sandbox Code Playgroud)

我有ApiController如下:

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

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

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

如果没有指定的路由名称,Web API将生成默认路由名称.如果特定控制器上的操作名称只有一个属性路由,则路径名称将采用"ControllerName.ActionName"形式.如果该控制器上有多个具有相同操作名称的属性,则会添加后缀以区分路径:"Customer.Get1","Customer.Get2".

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

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

基于这些资源,以及StackOverflow用户Karhgath回答,我被认为以下会产生一个指向我的WebApi路由的URL:

@(Url.RouteUrl("Subjects.Search"))
Run Code Online (Sandbox Code Playgroud)

但是,这会产生错误:

在路径集合中找不到名为"Subjects.Search"的路径.

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

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

@(Url.HttpRouteUrl("Search.Subjects", new {}))
Run Code Online (Sandbox Code Playgroud)

实际上,即使在属性中提供Route名称,也只能使用:

@(Url.HttpRouteUrl("Search.Subjects", new {}))
Run Code Online (Sandbox Code Playgroud)

其中"Search.Subjects"被指定为Route属性中的路径名称.

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

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

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

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

Nko*_*osi 12

通过检查Web Api IApiExplorer以及强类型表达式来查找路径,我能够生成WebApi2 URL,而无需NameRoute属性路由上指定属性.

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

<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" })) {...}    
Run Code Online (Sandbox Code Playgroud)

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

var targetUrl = '@(Url.HttpRouteUrl<TestsApiController>(c => c.TestAction(null)))';
Run Code Online (Sandbox Code Playgroud)

所以我不必硬编码我的网址(魔术字符串)

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

诀窍是获取行动的路线并使用它来生成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;
}
Run Code Online (Sandbox Code Playgroud)

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

[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 };
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我知道/api/tests/1/2,这就是我想要的,我相信会满足你的要求.

请注意,对于具有路径属性的操作,它也会默认返回到UrlHelper Name.


And*_*nov 5

根据CodePlex上的此页面,即使未指定,所有MVC路由也具有唯一的名称。

Codeplex上的文档适用于WebApi 2.0 beta,此后情况似乎有所变化。

我已经调试了属性路由,看起来WebApi可以为所有操作创建单个路由,而无需RouteName使用name 指定MS_attributerouteWebApi

您可以在_routeCollection._namedMap字段中找到它:

GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap
Run Code Online (Sandbox Code Playgroud)

该集合中也填充了已命名的路由,这些路由的路由名称是通过属性明确指定的。

生成URL时,Url.Route("RouteName", null);它将在_routeCollection字段中搜索路由名称:

VirtualPathData virtualPath1 =
    this._routeCollection.GetVirtualPath(requestContext, name, values1);
Run Code Online (Sandbox Code Playgroud)

并且它将仅在其中找到使用路由属性指定的路由。或config.Routes.MapHttpRoute当然。

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

不幸的是,如果不显式指定路由名称,就无法为WebApi操作生成URL。

实际上,即使在属性中提供Route名称似乎也只能用于 Url.HttpRouteUrl

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