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:
- Correspondendo o URI a um modelo de rota.
- Selecionando um controlador.
- 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 all
padrã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:
- Procure no dicionário de rotas a chave "controller".
- Pegue o valor dessa chave e acrescente a cadeia de caracteres "Controller" para obter o nome do tipo de controlador.
- 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:
- Você pode especificar o método HTTP com um atributo: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost ou HttpPut.
- 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.
- 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.
Crie uma lista de todas as ações no controlador que correspondam ao método de solicitação HTTP.
Se o dicionário de rotas tiver uma entrada de "ação", remova ações cujo nome não corresponda a esse valor.
Tente corresponder parâmetros de ação ao URI, da seguinte maneira:
- 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.
- 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.
- Selecione uma ação em que cada parâmetro na lista tenha uma correspondência no URI.
- Se mais uma ação atender a esses critérios, escolha aquela com mais correspondências de parâmetro.
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
, GetById
e 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));