Roteamento e seleção de ação na API Web do ASP.NET

Este artigo descreve como ASP.NET Web API roteia uma solicitação HTTP para uma ação específica em um controlador.

Observação

Para obter uma visão geral de alto nível do roteamento, consulte Roteamento em ASP.NET Web API.

Este artigo analisa os detalhes do processo de roteamento. Se você criar um projeto de API Web e descobrir que algumas solicitações não são roteada da maneira esperada, esperamos que este artigo ajude.

O roteamento tem três fases main:

  1. Correspondendo o URI a um modelo de rota.
  2. Selecionando um controlador.
  3. Selecionando uma ação.

Você pode substituir algumas partes do processo por seus próprios comportamentos personalizados. Neste artigo, descrevo o comportamento padrão. No final, anotei os locais onde você pode personalizar o comportamento.

Modelos de rota

Um modelo de rota é semelhante a um caminho de URI, mas pode ter valores de espaço reservado, indicados com chaves:

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

Ao criar uma rota, você pode fornecer valores padrão para alguns ou todos os espaços reservados:

defaults: new { category = "all" }

Você também pode fornecer restrições, que restringem como um segmento de URI pode corresponder a um espaço reservado:

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

A estrutura tenta corresponder os segmentos no caminho do URI ao modelo. Literais no modelo devem corresponder exatamente. Um espaço reservado corresponde a qualquer valor, a menos que você especifique restrições. A estrutura não corresponde a outras partes do URI, como o nome do host ou os parâmetros de consulta. A estrutura seleciona a primeira rota na tabela de rotas que corresponde ao URI.

Há dois espaços reservados especiais: "{controller}" e "{action}".

  • "{controller}" fornece o nome do controlador.
  • "{action}" fornece o nome da ação. Na API Web, a convenção usual é omitir "{action}".

Padrões

Se você fornecer padrões, a rota corresponderá a um URI que não tem esses segmentos. Por exemplo:

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

Os URIs http://localhost/api/products/all e http://localhost/api/products correspondem à rota anterior. No último URI, o segmento ausente {category} recebe o valor allpadrão .

Dicionário de Rotas

Se a estrutura encontrar uma correspondência para um URI, ela criará um dicionário que contém o valor de cada espaço reservado. As chaves são os nomes de espaço reservado, sem incluir as chaves. Os valores são obtidos do caminho do URI ou dos padrões. O dicionário é armazenado no objeto IHttpRouteData .

Durante essa fase de correspondência de rotas, os espaços reservados especiais "{controller}" e "{action}" são tratados da mesma forma que os outros espaços reservados. Eles são simplesmente armazenados no dicionário com os outros valores.

Um padrão pode ter o valor especial RouteParameter.Optional. Se um espaço reservado receber esse valor, o valor não será adicionado ao dicionário de rotas. Por exemplo:

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

Para o caminho de URI "api/products", o dicionário de rotas conterá:

  • controlador: "products"
  • categoria: "all"

No entanto, para "api/products/toys/123", o dicionário de rotas conterá:

  • controlador: "products"
  • categoria: "brinquedos"
  • id: "123"

Os padrões também podem incluir um valor que não aparece em nenhum lugar no modelo de rota. Se a rota corresponder, esse valor será armazenado no dicionário. Por exemplo:

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

Se o caminho do URI for "api/root/8", o dicionário conterá dois valores:

  • controlador: "clientes"
  • id: "8"

Selecionando um controlador

A seleção do controlador é tratada pelo método IHttpControllerSelector.SelectController . Esse método usa uma instância HttpRequestMessage e retorna um HttpControllerDescriptor. A implementação padrão é fornecida pela classe DefaultHttpControllerSelector . Essa classe usa um algoritmo simples:

  1. Procure no dicionário de rotas a chave "controller".
  2. Pegue o valor dessa chave e acrescente a cadeia de caracteres "Controller" para obter o nome do tipo de controlador.
  3. Procure um controlador de API Web com esse nome de tipo.

