Enrutamiento de atributos en ASP.NET Web API 2

Enrutamiento es cómo la API web coincide con un URI con una acción. Web API 2 admite un nuevo tipo de enrutamiento, denominado enrutamiento de atributos. Como indica el nombre, el enrutamiento de atributos usa atributos para definir rutas. El enrutamiento de atributos proporciona más control sobre los URI de la API web. Por ejemplo, puede crear fácilmente URI que describen jerarquías de recursos.

El estilo anterior de enrutamiento, denominado enrutamiento basado en convenciones, sigue siendo totalmente compatible. De hecho, puede combinar ambas técnicas en el mismo proyecto.

En este tema se muestra cómo habilitar el enrutamiento de atributos y se describen las distintas opciones para el enrutamiento de atributos. Para ver un tutorial completo que usa el enrutamiento de atributos, vea Creación de una API REST con enrutamiento de atributos en web API 2.

Requisitos previos

Visual Studio 2017 edición Community, Professional o Enterprise

Como alternativa, use el Administrador de paquetes NuGet para instalar los paquetes necesarios. En Visual Studio, en el menú Herramientas, seleccione Administrador de paquetes NuGet, después seleccione Consola del administrador de paquetes. Escriba el siguiente comando en la ventana Consola del Administrador de paquetes:

Install-Package Microsoft.AspNet.WebApi.WebHost

¿Por qué el enrutamiento de atributos?

La primera versión de la API web usada enrutamiento de basado en convención. En ese tipo de enrutamiento, se definen una o varias plantillas de ruta, que son básicamente cadenas parametrizadas. Cuando el marco recibe una solicitud, coincide con el URI con la plantilla de ruta. Para obtener más información sobre el enrutamiento basado en convenciones, vea Enrutamiento en ASP.NET Web API.

Una ventaja del enrutamiento basado en convención es que las plantillas se definen en un solo lugar y las reglas de enrutamiento se aplican de forma coherente en todos los controladores. Desafortunadamente, el enrutamiento basado en convenciones dificulta la compatibilidad con determinados patrones de URI que son comunes en las API de RESTful. Por ejemplo, los recursos suelen contener recursos secundarios: los clientes tienen pedidos, películas tienen actores, libros tienen autores, etc. Es natural crear URI que reflejen estas relaciones:

/customers/1/orders

Este tipo de URI es difícil de crear mediante el enrutamiento basado en convención. Aunque se puede hacer, los resultados no se escalan bien si tiene muchos controladores o tipos de recursos.

Con el enrutamiento de atributos, es trivial definir una ruta para este URI. Simplemente agregue un atributo a la acción del controlador:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

Estos son algunos otros patrones que facilita el enrutamiento de atributos.

Control de versiones de la API

En este ejemplo, "/api/v1/products" se enrutaría a un controlador diferente de "/api/v2/products".

/api/v1/products /api/v2/products

Segmentos de URI sobrecargados

En este ejemplo, "1" es un número de pedido, pero "pendiente" se asigna a una colección.

/orders/1 /orders/pending

Varios tipos de parámetros

En este ejemplo, "1" es un número de pedido, pero "2013/06/16" especifica una fecha.

/orders/1 /orders/2013/06/16

Habilitación del enrutamiento de atributos

Para habilitar el enrutamiento de atributos, llame a MapHttpAttributeRouteRoutes durante la configuración. Este método de extensión se define en la clase System.Web.Http.HttpConfigurationExtensions.

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

El enrutamiento de atributos se puede combinar con enrutamiento basado en convención. Para definir rutas basadas en convenciones, llame al método MapHttpRoute.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

        // Convention-based routing.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Para obtener más información sobre cómo configurar la API web, vea Configuración de ASP.NET API web 2.

Nota: Migración desde Web API 1

Antes de Web API 2, las plantillas de proyecto de API web generaron código similar al siguiente:

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

Si el enrutamiento de atributos está habilitado, este código producirá una excepción. Si actualiza un proyecto de API web existente para usar el enrutamiento de atributos, asegúrese de actualizar este código de configuración a lo siguiente:

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Nota:

