c# - query - ¿Cómo puedo generar una URL de WebApi2 sin especificar un Nombre en el atributo Ruta con AttributeRouting?
web api rest c# (3)
De acuerdo con esta página en CodePlex, todas las rutas MVC tienen un nombre distinto, incluso si no está especificado.
Los documentos en codeplex son para WebApi 2.0 beta y parece que las cosas han cambiado desde entonces.
He depurado las rutas de atributos y parece que WebApi crea una ruta única para todas las acciones sin especificar RouteName
con el nombre MS_attributerouteWebApi
.
Lo puedes encontrar en el campo _routeCollection._namedMap
:
GlobalConfiguration.Configuration.Routes)._routeCollection._namedMap
Esta colección también se llena con rutas con nombre para las cuales el nombre de la ruta se especificó explícitamente a través del atributo.
Cuando genera una URL con Url.Route("RouteName", null);
busca nombres de ruta en el campo _routeCollection
:
VirtualPathData virtualPath1 =
this._routeCollection.GetVirtualPath(requestContext, name, values1);
Y encontrará solo rutas especificadas con atributos de ruta allí. O con config.Routes.MapHttpRoute
por supuesto.
No quiero que me obliguen a especificar un nombre único para mis rutas.
Desafortunadamente, no hay manera de generar una URL para la acción de WebApi sin especificar explícitamente el nombre de la ruta.
De hecho, incluso proporcionar un nombre de ruta en el atributo solo parece funcionar con
Url.HttpRouteUrl
Sí, y eso se debe a que las rutas API y las rutas MVC usan colecciones diferentes para almacenar rutas y tienen una implementación interna diferente.
He configurado mi aplicación ASP.NET MVC5 para usar AttributeRouting para WebApi:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
Tengo un ApiController
siguiente manera:
[RoutePrefix("api/v1/subjects")]
public class SubjectsController : ApiController
{
[Route("search")]
[HttpPost]
public SearchResultsViewModel Search(SearchCriteriaViewModel criteria)
{
//...
}
}
Me gustaría generar una URL para la acción de mi controlador WebApi sin tener que especificar un nombre de ruta explícito.
De acuerdo con esta página en CodePlex , todas las rutas MVC tienen un nombre distinto, incluso si no está especificado.
En ausencia de un nombre de ruta específico, la API web generará un nombre de ruta predeterminado. Si solo hay una ruta de atributo para el nombre de la acción en un controlador particular, el nombre de la ruta tomará la forma "ControllerName.ActionName". Si hay varios atributos con el mismo nombre de acción en ese controlador, se agrega un sufijo para diferenciar entre las rutas: "Customer.Get1", "Customer.Get2".
En ASP.NET , no dice exactamente cuál es la convención de nomenclatura predeterminada, pero sí indica que cada ruta tiene un nombre.
En la API web, cada ruta tiene un nombre. Los nombres de ruta son útiles para generar enlaces, por lo que puede incluir un enlace en una respuesta HTTP.
Basándome en estos recursos y en una respuesta del usuario de StackOverflow , Karhgath , se me hizo creer que lo siguiente produciría una URL a mi ruta de WebApi:
@(Url.RouteUrl("Subjects.Search"))
Sin embargo, esto produce un error:
No se pudo encontrar una ruta llamada ''Subjects.Search'' en la colección de rutas.
He probado algunas otras variantes basadas en otras respuestas que encontré en StackOverflow, ninguna con éxito.
@(Url.Action("Search", "Subjects", new { httproute = "" }))
@(Url.HttpRouteUrl("Search.Subjects", new {}))
De hecho, incluso proporcionar un nombre de ruta en el atributo solo parece funcionar con:
@(Url.HttpRouteUrl("Search.Subjects", new {}))
Donde "Search.Subjects" se especifica como el nombre de la ruta en el atributo Route.
No quiero que me obliguen a especificar un nombre único para mis rutas.
¿Cómo puedo generar una URL para la acción de mi controlador WebApi sin tener que especificar explícitamente un nombre de ruta en el atributo Ruta?
¿Es posible que el esquema de nombres de ruta predeterminado haya cambiado o esté documentado incorrectamente en CodePlex?
¿Alguien tiene alguna idea sobre la forma correcta de recuperar una URL para una ruta que se ha configurado con AttributeRouting?
Lo primero es que, si desea acceder a una ruta, definitivamente necesita un identificador único para eso, como cualquier otra variable que usamos en la programación normal de c #.
Por lo tanto, si definir un nombre único para cada ruta es un dolor de cabeza para usted, pero aún así creo que tendrá que hacerlo porque el beneficio que brinda es mucho mejor.
Beneficio: piense en un escenario en el que desee cambiar su ruta a un nuevo valor, pero tendrá que cambiar ese valor en toda la aplicación donde sea que la haya utilizado. En este escenario, será útil.
A continuación se muestra el ejemplo de código para generar un enlace desde el nombre de la ruta.
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;
}
}
Por favor lea este link
Y sí, tendrá que definir este nombre de ruta para poder acceder a ellos con la facilidad a la que desea acceder. La generación de enlaces basada en convenciones que desea actualmente no está disponible.
Una cosa más que me gustaría agregar aquí es que si este es un problema realmente importante para usted, entonces podemos escribir nuestros propios métodos de ayuda, que tomarán dos parámetros {ControllerName} y {ActionName} y devolveremos el valor de la ruta con cierta lógica.
Háganos saber si realmente cree que es digno de hacer eso.
Usando una IApiExplorer
para encontrar la ruta a través de la inspección del IApiExplorer
de Web Api junto con expresiones fuertemente tipadas, pude generar una URL de WebApi2 sin especificar un Name
en el atributo de Route
con enrutamiento de atributos.
He creado una extensión auxiliar que me permite tener expresiones fuertemente escritas con UrlHelper
en MVC razor. Esto funciona muy bien para resolver URIs para mis controladores MVC desde con vistas.
<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" })) {...}
Ahora tengo una vista en la que estoy tratando de usar knockout para publicar algunos datos en mi API web y necesito poder hacer algo como esto
var targetUrl = ''@(Url.HttpRouteUrl<TestsApiController>(c => c.TestAction(null)))'';
para que no tenga que codificar mis urls (cadenas mágicas)
Mi implementación actual de mi método de extensión para obtener la URL de la API web se define en la siguiente clase.
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
inspecciona la expresión y genera un RouteValueDictionary
que se usará para generar la 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;
}
}
El truco era obtener la ruta a la acción y usarla para generar la 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;
}
Así que ahora si por ejemplo tengo el siguiente controlador 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 };
}
}
Funciona en su mayor parte hasta ahora cuando lo pruebo
@section Scripts {
<script type="text/javascript">
var url = ''@(Url.HttpRouteUrl<TestsApiController>(c => c.Get(1,2)))'';
alert(url);
</script>
}
Obtengo /api/tests/1/2
, que es lo que quería y lo que creo que satisfaría sus requisitos.
Tenga en cuenta que también volverá de forma predeterminada a UrlHelper para acciones con atributos de ruta que tengan el Name
.