Por exemplo, se o dicionário de rotas contiver o par chave-valor "controller" = "products", o tipo de controlador será "ProductsController". Se não houver nenhum tipo correspondente ou várias correspondências, a estrutura retornará um erro para o cliente.

Para a etapa 3, DefaultHttpControllerSelector usa a interface IHttpControllerTypeResolver para obter a lista de tipos de controlador de API Web. A implementação padrão de IHttpControllerTypeResolver retorna todas as classes públicas que (a) implementam IHttpController, (b) não são abstratas e (c) têm um nome que termina em "Controller".

Seleção de ação

Depois de selecionar o controlador, a estrutura seleciona a ação chamando o método IHttpActionSelector.SelectAction . Esse método usa um HttpControllerContext e retorna um HttpActionDescriptor.

A implementação padrão é fornecida pela classe ApiControllerActionSelector . Para selecionar uma ação, ela examina o seguinte:

  • O método HTTP da solicitação.
  • O espaço reservado "{action}" no modelo de rota, se presente.
  • Os parâmetros das ações no controlador.

Antes de examinar o algoritmo de seleção, precisamos entender algumas coisas sobre ações do controlador.

Quais métodos no controlador são considerados "ações"? Ao selecionar uma ação, a estrutura examina apenas os métodos de instância pública no controlador. Além disso, ele exclui métodos de "nome especial" (construtores, eventos, sobrecargas de operador e assim por diante) e métodos herdados da classe ApiController .

Métodos HTTP. A estrutura escolhe apenas as ações que correspondem ao método HTTP da solicitação, determinadas da seguinte maneira:

  1. Você pode especificar o método HTTP com um atributo: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost ou HttpPut.
  2. Caso contrário, se o nome do método do controlador começar com "Get", "Post", "Put", "Delete", "Head", "Options" ou "Patch", então, por convenção, a ação dá suporte a esse método HTTP.
  3. Se nenhum dos itens acima for, o método oferecerá suporte a POST.

Associações de parâmetro. Uma associação de parâmetro é como a API Web cria um valor para um parâmetro. Esta é a regra padrão para associação de parâmetro:

  • Tipos simples são obtidos do URI.
  • Tipos complexos são obtidos do corpo da solicitação.

Os tipos simples incluem todos os tipos primitivos .NET Framework, além de DateTime, Decimal, Guid, String e TimeSpan. Para cada ação, no máximo um parâmetro pode ler o corpo da solicitação.

Observação

É possível substituir as regras de associação padrão. Confira Associação de parâmetro WebAPI nos bastidores.

Com essa tela de fundo, aqui está o algoritmo de seleção de ação.

  1. Crie uma lista de todas as ações no controlador que correspondam ao método de solicitação HTTP.

  2. Se o dicionário de rotas tiver uma entrada de "ação", remova ações cujo nome não corresponda a esse valor.

  3. Tente corresponder parâmetros de ação ao URI, da seguinte maneira:

    1. Para cada ação, obtenha uma lista dos parâmetros que são um tipo simples, em que a associação obtém o parâmetro do URI. Exclua parâmetros opcionais.
    2. Nessa lista, tente encontrar uma correspondência para cada nome de parâmetro, seja no dicionário de rotas ou na cadeia de caracteres de consulta URI. As correspondências não diferenciam maiúsculas de minúsculas e não dependem da ordem do parâmetro.
    3. Selecione uma ação em que cada parâmetro na lista tenha uma correspondência no URI.
    4. Se mais uma ação atender a esses critérios, escolha aquela com mais correspondências de parâmetro.
  4. Ignorar ações com o atributo [NonAction] .

A etapa 3 é provavelmente a mais confusa. A ideia básica é que um parâmetro possa obter seu valor do URI, do corpo da solicitação ou de uma associação personalizada. Para parâmetros provenientes do URI, queremos garantir que o URI realmente contenha um valor para esse parâmetro, seja no caminho (por meio do dicionário de rotas) ou na cadeia de caracteres de consulta.