Para obtener más información, vea Configuración de la API web con ASP.NET hospedaje.

Agregar atributos de ruta

Este es un ejemplo de una ruta definida mediante un atributo:

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

La cadena "customers/{customerId}/orders" es la plantilla de URI de la ruta. La API web intenta hacer coincidir el URI de solicitud con la plantilla. En este ejemplo, "customers" y "orders" son segmentos literales y "{customerId}" es un parámetro variable. Los siguientes URI coincidirían con esta plantilla:

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

Puede restringir la coincidencia mediante restricciones, que se describen más adelante en este tema.

Observe que el parámetro "{customerId}" de la plantilla de ruta coincide con el nombre del parámetro customerId en el método. Cuando la API web invoca la acción del controlador, intenta enlazar los parámetros de ruta. Por ejemplo, si el URI es http://example.com/customers/1/orders, la API web intenta enlazar el valor "1" al parámetro customerId de la acción.

Una plantilla de URI puede tener varios parámetros:

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

Los métodos de controlador que no tienen un atributo de ruta usan enrutamiento basado en convención. De este modo, puede combinar ambos tipos de enrutamiento en el mismo proyecto.

HTTP Methods

La API web también selecciona acciones basadas en el método HTTP de la solicitud (GET, POST, etc.). De forma predeterminada, la API web busca una coincidencia que no distingue mayúsculas de minúsculas con el inicio del nombre del método de controlador. Por ejemplo, un método de controlador denominado PutCustomers coincide con una solicitud HTTP PUT.

Puede invalidar esta convención mediante la decoración del método con cualquiera de los atributos siguientes:

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

En el ejemplo siguiente, la API web asigna el método CreateBook a las solicitudes HTTP POST.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

Para todos los demás métodos HTTP, incluidos los métodos no estándar, use el atributo AcceptVerbs, que toma una lista de métodos HTTP.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

Prefijos de ruta

A menudo, las rutas de un controlador comienzan con el mismo prefijo. Por ejemplo:

public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }

    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }

    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}

Puede establecer un prefijo común para un controlador completo mediante el atributo [RoutePrefix]:

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }

    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }

    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}

Use una tilde (~) en el atributo de método para invalidar el prefijo de ruta:

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

El prefijo de ruta puede incluir parámetros:

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

Restricciones de ruta

Las restricciones de ruta permiten restringir cómo coinciden los parámetros de la plantilla de ruta. La sintaxis general es "{parameter:constraint}". Por ejemplo:

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

Aquí, la primera ruta solo se seleccionará si el segmento "id" del URI es un entero. De lo contrario, se elegirá la segunda ruta.

En la tabla siguiente se enumeran las restricciones admitidas.

Restricción Descripción Ejemplo
alpha Coincide con caracteres alfabéticos latinos en mayúsculas o minúsculas (a-z, A-Z) {x:alpha}
bool Coincide con un valor booleano. {x:bool}
datetime Coincide con un valor DateTime. {x:datetime}
decimal Coincide con un valor decimal. {x:decimal}
doble Coincide con un valor de punto flotante de 64 bits. {x:double}
flotante Coincide con un valor de punto flotante de 32 bits. {x:float}
guid Coincide con un valor GUID. {x:guid}
int Coincide con un valor entero de 32 bits. {x:int}
length Coincide con una cadena con la longitud especificada o dentro de un intervalo de longitudes especificado. {x:length(6)} {x:length(1,20)}
long Coincide con un valor entero de 64 bits. {x:long}
max Coincide con un entero con un valor máximo. {x:max(10)}
maxlength Coincide con una cadena con una longitud máxima. {x:maxlength(10)}
min Coincide con un entero con un valor mínimo. {x:min(10)}
minlength Coincide con una cadena con una longitud mínima. {x:minlength(10)}
range Coincide con un entero dentro de un intervalo de valores. {x:range(10,50)}
regex Coincide con una expresión regular. {x:regex(^\d{3}-\d{3}-\d{4}$)}

Observe que algunas de las restricciones, como "min", toman argumentos entre paréntesis. Puede aplicar varias restricciones a un parámetro, separados por dos puntos.

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

