20

Ho configurato la mia applicazione ASP.NET MVC5 da utilizzare per AttributeRouting WebAPI:Come posso generare un URL WebApi2 senza specificare un nome sull'attributo Route con AttributeRouting?

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

ho un ApiController come segue:

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

Vorrei generare un URL al mio controller WebAPI azione senza dover specificare un nome di percorso esplicito.

In base a this page on CodePlex, tutti i percorsi MVC hanno un nome distinto, anche se non è specificato.

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, non dice esattamente che cosa è la convenzione di denominazione di default, ma non indica che ogni percorso ha un nome.

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.

Sulla base di queste risorse, e di un answer by StackOverflow user Karhgath, sono stato portato a credere che il seguente produrrebbe un URL al mio percorso WebAPI:

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

Tuttavia, questo produce un errore:

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

Ho provato alcune altre varianti sulla base di altre risposte che ho trovato su StackOverflow, nessuna con successo.

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

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

In realtà, anche fornendo un nome percorso nell'attributo sembra funzionare solo con:

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

Dove "Search.Subjects" è specificato come il nome della rotta nell'attributo rotta.

Non voglio essere costretto a specificare un nome univoco per i miei percorsi.

Come è possibile generare un URL per l'azione del controller WebApi senza specificare esplicitamente un nome di percorso nell'attributo Route?

È possibile che lo schema di denominazione della route predefinito sia stato modificato o documentato in modo errato in CodePlex?

Qualcuno ha qualche idea sul modo corretto di recuperare un URL per un percorso che è stato configurato con AttributeRouting?

risposta

3

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

Docs su CodePlex è per WebAPI 2.0 beta e sembra che le cose sono cambiate da allora.

Ho percorsi di attributo debugded e sembra che WebApi crei un'unica route per tutte le azioni senza specificare RouteName con il nome MS_attributerouteWebApi.

Lo si può trovare in _routeCollection._namedMap campo:

GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap 

Questa collezione è popolato anche con percorsi di nome per il quale è stato specificato nome della rotta in modo esplicito tramite l'attributo.

Quando si genera URL con Url.Route("RouteName", null); esso cerca per i nomi di percorso nel campo _routeCollection:

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

e troverà solo linee indicate con percorso attributi lì. O con config.Routes.MapHttpRoute ovviamente.

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

Sfortunatamente, non c'è modo di generare URL per l'azione WebApi senza specificare esplicitamente il nome del percorso.

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

Sì, e questo perché rotte API e rotte MVC utilizzare diverse collezioni per memorizzare rotte e hanno differente implementazione interna.

+1

Spero che MS risolva presto questo intero casino. Le API Web e MVC dovrebbero essere state interamente separate o completamente identiche. Ora abbiamo una combinazione orribile con compatibilità interrotta ovunque e sviluppatori universalmente confusi. – MarioDS

+1

@MarioDS, hanno effettivamente fatto in ASP.NET Core. –

+0

Sfortunatamente ASP.NET Core non è un'opzione per noi. – MarioDS

11

Utilizzando un lavoro in giro per trovare il percorso attraverso l'ispezione di Web Api di IApiExplorer insieme fortemente tipizzato espressioni sono stato in grado di generare un URL WebApi2 senza specificare un Name sull'attributo Route con l'attributo di routing.

Ho creato un'estensione di supporto che mi consente di avere espressioni fortemente digitate con UrlHelper nel rasoio MVC. Questo funziona molto bene per risolvere gli URI per i miei controller MVC da in vista.

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

Ora ho una vista in cui sto cercando di utilizzare a eliminazione diretta di inviare alcuni dati nel mio spazio web api e hanno bisogno di essere in grado di fare qualcosa di simile

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

in modo che io don Devo codificare i miei URL (stringhe magiche)

La mia attuale implementazione del mio metodo di estensione per ottenere l'URL dell'API web è definita nella seguente classe.

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 ispeziona l'espressione e genera un RouteValueDictionary che verrà utilizzato per generare 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; 
    } 
} 

Il trucco era ottenere il percorso verso l'azione e utilizzarlo per generare 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; 
} 

Così ora se per esempio io ho il seguente controller 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 }; 
    } 
} 

Lavori per la maggior parte finora quando verifico che

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

ottengo /api/tests/1/2, che è ciò che Volevo e quello che credo avrebbe soddisfatto le tue esigenze.

Si noti che verrà anche ripristinato l'UrlHelper per le azioni con attributi di percorso che hanno lo Name.

0

Per prima cosa, se si desidera accedere a un percorso, è necessario disporre di un identificativo univoco, proprio come qualsiasi altra variabile utilizzata nella normale programmazione C#.

Quindi se definire un nome univoco per ogni percorso è un mal di testa per te, ma comunque penso che dovrai farlo perché il vantaggio che fornisce è molto meglio.

Vantaggio: pensare a uno scenario in cui si desidera modificare il percorso verso un nuovo valore, ma è necessario modificare tale valore attraverso l'appli- cazione ovunque sia stato utilizzato. In questo scenario, sarà utile.

Di seguito è riportato l'esempio di codice per generare il collegamento dal nome dell'itinerario.

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

Si prega di leggere questo link

E sì si sono gonna necessità di definire questo nome di routing, al fine di accedervi con la facilità che si desidera accedere. La generazione di collegamenti basata su convenzione che desideri non è al momento disponibile.

Un'altra cosa che vorrei aggiungere qui è, se questo è davvero un problema molto serio per te, allora possiamo scrivere i propri metodi di supporto che prenderanno due parametri {ControllerName} e {ActionName} e restituiranno il valore del percorso usando un po 'di logica.

Facci sapere se pensi davvero che sia degno di farlo.

+0

L'unico grosso problema con i nomi di route "statici" è che non è possibile fare riferimento ai percorsi dinamicamente. Il mio scenario specifico è che ho un 'IHttpActionResult personalizzato 'e voglio generare un URL per l'azione che è stata chiamata, solo con parametri diversi. Hmm, azione e parametri ... suona come una via giusta? Solo se l'azione ** qualsiasi ** può restituire questo risultato, come possiamo scoprire il percorso? Giusto, non puoi. Il nome della route statica non è il problema, solo che l'API Web non offre nulla per aiutarti a trovarla in base a MarioDS

Problemi correlati