Comment puis-je générer une URL WebApi2 sans spécifier un nom sur L'attribut Route avec Attribuuterouting?

j'ai configuré mon ASP.NET MVC5 application to use Attribuuterouting for WebApi:

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

j'ai un ApiController comme suit:

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

je voudrais générer une URL à mon action de contrôleur WebApi sans avoir à spécifier un nom de route explicite.

Selon cette page sur CodePlex, toutes les routes MVC ont un nom distinct, même s'il n'est pas spécifié.

En l'absence de précision sur le nom de l'itinéraire, Web L'API va générer un par défaut le nom de l'itinéraire. S'il n'y a qu'une seule route d'attribut pour nom de l'action sur un contrôleur particulier, le nom de la route prendra form " ControllerName.ActionName". S'il y a plusieurs attributs avec le même nom d'action sur ce contrôleur, un suffixe est ajouté à faire la différence entre les itinéraires: "client.Get1", " Client.Get2".

sur ASP.NET, Il ne dit pas exactement quelle est la convention de nommage par défaut, mais il fait indiquer que chaque route a un nom.

Dans l'API Web, chaque route a un nom. les noms de routes sont utiles pour générer des liens, de sorte que vous pouvez inclure un lien dans une réponse HTTP.

basé sur ces ressources, et un réponse par StackOverflow utilisateur Karhgath, j'ai été amené à croire que la suivante serait de produire une URL à mon WebApi route:

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

Toutefois, cela produit une erreur:

une route nommée "Subjects".Recherche " ne peut pas être trouvé dans l'itinéraire collection.

j'ai essayé quelques autres variantes basées sur d'autres réponses que j'ai trouvé sur StackOverflow, aucun avec succès.

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

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

en fait, même fournir un nom de Route dans l'attribut ne semble fonctionner qu'avec:

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

Où "Rechercher.Subjects" est spécifié comme nom de route dans L'attribut Route.

je ne veux pas être obligé de spécifier un unique nom pour mon parcours.

Comment puis-je générer une URL à l'action de mon contrôleur WebApi sans avoir à spécifier explicitement un nom de route dans l'attribut Route?

est-il possible que le schéma de nommage de route par défaut ait changé ou soit mal documenté au CodePlex?

quelqu'un a-t-il une idée de la bonne façon de récupérer une URL pour une route qui a été configurée avec Attribuuterouting?

20
demandé sur abatishchev 2015-02-06 18:43:28

3 réponses

utilisation d'un work around pour trouver la route via l'inspection des Api Web IApiExplorer avec des expressions fortement dactylographiées, j'ai pu générer une URL WebApi2 sans spécifier un Name sur le Route attribut avec routage d'attribut.

j'ai créé une extension helper qui me permet d'avoir fortement tapé des expressions avec UrlHelper in MVC razor. Cela fonctionne très bien pour résoudre URIs pour mes contrôleurs MVC de dans les vues.

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

j'ai maintenant un point de vue où je suis en train d'utiliser knock-out pour poster quelques données sur mon api web et doivent être en mesure de faire quelque chose comme ceci

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

pour que je n'aie pas à coder mes urls (Magic strings)

mon implémentation actuelle de ma méthode d'extension pour obtenir l'url de L'API web est définie dans la classe suivante.

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 inspecte l'expression et génère un RouteValueDictionary qui sera utilisé pour générer l'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;
    }
}

le trick était d'obtenir la route de l'action et d'utiliser cela pour générer L'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;
}

donc maintenant si par exemple j'ai le contrôleur d'api suivant

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

Œuvres, pour la plupart, donc loin quand je l'ai tester

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

je /api/tests/1/2, ce qui est ce que je voulais et ce que je crois satisferait vos exigences.

notez qu'il retournera par défaut au Helper URL pour les actions avec les attributs de route qui ont le Name.

11
répondu Nkosi 2017-06-05 22:48:56

selon cette page sur CodePlex, toutes les routes MVC ont un nom distinct, même s'il n'est pas spécifié.

Docs sur codeplex est pour WebApi 2.0 beta et on dirait que les choses ont changé depuis.

j'ai débogué les routes d'attribut et il semble que WebApi crée une route unique pour toutes les actions sans RouteName le nom MS_attributerouteWebApi.

Vous pouvez le trouver dans _routeCollection._namedMap champ:

GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap

ce collection est aussi peuplée avec des routes nommées pour lesquelles le nom de la route a été spécifié explicitement via l'attribut.

lorsque vous générez une URL avec Url.Route("RouteName", null); recherche de noms de routes dans _routeCollection champ:

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

et il ne trouvera que les routes spécifiées avec les attributs de route. Ou avec config.Routes.MapHttpRoute bien sûr.

je ne veux pas être obligé de spécifier un nom unique pour mes itinéraires.

malheureusement, il n'y a aucun moyen générer L'URL pour L'action WebApi sans spécifier explicitement le nom de la route.

en fait, même fournir un nom de Route dans l'attribut ne semble fonctionner qu'avec Url.HttpRouteUrl

Oui, et c'est parce que les routes API et les routes MVC utilisent des collections différentes pour stocker les routes et ont une implémentation interne différente.

4
répondu Andrii Litvinov 2017-06-01 08:56:47

La première chose est que, si vous voulez accéder à une route, alors vous avez certainement besoin d'un identifiant unique pour cela comme pour toute autre variable que nous utilisons dans la programmation c# normale.

par conséquent, si définir un nom unique pour chaque route est un casse-tête pour vous, mais je pense que vous aurez avec elle parce que le bénéfice qu'elle fournit est beaucoup mieux.

avantage: pensez à un scénario où vous voulez changer votre route à une nouvelle valeur mais il vous faudra changer cette valeur à travers l'application où vous l'avez utilisé. Dans ce scénario, il sera utile.

Voici l'exemple de code pour générer le lien à partir du nom de la route.

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

Merci de lire ce lien

Et oui, vous êtes va avoir besoin de définir ce nom de gamme pour avoir accès à la facilité vous souhaitez accéder. La convention en fonction de la génération de liens que vous souhaitez n'est pas disponible actuellement.

Une chose que je voudrais ajouter ici si c'est vraiment très important pour vous, alors nous pouvons écrire nos propres méthodes d'aide qui prendront deux paramètres {ControllerName} et {Actioname} et retourneront la valeur de la route en utilisant une certaine logique.

faites-nous savoir si vous pensez vraiment que son digne de le faire.

0
répondu Ashutosh Singh 2017-06-08 04:42:37