Restricciones de ruta personalizadas

Puede crear restricciones de ruta personalizadas mediante la implementación de la interfaz IHttpRouteConstraint. Por ejemplo, la restricción siguiente restringe un parámetro a un valor entero distinto de cero.

public class NonZeroConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            long longValue;
            if (value is long)
            {
                longValue = (long)value;
                return longValue != 0;
            }

            string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (Int64.TryParse(valueString, NumberStyles.Integer, 
                CultureInfo.InvariantCulture, out longValue))
            {
                return longValue != 0;
            }
        }
        return false;
    }
}

El código siguiente muestra cómo registrar la restricción:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

Ahora puede aplicar la restricción en las rutas:

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

También puede reemplazar toda la clase DefaultInlineConstraintResolver implementando la interfaz IInlineConstraintResolver. Al hacerlo, se reemplazarán todas las restricciones integradas, a menos que la implementación de IInlineConstraintResolver las agregue específicamente.

Parámetros de URI opcionales y valores predeterminados

Puede hacer que un parámetro de URI sea opcional agregando un signo de interrogación al parámetro de ruta. Si un parámetro de ruta es opcional, debe definir un valor predeterminado para el parámetro de método.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

En este ejemplo, /api/books/locale/1033 y /api/books/locale devuelven el mismo recurso.

Como alternativa, puede especificar un valor predeterminado dentro de la plantilla de ruta, como se indica a continuación:

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

Esto es casi lo mismo que el ejemplo anterior, pero hay una ligera diferencia de comportamiento cuando se aplica el valor predeterminado.

  • En el primer ejemplo ("{lcid:int?}"), el valor predeterminado de 1033 se asigna directamente al parámetro de método, por lo que el parámetro tendrá este valor exacto.
  • En el segundo ejemplo ("{lcid:int=1033}"), el valor predeterminado de "1033" pasa por el proceso de enlace de modelos. El enlazador de modelos predeterminado convertirá "1033" al valor numérico 1033. Sin embargo, podría conectar un enlazador de modelos personalizado, que podría hacer algo diferente.

(En la mayoría de los casos, a menos que tenga enlazadores de modelos personalizados en la canalización, los dos formularios serán equivalentes.)

Nombres de ruta

En la API web, cada ruta tiene un nombre. Los nombres de ruta son útiles para generar vínculos, de modo que pueda incluir un vínculo en una respuesta HTTP.

Para especificar el nombre de ruta, establezca la propiedad Name en el atributo. En el ejemplo siguiente se muestra cómo establecer el nombre de ruta y cómo usar el nombre de ruta al generar un vínculo.

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

Orden de ruta

Cuando el marco intenta hacer coincidir un URI con una ruta, evalúa las rutas en un orden determinado. Para especificar el orden, establezca la propiedad Order en el atributo route. Los valores inferiores se evalúan primero. El valor de orden predeterminado es cero.

Aquí se muestra cómo se determina el orden total:

  1. Compare la propiedad Order del atributo route.

  2. Examine cada segmento de URI en la plantilla de ruta. Para cada segmento, ordene como se indica a continuación:

    1. Segmentos literales.
    2. Parámetros de ruta con restricciones.
    3. Parámetros de ruta sin restricciones.
    4. Segmentos de parámetros comodín con restricciones.
    5. Segmentos de parámetros comodín sin restricciones.
  3. En el caso de un empate, las rutas se ordenan mediante una comparación de cadenas ordinales sin distinción entre mayúsculas y minúsculas (OrdinalIgnoreCase) de la plantilla de ruta.

A continuación se muestra un ejemplo: Supongamos que define el controlador siguiente:

[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }

    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }

    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }

    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }

    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}

Estas rutas se ordenan de la siguiente manera.

  1. pedidos y detalles
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending

Observe que "details" es un segmento literal y aparece antes de "{id}", pero "pendiente" aparece en último lugar porque la propiedad Order es 1. (En este ejemplo se supone que no hay clientes denominados "detalles" o "pendientes". En general, intente evitar rutas ambiguas. En este ejemplo, una plantilla de ruta mejor para GetByCustomer es "customers/{customerName}" )