Roteamento de atributo no ASP.NET Web API 2
Roteamento é como a API Web corresponde um URI a uma ação. A API Web 2 dá suporte a um novo tipo de roteamento, chamado roteamento de atributo. Como o nome indica, o roteamento de atributo usa atributos para definir rotas. O roteamento de atributo oferece mais controle sobre os URIs em sua API Web. Por exemplo, você pode facilmente criar URIs que descrevem hierarquias de recursos.
O estilo anterior de roteamento, chamado roteamento baseado em convenções, ainda é totalmente compatível. Na verdade, você pode combinar ambas as técnicas no mesmo projeto.
Este tópico mostra como habilitar o roteamento de atributos e descreve as várias opções de roteamento de atributo. Para obter um tutorial de ponta a ponta que usa o roteamento de atributos, consulte Criar uma API REST com roteamento de atributo na API Web 2.
Pré-requisitos
Visual Studio 2017 Community, Professional ou Enterprise Edition
Como alternativa, use o Gerenciador de Pacotes NuGet para instalar os pacotes necessários. No menu Ferramentas no Visual Studio, selecione Gerenciador de Pacotes NuGet e, em seguida, Console do Gerenciador de Pacotes. Insira o seguinte comando na janela Console do Gerenciador de Pacotes:
Install-Package Microsoft.AspNet.WebApi.WebHost
Por que roteamento de atributo?
A primeira versão da API Web usou o roteamento baseado em convenções . Nesse tipo de roteamento, você define um ou mais modelos de rota, que são basicamente cadeias de caracteres parametrizadas. Quando a estrutura recebe uma solicitação, ela corresponde ao URI em relação ao modelo de rota. Para obter mais informações sobre o roteamento baseado em convenção, consulte Roteamento em ASP.NET Web API.
Uma vantagem do roteamento baseado em convenções é que os modelos são definidos em um único local e as regras de roteamento são aplicadas consistentemente em todos os controladores. Infelizmente, o roteamento baseado em convenções dificulta o suporte a determinados padrões de URI comuns em APIs RESTful. Por exemplo, os recursos geralmente contêm recursos filho: os clientes têm pedidos, filmes têm atores, livros têm autores e assim por diante. É natural criar URIs que reflitam essas relações:
/customers/1/orders
Esse tipo de URI é difícil de criar usando o roteamento baseado em convenção. Embora isso possa ser feito, os resultados não serão bem dimensionados se você tiver muitos controladores ou tipos de recursos.
Com o roteamento de atributo, é trivial definir uma rota para esse URI. Basta adicionar um atributo à ação do controlador:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
Aqui estão alguns outros padrões que o roteamento de atributo facilita.
Controle de versão da API
Neste exemplo, "/api/v1/products" seria roteado para um controlador diferente de "/api/v2/products".
/api/v1/products
/api/v2/products
Segmentos de URI sobrecarregados
Neste exemplo, "1" é um número de pedido, mas "pendente" é mapeado para uma coleção.
/orders/1
/orders/pending
Vários tipos de parâmetro
Neste exemplo, "1" é um número de pedido, mas "2013/06/16" especifica uma data.
/orders/1
/orders/2013/06/16
Habilitando o roteamento de atributo
Para habilitar o roteamento de atributo, chame MapHttpAttributeRoutes durante a configuração. Esse método de extensão é definido na classe 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.
}
}
}
O roteamento de atributo pode ser combinado com o roteamento baseado em convenções . Para definir rotas baseadas em convenção, chame o 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 obter mais informações sobre como configurar a API Web, consulte Configurando ASP.NET Web API 2.
Observação: Migrando da API Web 1
Antes da API Web 2, os modelos de projeto da API Web geravam um código como este:
protected void Application_Start()
{
// WARNING - Not compatible with attribute routing.
WebApiConfig.Register(GlobalConfiguration.Configuration);
}
Se o roteamento de atributo estiver habilitado, esse código gerará uma exceção. Se você atualizar um projeto de API Web existente para usar o roteamento de atributo, atualize esse código de configuração para o seguinte:
protected void Application_Start()
{
// Pass a delegate to the Configure method.
GlobalConfiguration.Configure(WebApiConfig.Register);
}
Observação
Para obter mais informações, consulte Configurando a API Web com ASP.NET Hospedagem.
Adicionando atributos de rota
Aqui está um exemplo de uma rota definida usando um atributo:
public class OrdersController : ApiController
{
[Route("customers/{customerId}/orders")]
[HttpGet]
public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}
A cadeia de caracteres "customers/{customerId}/orders" é o modelo de URI para a rota. A API Web tenta corresponder o URI da solicitação ao modelo. Neste exemplo, "clientes" e "pedidos" são segmentos literais e "{customerId}" é um parâmetro variável. Os seguintes URIs corresponderiam a este modelo:
http://localhost/customers/1/orders
http://localhost/customers/bob/orders
http://localhost/customers/1234-5678/orders
Você pode restringir a correspondência usando restrições, descritas posteriormente neste tópico.
Observe que o parâmetro "{customerId}" no modelo de rota corresponde ao nome do parâmetro customerId no método . Quando a API Web invoca a ação do controlador, ela tenta associar os parâmetros de rota. Por exemplo, se o URI for http://example.com/customers/1/orders
, a API Web tentará associar o valor "1" ao parâmetro customerId na ação.
Um modelo de URI pode ter vários parâmetros:
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
Todos os métodos de controlador que não têm um atributo de rota usam roteamento baseado em convenção. Dessa forma, você pode combinar os dois tipos de roteamento no mesmo projeto.
Métodos HTTP
A API Web também seleciona ações com base no método HTTP da solicitação (GET, POST etc. Por padrão, a API Web procura uma correspondência que não diferencia maiúsculas de minúsculas com o início do nome do método do controlador. Por exemplo, um método de controlador chamado PutCustomers
corresponde a uma solicitação HTTP PUT.
Você pode substituir essa convenção decorando o método com qualquer um dos seguintes atributos:
- [HttpDelete]
- [HttpGet]
- [HttpHead]
- [HttpOptions]
- [HttpPatch]
- [HttpPost]
- [HttpPut]
No exemplo a seguir, a API Web mapeia o método CreateBook para solicitações HTTP POST.
[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }
Para todos os outros métodos HTTP, incluindo métodos não padrão, use o atributo AcceptVerbs , que usa uma lista de métodos HTTP.
// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }
Prefixos de rota
Geralmente, as rotas em um controlador começam com o mesmo prefixo. Por exemplo:
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) { ... }
}
Você pode definir um prefixo comum para um controlador inteiro usando o 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 um til (~) no atributo de método para substituir o prefixo de rota:
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// GET /api/authors/1/books
[Route("~/api/authors/{authorId:int}/books")]
public IEnumerable<Book> GetByAuthor(int authorId) { ... }
// ...
}
O prefixo de rota pode incluir parâmetros:
[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
// GET customers/1/orders
[Route("orders")]
public IEnumerable<Order> Get(int customerId) { ... }
}
Restrições de rota
As restrições de rota permitem restringir como os parâmetros no modelo de rota são correspondidos. A sintaxe geral é "{parameter:constraint}". Por exemplo:
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
Aqui, a primeira rota só será selecionada se o segmento "id" do URI for um inteiro. Caso contrário, a segunda rota será escolhida.
A tabela a seguir lista as restrições com suporte.
Constraint | Descrição | Exemplo |
---|---|---|
alpha | Corresponde a caracteres em alfabeto latino maiúsculos ou minúsculos (a-z, A-Z) | {x:alpha} |
bool | Corresponde a um valor booliano. | {x:bool} |
DATETIME | Corresponde a um valor DateTime . | {x:datetime} |
decimal | Corresponde a um valor decimal. | {x:decimal} |
double | Corresponde a um valor de ponto flutuante de 64 bits. | {x:double} |
FLOAT | Corresponde a um valor de ponto flutuante de 32 bits. | {x:float} |
guid | Corresponde a um valor de GUID. | {x:guid} |
INT | Corresponde a um valor inteiro de 32 bits. | {x:int} |
comprimento | Corresponde a uma cadeia de caracteres com o comprimento especificado ou dentro de um intervalo de comprimentos especificado. | {x:length(6)} {x:length(1,20)} |
long | Corresponde a um valor inteiro de 64 bits. | {x:long} |
max | Corresponde a um inteiro com um valor máximo. | {x:max(10)} |
Maxlength | Corresponde a uma cadeia de caracteres com um comprimento máximo. | {x:maxlength(10)} |
min | Corresponde a um inteiro com um valor mínimo. | {x:min(10)} |
Minlength | Corresponde a uma cadeia de caracteres com um comprimento mínimo. | {x:minlength(10)} |
range | Corresponde a um inteiro dentro de um intervalo de valores. | {x:range(10,50)} |
regex | Corresponde a uma expressão regular. | {x:regex(^\d{3}-\d{3}-\d{4}$)} |
Observe que algumas das restrições, como "min", usam argumentos entre parênteses. Você pode aplicar várias restrições a um parâmetro, separado por dois-pontos.
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }
Restrições de rota personalizadas
Você pode criar restrições de rota personalizadas implementando a interface IHttpRouteConstraint . Por exemplo, a restrição a seguir restringe um parâmetro a um valor inteiro diferente de zero.
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;
}
}
O código a seguir mostra como registrar a restrição:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
}
}
Agora você pode aplicar a restrição em suas rotas:
[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }
Você também pode substituir toda a classe DefaultInlineConstraintResolver implementando a interface IInlineConstraintResolver . Isso substituirá todas as restrições internas, a menos que a implementação de IInlineConstraintResolver as adicione especificamente.
Parâmetros de URI opcionais e valores padrão
Você pode tornar um parâmetro de URI opcional adicionando um ponto de interrogação ao parâmetro de rota. Se um parâmetro de rota for opcional, você deverá definir um valor padrão para o parâmetro de método.
public class BooksController : ApiController
{
[Route("api/books/locale/{lcid:int?}")]
public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}
Neste exemplo, /api/books/locale/1033
e /api/books/locale
retornam o mesmo recurso.
Como alternativa, você pode especificar um valor padrão dentro do modelo de rota, da seguinte maneira:
public class BooksController : ApiController
{
[Route("api/books/locale/{lcid:int=1033}")]
public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}
Isso é quase o mesmo que o exemplo anterior, mas há uma pequena diferença de comportamento quando o valor padrão é aplicado.
- No primeiro exemplo ("{lcid:int?}"), o valor padrão de 1033 é atribuído diretamente ao parâmetro de método, portanto, o parâmetro terá esse valor exato.
- No segundo exemplo ("{lcid:int=1033}"), o valor padrão de "1033" passa pelo processo de model-binding. O model-binder padrão converterá "1033" no valor numérico 1033. No entanto, você pode conectar um associador de modelo personalizado, o que pode fazer algo diferente.
(Na maioria dos casos, a menos que você tenha associadores de modelo personalizados em seu pipeline, os dois formulários serão equivalentes.)
Nomes de rota
Na API Web, cada rota tem um nome. Os nomes de rota são úteis para gerar links, para que você possa incluir um link em uma resposta HTTP.
Para especificar o nome da rota, defina a propriedade Name no atributo . O exemplo a seguir mostra como definir o nome da rota e também como usar o nome da rota ao gerar um link.
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;
}
}
Ordem de Rota
Quando a estrutura tenta corresponder um URI com uma rota, ela avalia as rotas em uma ordem específica. Para especificar a ordem, defina a propriedade Order no atributo de rota. Os valores mais baixos são avaliados primeiro. O valor padrão da ordem é zero.
Veja como a ordenação total é determinada:
Compare a propriedade Order do atributo route.
Examine cada segmento de URI no modelo de rota. Para cada segmento, peça da seguinte maneira:
- Segmentos literais.
- Parâmetros de rota com restrições.
- Parâmetros de rota sem restrições.
- Segmentos de parâmetro curinga com restrições.
- Segmentos de parâmetro curinga sem restrições.
No caso de um empate, as rotas são ordenadas por uma comparação de cadeia de caracteres ordinal que não diferencia maiúsculas de minúsculas (OrdinalIgnoreCase) do modelo de rota.
Veja um exemplo. Suponha que você defina o seguinte controlador:
[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) { ... }
}
Essas rotas são ordenadas da seguinte maneira.
- pedidos/detalhes
- orders/{id}
- orders/{customerName}
- orders/{*date}
- pedidos/pendentes
Observe que "detalhes" é um segmento literal e aparece antes de "{id}", mas "pendente" aparece por último porque a propriedade Order é 1. (Este exemplo pressupõe que não haja clientes chamados "detalhes" ou "pendentes". Em geral, tente evitar rotas ambíguas. Neste exemplo, um modelo de rota melhor para GetByCustomer
é "customers/{customerName}" )