Enrutamiento y selección de una acción en ASP.NET Web API

En este artículo se describe cómo ASP.NET API web enruta una solicitud HTTP a una acción determinada en un controlador.

Nota:

Para obtener información general de alto nivel sobre el enrutamiento, vea Enrutamiento en ASP.NET Web API.

En este artículo se examinan los detalles del proceso de enrutamiento. Si crea un proyecto de API web y encuentra que algunas solicitudes no se enrutan de la manera esperada, esperamos que este artículo le ayude.

El enrutamiento tiene tres fases principales:

  1. Coincidencia del URI con una plantilla de ruta.
  2. Selección de un controlador.
  3. Selección de una acción.

Puede reemplazar algunas partes del proceso por sus propios comportamientos personalizados. En este artículo, describo el comportamiento predeterminado. Al final, anote los lugares donde puede personalizar el comportamiento.

Plantillas de ruta

Una plantilla de ruta tiene un aspecto similar a una ruta de acceso de URI, pero puede tener valores de marcador de posición, indicados con llaves:

"api/{controller}/public/{category}/{id}"

Al crear una ruta, puede proporcionar valores predeterminados para algunos o todos los marcadores de posición:

defaults: new { category = "all" }

También puede proporcionar restricciones, que restringen cómo un segmento de URI puede coincidir con un marcador de posición:

constraints: new { id = @"\d+" }   // Only matches if "id" is one or more digits.

El marco intenta buscar coincidencias con los segmentos de la ruta de acceso del URI a la plantilla. Los literales de la plantilla deben coincidir exactamente. Un marcador de posición coincide con cualquier valor, a menos que especifique restricciones. El marco no coincide con otras partes del URI, como el nombre de host o los parámetros de consulta. El marco selecciona la primera ruta de la tabla de rutas que coincide con el URI.

Hay dos marcadores de posición especiales: "{controlador}" y "{acción}".

  • "{controlador}" proporciona el nombre del controlador.
  • "{acción}" proporciona el nombre de la acción. En la API web, la convención habitual es omitir "{acción}".

Defaults

Si proporciona valores predeterminados, la ruta coincidirá con un URI que falta en esos segmentos. Por ejemplo:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}",
    defaults: new { category = "all" }
);

Los URI http://localhost/api/products/all y http://localhost/api/products coinciden con la ruta anterior. En el último URI, al segmento {category} que falta se le asigna el valor predeterminado all.

Diccionario de rutas

Si el marco encuentra una coincidencia para un URI, crea un diccionario que contiene el valor de cada marcador de posición. Las claves son los nombres de marcador de posición, no incluidas las llaves. Los valores se toman de la ruta de acceso del URI o de los valores predeterminados. El diccionario se almacena en el objeto IHttpRouteData.

Durante esta fase de coincidencia de rutas, los marcadores de posición especiales "{controlador}" y "{acción}" se tratan igual que los otros marcadores de posición. Simplemente se almacenan en el diccionario con los demás valores.

Un valor predeterminado puede tener el valor especial RouteParameter.Optional. Si se asigna este valor a un marcador de posición, el valor no se agrega al diccionario de rutas. Por ejemplo:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}/{id}",
    defaults: new { category = "all", id = RouteParameter.Optional }
);

Para la ruta de acceso de URI "api/products", el diccionario de rutas contendrá:

  • controlador: "productos"
  • categoría: "todas"

Sin embargo, para "api/products/toys/123", el diccionario de rutas contendrá:

  • controlador: "productos"
  • categoría: "juguetes"
  • id: "123"

Los valores predeterminados también pueden incluir un valor que no aparece en ningún lugar de la plantilla de ruta. Si la ruta coincide, ese valor se almacena en el diccionario. Por ejemplo:

routes.MapHttpRoute(
    name: "Root",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "customers", id = RouteParameter.Optional }
);

Si la ruta de acceso del URI es "api/root/8", el diccionario contendrá dos valores:

  • controlador: "customers"
  • id: "8"

Selección de un controlador

La selección del controlador se controla mediante el método IHttpControllerSelector.SelectController. Este método toma una instancia de HttpRequestMessage y devuelve un HttpControllerDescriptor. La implementación predeterminada la proporciona la clase DefaultHttpControllerSelector. Esta clase usa un algoritmo sencillo:

  1. Busque en el diccionario de rutas la clave "controlador".
  2. Tome el valor de esta clave y anexe la cadena "Controlador" para obtener el nombre del tipo de controlador.
  3. Busque un controlador de API web con este nombre de tipo.

