Roteamento de atributo no ASP.NET Web API 2

O 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 fornece mais controle sobre as URIs em sua API Web. Por exemplo, você pode facilmente criar URIs que descrevem hierarquias de recursos.

O estilo anterior de roteamento, chamado de roteamento baseado em convenção, ainda tem suporte total. 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, selecione 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ção . 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 com o 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 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 de 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ção . Para definir rotas baseadas em convenções, 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 este 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 Configurar 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. As 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 bloco (~) 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 letras maiúsculas ou minúsculas do alfabeto latino (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 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 sua 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 retorne 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 do 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 associação de modelo. O associador de modelo 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, as duas formas 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. Valores mais baixos são avaliados primeiro. O valor padrão da ordem é zero.

Veja como a ordenação total é determinada:

  1. Compare a propriedade Order do atributo de rota.

  2. Examine cada segmento de URI no modelo de rota. Para cada segmento, peça da seguinte maneira:

    1. Segmentos literais.
    2. Parâmetros de rota com restrições.
    3. Parâmetros de rota sem restrições.
    4. Segmentos de parâmetro curinga com restrições.
    5. Segmentos de parâmetro curinga sem restrições.
  3. No caso de um empate, as rotas são ordenadas por uma comparação de cadeia de caracteres ordinal (OrdinalIgnoreCase) sem maiúsculas de minúsculas 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.

  1. pedidos/detalhes
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. 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 é GetByCustomer "customers/{customerName}" )