Подтвердить что ты не робот

Как я могу создать URL-адрес WebApi2 без указания атрибута Name at the Route с атрибутом Routing?

Я настроил приложение ASP.NET MVC5 для использования AttributeRouting для 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)
    {
        //...
    }
}

Я хотел бы создать URL-адрес моего действия контроллера WebApi без указания явного имени маршрута.

Согласно этой странице в CodePlex, все маршруты MVC имеют четкое имя, даже если оно не указано.

При отсутствии указанного имени маршрута веб-API будет генерировать имя маршрута по умолчанию. Если есть только один маршрут атрибута для имя действия на конкретном контроллере, имя маршрута примет form "ControllerName.ActionName". Если есть несколько атрибутов с тем же именем действия на этом контроллере, суффикс добавляется к различать маршруты: "Customer.Get1", "Customer.Get2".

В ASP.NET он не говорит точно, что такое соглашение об именах по умолчанию, но оно указывает, что каждый маршрут имеет имя.

В веб-API каждый маршрут имеет имя. Имена маршрутов полезны для генерируя ссылки, чтобы вы могли включить ссылку в ответ HTTP.

На основе этих ресурсов и ответа qaru.site/info/263988/... мне показалось, что следующее приведет к URL-адресу моего маршрута WebApi:

@(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.

Я не хочу, чтобы меня указывали уникальное имя для моих маршрутов.

Как я могу создать URL-адрес для моего контроллера контроллера WebApi без явного указания имени маршрута в атрибуте Route?

Возможно ли, что схема именования маршрутов по умолчанию изменилась или неправильно зарегистрирована в CodePlex?

Есть ли у кого-нибудь представление о правильном способе получения URL-адреса для маршрута, который был настроен с помощью AttributeRouting?

4b9b3361

Ответ 1

Используя работу, чтобы найти маршрут через проверку Web Api IApiExplorer вместе с сильно типизированными выражениями, я смог создать URL-адрес WebApi2 без указания Name атрибута Route с маршрутизацией атрибутов.

Я создал вспомогательное расширение, которое позволяет мне иметь строго типизированные выражения с UrlHelper в бритве MVC. Это очень хорошо работает для разрешения URI для моих MVC-контроллеров с помощью в представлениях.

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

Теперь у меня есть представление, где я пытаюсь использовать нокаут для публикации некоторых данных в моем веб-апи и должен иметь возможность делать что-то вроде этого

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

так что мне не нужно жестко кодировать мои URL-адреса (магические строки)

Моя текущая реализация моего метода расширения для получения URL-адреса веб-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);
    }
}

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

Трюк состоял в том, чтобы получить маршрут к действию и использовать его для создания 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;
}

Итак, если, например, у меня есть следующий контроллер 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 };
    }
}

Работает по большей части до сих пор, когда я тестирую его

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

Я получаю /api/tests/1/2, что я и хотел, и, как я полагаю, удовлетворял бы ваши требования.

Обратите внимание, что он также будет по умолчанию возвращаться к UrlHelper для действий с атрибутами маршрута, которые имеют Name.

Ответ 2

В соответствии с этой страницей на CodePlex все маршруты MVC имеют четкое имя, даже если оно не указано.

Документы на codeplex предназначены для бета-версии WebApi 2.0 и выглядят так, как будто все изменилось с тех пор.

У меня есть отлаженные маршруты атрибутов, и похоже, что WebApi создает один маршрут для всех действий без указанного RouteName с именем MS_attributerouteWebApi.

Вы можете найти его в поле _routeCollection._namedMap:

GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap

Эта коллекция также заполнена именованными маршрутами, для которых имя маршрута указано явно через атрибут.

Когда вы создаете URL-адрес с Url.Route("RouteName", null);, он ищет имена маршрутов в поле _routeCollection:

VirtualPathData virtualPath1 =
    this._routeCollection.GetVirtualPath(requestContext, name, values1);

И он найдет только маршруты, указанные с атрибутами маршрута. Или с config.Routes.MapHttpRoute, конечно.

Я не хочу, чтобы меня указывали уникальное имя для моих маршрутов.

К сожалению, нет способа генерировать URL для действия WebApi без явного указания имени маршрута.

Фактически даже предоставление имени маршрута в атрибуте, похоже, работает только с Url.HttpRouteUrl

Да, и это связано с тем, что маршруты API и маршруты MVC используют разные коллекции для хранения маршрутов и имеют разную внутреннюю реализацию.

Ответ 3

Прежде всего, если вы хотите получить доступ к маршруту, то определенно вам нужен уникальный идентификатор для него, как и любая другая переменная, которую мы используем при нормальном программировании С#.

Следовательно, если определение уникального имени для каждого маршрута является головной болью для вас, но все же я думаю, вам придется с ним, потому что преимущество его предоставления намного лучше.

Преимущество: подумайте о сценарии, в котором вы хотите изменить маршрут на новое значение, но вам потребуется изменить это значение в приложении, где бы вы его не использовали. В этом случае это будет полезно.

Ниже приведен пример кода для создания ссылки с именем маршрута.

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

Прочтите эту ссылку

И да, вам нужно будет определить это имя маршрутизации, чтобы получить доступ к ним с легкостью, к которой вы хотите получить доступ. В настоящее время недоступна генерация ссылок на основе соглашения.

Еще одна вещь, которую я хотел бы добавить здесь: если это действительно очень важно для вас, мы можем написать собственные вспомогательные методы, которые будут принимать два параметра {ControllerName} и {ActionName} и возвратят значение маршрута, используя некоторая логика.

Сообщите нам, действительно ли вы думаете, что это достойно этого.