Por exemplo, considere a seguinte ação:

public void Get(int id)

O parâmetro id é associado ao URI. Portanto, essa ação só pode corresponder a um URI que contém um valor para "id", seja no dicionário de rotas ou na cadeia de caracteres de consulta.

Parâmetros opcionais são uma exceção, pois são opcionais. Para um parâmetro opcional, não há problema se a associação não conseguir obter o valor do URI.

Tipos complexos são uma exceção por um motivo diferente. Um tipo complexo só pode ser associado ao URI por meio de uma associação personalizada. Mas, nesse caso, a estrutura não pode saber com antecedência se o parâmetro seria associado a um URI específico. Para descobrir, seria necessário invocar a associação. O objetivo do algoritmo de seleção é selecionar uma ação na descrição estática, antes de invocar qualquer associação. Portanto, tipos complexos são excluídos do algoritmo correspondente.

Depois que a ação é selecionada, todas as associações de parâmetro são invocadas.

Resumo:

  • A ação deve corresponder ao método HTTP da solicitação.
  • O nome da ação deve corresponder à entrada de "ação" no dicionário de rotas, se presente.
  • Para cada parâmetro da ação, se o parâmetro for obtido do URI, o nome do parâmetro deverá ser encontrado no dicionário de rotas ou na cadeia de caracteres de consulta URI. (Parâmetros e parâmetros opcionais com tipos complexos são excluídos.)
  • Tente corresponder ao maior número de parâmetros. A melhor correspondência pode ser um método sem parâmetros.

Exemplo estendido

Rotas:

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

Solicitação HTTP:

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

Correspondência de rotas

O URI corresponde à rota chamada "DefaultApi". O dicionário de rotas contém as seguintes entradas:

  • controlador: "products"
  • id: "1"

O dicionário de rotas não contém os parâmetros de cadeia de caracteres de consulta, "versão" e "detalhes", mas eles ainda serão considerados durante a seleção de ação.

Seleção do Controlador

Na entrada "controlador" no dicionário de rotas, o tipo de controlador é ProductsController.

Seleção de ação

A solicitação HTTP é uma solicitação GET. As ações do controlador que dão suporte a GET são GetAll, GetByIde FindProductsByName. O dicionário de rotas não contém uma entrada para "ação", portanto, não precisamos corresponder ao nome da ação.

Em seguida, tentamos corresponder nomes de parâmetro para as ações, examinando apenas as ações GET.

Ação Parâmetros a serem correspondidos
GetAll nenhum
GetById "id"
FindProductsByName "name"

Observe que o parâmetro de versão de GetById não é considerado, pois é um parâmetro opcional.

O GetAll método corresponde trivialmente. O GetById método também corresponde, pois o dicionário de rotas contém "id". O FindProductsByName método não corresponde.

O GetById método vence, porque corresponde a um parâmetro, em vez de nenhum parâmetro para GetAll. O método é invocado com os seguintes valores de parâmetro:

  • id = 1
  • version = 1.5

Observe que, embora a versão não tenha sido usada no algoritmo de seleção, o valor do parâmetro vem da cadeia de caracteres de consulta URI.

Pontos de extensão

A API Web fornece pontos de extensão para algumas partes do processo de roteamento.

Interface Descrição
IHttpControllerSelector Seleciona o controlador.
IHttpControllerTypeResolver Obtém a lista de tipos de controlador. O DefaultHttpControllerSelector escolhe o tipo de controlador nessa lista.
IAssembliesResolver Obtém a lista de assemblies de projeto. A interface IHttpControllerTypeResolver usa essa lista para localizar os tipos de controlador.
IHttpControllerActivator Cria novas instâncias de controlador.
IHttpActionSelector Seleciona a ação.
IHttpActionInvoker Invoca a ação.

Para fornecer sua própria implementação para qualquer uma dessas interfaces, use a coleção Services no objeto HttpConfiguration :

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