Por ejemplo, si el diccionario de rutas contiene el par clave-valor "controller" = "products", el tipo de controlador es "ProductsController". Si no hay ningún tipo coincidente o varias coincidencias, el marco devuelve un error al cliente.

Para el paso 3, DefaultHttpControllerSelector usa la interfaz IHttpControllerTypeResolver para obtener la lista de tipos de controlador de API web. La implementación predeterminada de IHttpControllerTypeResolver devuelve todas las clases públicas que (a) implementan IHttpController, (b) no son abstractas y (c) tienen un nombre que termina en "Controller".

Selección de acciones

Después de seleccionar el controlador, el marco selecciona la acción llamando al método IHttpActionSelector.SelectAction. Este método toma HttpControllerContext y devuelve un HttpActionDescriptor.

La implementación predeterminada la proporciona la clase ApiControllerActionSelector. Para seleccionar una acción, examina lo siguiente:

  • Método HTTP de la solicitud.
  • Marcador de posición "{acción}" de la plantilla de ruta, si está presente.
  • Parámetros de las acciones del controlador.

Antes de examinar el algoritmo de selección, es necesario comprender algunas cosas sobre las acciones del controlador.

¿Qué métodos del controlador se consideran "acciones"? Al seleccionar una acción, el marco solo examina los métodos de instancia pública en el controlador. Además, excluye los métodos "nombre especial" (constructores, eventos, sobrecargas de operador, etc.) y los métodos heredados de la clase ApiController.

Métodos HTTP. El marco de trabajo solo elige acciones que coinciden con el método HTTP de la solicitud, determinado como se indica a continuación:

  1. Puede especificar el método HTTP con un atributo: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, o HttpPut.
  2. De lo contrario, si el nombre del método de controlador comienza por "Get", "Post", "Put", "Delete", "Head", "Options" o "Patch", por convención, la acción admite ese método HTTP.
  3. Si ninguno de los elementos anteriores, el método admite POST.

Enlaces de parámetros. Un enlace de parámetros es cómo web API crea un valor para un parámetro. Esta es la regla predeterminada para el enlace de parámetros:

  • Los tipos simples se toman del URI.
  • Los tipos complejos se toman del cuerpo de la solicitud.

Los tipos simples incluyen todos los tipo primitivos de .NET Framework, además de DateTime, Decimal, Guid, String, y TimeSpan. Para cada acción, como máximo un parámetro puede leer el cuerpo de la solicitud.

Nota:

Es posible invalidar las reglas de enlace predeterminadas. Vea enlace de parámetros de WebAPI en el capó.

Con ese fondo, este es el algoritmo de selección de acciones.

  1. Cree una lista de todas las acciones del controlador que coincidan con el método de solicitud HTTP.

  2. Si el diccionario de rutas tiene una entrada de "acción", quite las acciones cuyo nombre no coincide con este valor.

  3. Intente hacer coincidir los parámetros de acción con el URI, como se indica a continuación:

    1. Para cada acción, obtenga una lista de los parámetros que son un tipo simple, donde el enlace obtiene el parámetro del URI. Excluir parámetros opcionales.
    2. En esta lista, intente buscar una coincidencia para cada nombre de parámetro, ya sea en el diccionario de rutas o en la cadena de consulta de URI. Las coincidencias no distinguen mayúsculas de minúsculas y no dependen del orden de los parámetros.
    3. Seleccione una acción en la que cada parámetro de la lista tenga una coincidencia en el URI.
    4. Si más que una acción cumple estos criterios, elija la que más coincide con los parámetros.
  4. Omita las acciones con el atributo [NonAction].

El paso 3 es probablemente el más confuso. La idea básica es que un parámetro puede obtener su valor desde el URI, desde el cuerpo de la solicitud o desde un enlace personalizado. Para los parámetros que proceden del URI, queremos asegurarnos de que el URI contiene realmente un valor para ese parámetro, ya sea en la ruta de acceso (a través del diccionario de rutas) o en la cadena de consulta.

Considere, por ejemplo, las siguientes acciones:

public void Get(int id)

El parámetro id se enlaza al URI. Por lo tanto, esta acción solo puede coincidir con un URI que contiene un valor para "id", ya sea en el diccionario de rutas o en la cadena de consulta.

Los parámetros opcionales son una excepción, ya que son opcionales. Para un parámetro opcional, es Correcto si el enlace no puede obtener el valor del URI.

Los tipos complejos son una excepción por otra razón. Un tipo complejo solo se puede enlazar al URI a través de un enlace personalizado. Pero en ese caso, el marco no puede saber con antelación si el parámetro se enlazaría a un URI determinado. Para averiguarlo, tendría que invocar el enlace. El objetivo del algoritmo de selección es seleccionar una acción de la descripción estática, antes de invocar los enlaces. Por lo tanto, los tipos complejos se excluyen del algoritmo coincidente.

Una vez seleccionada la acción, se invocan todos los enlaces de parámetros.

Resumen:

  • La acción debe coincidir con el método HTTP de la solicitud.
  • El nombre de la acción debe coincidir con la entrada "acción" del diccionario de rutas, si está presente.
  • Para cada parámetro de la acción, si el parámetro se toma del URI, el nombre del parámetro debe encontrarse en el diccionario de rutas o en la cadena de consulta URI. (Se excluyen los parámetros y parámetros opcionales con tipos complejos).
  • Intente coincidir con el mayor número de parámetros. La mejor coincidencia puede ser un método sin parámetros.

Ejemplo extendido

Rutas:

routes.MapHttpRoute(
    name: "ApiRoot",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "products", id = RouteParameter.Optional }
);
routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Controlador:

public class ProductsController : ApiController
{
    public IEnumerable<Product> GetAll() {}
    public Product GetById(int id, double version = 1.0) {}
    [HttpGet]
    public void FindProductsByName(string name) {}
    public void Post(Product value) {}
    public void Put(int id, Product value) {}
}

Solicitud HTTP:

GET http://localhost:34701/api/products/1?version=1.5&details=1

Coincidencia de rutas

El URI coincide con la ruta denominada "DefaultApi". El diccionario de rutas contiene las siguientes entradas:

  • controlador: "productos"
  • id: "1"

El diccionario de rutas no contiene los parámetros de cadena de consulta, "version" y "details", pero estos se seguirán considerando durante la selección de acciones.

Selección del controlador

En la entrada "controller" del diccionario de rutas, el tipo de controlador es ProductsController.

Selección de acciones

La solicitud HTTP es una solicitud GET. Las acciones del controlador que admiten GET se GetAll, GetById, y FindProductsByName. El diccionario de rutas no contiene una entrada para "action", por lo que no es necesario que coincida con el nombre de la acción.

A continuación, intentamos buscar coincidencias con los nombres de parámetro de las acciones, examinando solo las acciones GET.

Action Parámetros para buscar coincidencias
GetAll None
GetById "id"
FindProductsByName "name"

Observe que no se tiene en cuenta el parámetro de versión de GetById porque es un parámetro opcional.

El método GetAll coincide de manera trivial. El método GetById también coincide, porque el diccionario de rutas contiene "id". El método FindProductsByName no coincide.

El método GetById gana, porque coincide con un parámetro, frente a ningún parámetro de GetAll. El método se invoca con los siguientes valores de parámetro:

  • id. = 1
  • versión = 1.5

Observe que aunque versión no se usó en el algoritmo de selección, el valor del parámetro procede de la cadena de consulta URI.

Puntos de extensión

La API web proporciona puntos de extensión para algunas partes del proceso de enrutamiento.

Interfaz Descripción
IHttpControllerSelector Selecciona el controlador.
IHttpControllerTypeResolver Obtiene la lista de tipos de controlador. El DefaultHttpControllerSelector elige el tipo de controlador de esta lista.
IAssembliesResolver Obtiene la lista de ensamblados de proyecto. La interfaz IHttpControllerTypeResolverusa esta lista para buscar los tipos de controlador.
IHttpControllerActivator Crea nuevas instancias de controlador.
IHttpActionSelector Selecciona la acción.
IHttpActionInvoker Invoca la acción.

Para proporcionar su propia implementación para cualquiera de estas interfaces, use la colección Services en el objeto HttpConfiguration:

var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));