Маршрутизация атрибутов в веб-API ASP.NET 2

Маршрутизация — это то, как веб-API сопоставляет универсальный код ресурса (URI) с действием. Веб-API 2 поддерживает новый тип маршрутизации, называемый маршрутизацией атрибутов. Как следует из названия, маршрутизация атрибутов использует атрибуты для определения маршрутов. Маршрутизация атрибутов обеспечивает больший контроль над URI в веб-API. Например, можно легко создать URI, описывающие иерархии ресурсов.

Более ранний стиль маршрутизации, называемый маршрутизацией на основе соглашений, по-прежнему полностью поддерживается. Фактически вы можете объединить оба метода в одном проекте.

В этом разделе показано, как включить маршрутизацию атрибутов, и описаны различные варианты маршрутизации атрибутов. Полный учебник, в котором используется маршрутизация атрибутов, см. в статье Создание REST API с маршрутизацией атрибутов в веб-API 2.

Предварительные требования

Visual Studio 2017 Выпуск Community, Professional или Enterprise

Кроме того, для установки необходимых пакетов можно использовать диспетчер пакетов NuGet. В меню Сервис в Visual Studio выберите Диспетчер пакетов NuGet, а затем — Консоль диспетчера пакетов. Введите следующую команду в окне консоли диспетчера пакетов:

Install-Package Microsoft.AspNet.WebApi.WebHost

Зачем нужна маршрутизация атрибутов?

В первом выпуске веб-API использовалась маршрутизация на основе соглашений . В этом типе маршрутизации вы определяете один или несколько шаблонов маршрутов, которые в основном являются параметризованными строками. Когда платформа получает запрос, она сопоставляет универсальный код ресурса (URI) с шаблоном маршрута. Дополнительные сведения о маршрутизации на основе соглашений см. в статье Маршрутизация в веб-API ASP.NET.

Одним из преимуществ маршрутизации на основе соглашений является то, что шаблоны определяются в одном месте, а правила маршрутизации применяются согласованно ко всем контроллерам. К сожалению, маршрутизация на основе соглашений затрудняет поддержку определенных шаблонов URI, которые являются общими в ИНТЕРФЕЙСАх API RESTful. Например, ресурсы часто содержат дочерние ресурсы: у клиентов есть заказы, у фильмов есть актеры, у книг есть авторы и т. д. Естественно создавать универсальные коды ресурса (URI), которые отражают следующие связи:

/customers/1/orders

Этот тип URI трудно создать с помощью маршрутизации на основе соглашений. Хотя это можно сделать, результаты не будут масштабироваться, если у вас много контроллеров или типов ресурсов.

При маршрутизации атрибутов легко определить маршрут для этого URI. Просто добавьте атрибут к действию контроллера:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

Ниже приведены некоторые другие шаблоны, упрощающие маршрутизацию атрибутов.

Управление версиями API

В этом примере "/api/v1/products" будет направляться на контроллер, отличный от "/api/v2/products".

/api/v1/products /api/v2/products

Перегруженные сегменты URI

В этом примере "1" является номером заказа, но "ожидание" сопоставляется с коллекцией.

/orders/1 /orders/pending

Несколько типов параметров

В этом примере "1" является номером заказа, но "2013/06/16" указывает дату.

/orders/1 /orders/2013/06/16

Включение маршрутизации атрибутов

Чтобы включить маршрутизацию атрибутов, вызовите MapHttpAttributeRoutes во время настройки. Этот метод расширения определен в классе 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.
        }
    }
}

Маршрутизацию атрибутов можно сочетать с маршрутизацией на основе соглашений . Чтобы определить маршруты на основе соглашений, вызовите метод 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 }
        );
    }
}

Дополнительные сведения о настройке веб-API см. в разделе Настройка веб-API ASP.NET 2.

Примечание. Миграция с веб-API 1

До версии 2 шаблоны проектов веб-API формировали код следующим образом:

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

Если маршрутизация атрибутов включена, этот код вызовет исключение. Если вы обновляете существующий проект веб-API для использования маршрутизации атрибутов, обязательно обновите этот код конфигурации до следующего:

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Примечание

Дополнительные сведения см. в статье Настройка веб-API с помощью ASP.NET Hosting.

Добавление атрибутов маршрута

Ниже приведен пример маршрута, определенного с помощью атрибута :

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

Строка "customers/{customerId}/orders" — это шаблон URI для маршрута. Веб-API пытается сопоставить URI запроса с шаблоном. В этом примере "клиенты" и "заказы" являются литеральными сегментами, а "{customerId}" — параметром переменной. Следующие универсальные коды ресурса (URI) будут соответствовать этому шаблону:

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

Вы можете ограничить сопоставление с помощью ограничений, описанных далее в этом разделе.

Обратите внимание, что параметр "{customerId}" в шаблоне маршрута соответствует имени параметра customerId в методе . Когда веб-API вызывает действие контроллера, он пытается привязать параметры маршрута. Например, если URI имеет значение http://example.com/customers/1/orders, веб-API пытается привязать значение "1" к параметру customerId в действии.

Шаблон URI может иметь несколько параметров:

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

Все методы контроллера, у которых нет атрибута маршрута, используют маршрутизацию на основе соглашений. Таким образом, можно объединить оба типа маршрутизации в одном проекте.

Методы HTTP

Веб-API также выбирает действия на основе метода HTTP запроса (GET, POST и т. д.). По умолчанию веб-API ищет совпадение без учета регистра с началом имени метода контроллера. Например, метод контроллера с именем PutCustomers соответствует HTTP-запросу PUT.

Это соглашение можно переопределить, дополнив метод любым из следующих атрибутов:

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

В следующем примере веб-API сопоставляет метод CreateBook с HTTP-запросами POST.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

Для всех других методов HTTP, включая нестандартные методы, используйте атрибут AcceptVerbs , который принимает список методов HTTP.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

Префиксы маршрута

Часто маршруты в контроллере начинаются с одного префикса. Пример:

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

Общий префикс для всего контроллера можно задать с помощью атрибута [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) { ... }
}

Используйте тильду (~) в атрибуте метода, чтобы переопределить префикс маршрута:

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

Префикс маршрута может содержать параметры:

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

Ограничения маршрутов

Ограничения маршрута позволяют ограничить способ сопоставления параметров в шаблоне маршрута. Общий синтаксис — "{parameter:constraint}". Пример:

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

Здесь первый маршрут будет выбран только в том случае, если сегмент "id" универсального кода ресурса (URI) является целым числом. В противном случае будет выбран второй маршрут.

В следующей таблице перечислены поддерживаемые ограничения.

Ограничение Описание Пример
alpha Соответствует символам латинского алфавита верхнего или нижнего регистра (a–z, A–Z) {x:alpha}
bool Соответствует логическому значению. {x:bool}
DATETIME Соответствует значению DateTime . {x:datetime}
Decimal Соответствует десятичному значению. {x:decimal}
double Соответствует 64-разрядному значению с плавающей запятой. {x:double}
FLOAT Соответствует 32-разрядному значению с плавающей запятой. {x:float}
guid Соответствует значению GUID. {x:guid}
INT Соответствует 32-разрядному целочисленное значение. {x:int}
length Соответствует строке с указанной длиной или в пределах заданного диапазона длин. {x:length(6)} {x:length(1,20)}
long Соответствует 64-разрядному целочисленное значение. {x:long}
max Сопоставляет целое число с максимальным значением. {x:max(10)}
Maxlength Соответствует строке с максимальной длиной. {x:maxlength(10)}
мин Сопоставляет целое число с минимальным значением. {x:min(10)}
minlength Соответствует строке с минимальной длиной. {x:minlength(10)}
range Соответствует целым числам в диапазоне значений. {x:range(10,50)}
regex Соответствует регулярному выражению. {x:regex(^\d{3}-\d{3}-\d{4}$)}

Обратите внимание, что некоторые ограничения, например "min", принимают аргументы в скобках. К параметру можно применить несколько ограничений, разделенных двоеточием.

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

Пользовательские ограничения маршрутов

Вы можете создать настраиваемые ограничения маршрутов, реализовав интерфейс IHttpRouteConstraint . Например, следующее ограничение ограничивает параметр ненулевым целочисленным значением.

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;
    }
}

В следующем коде показано, как зарегистрировать ограничение:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

Теперь вы можете применить ограничение в маршрутах:

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

Вы также можете заменить весь класс DefaultInlineConstraintResolver , реализовав интерфейс IInlineConstraintResolver . Это приведет к замене всех встроенных ограничений, если только ваша реализация IInlineConstraintResolver не добавит их.

Необязательные параметры URI и значения по умолчанию

Параметр URI можно сделать необязательным, добавив вопросительный знак в параметр маршрута. Если параметр маршрута является необязательным, необходимо определить значение по умолчанию для параметра метода.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

В этом примере /api/books/locale/1033 и /api/books/locale возвращают тот же ресурс.

Кроме того, можно указать значение по умолчанию в шаблоне маршрута следующим образом:

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

Это почти то же самое, что и в предыдущем примере, но при применении значения по умолчанию существует небольшая разница в поведении.

  • В первом примере ("{lcid:int?}") значение по умолчанию 1033 присваивается непосредственно параметру метода, поэтому параметр будет иметь это точное значение.
  • Во втором примере ("{lcid:int=1033}") значение по умолчанию "1033" проходит процесс привязки модели. Связыватель модели по умолчанию преобразует "1033" в числовое значение 1033. Однако вы можете подключить пользовательский связыватель модели, который может сделать что-то другое.

(В большинстве случаев, если в конвейере нет пользовательских связывателей моделей, две формы будут эквивалентными.)

Имена маршрутов

В веб-API каждый маршрут имеет имя. Имена маршрутов полезны для создания ссылок, чтобы можно было включить ссылку в HTTP-ответ.

Чтобы указать имя маршрута, задайте свойство Name атрибута . В следующем примере показано, как задать имя маршрута, а также как использовать его при создании ссылки.

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;
    }
}

Порядок маршрутов

Когда платформа пытается сопоставить URI с маршрутом, она оценивает маршруты в определенном порядке. Чтобы указать порядок, задайте свойство Order в атрибуте маршрута. Сначала вычисляются более низкие значения. Значение порядка по умолчанию равно нулю.

Вот как определяется общий порядок.

  1. Сравните свойство Order атрибута маршрута.

  2. Просмотрите каждый сегмент URI в шаблоне маршрута. Для каждого сегмента упорядочено следующим образом:

    1. Литеральные сегменты.
    2. Параметры маршрута с ограничениями.
    3. Параметры маршрута без ограничений.
    4. Сегменты параметров с подстановочными знаками с ограничениями.
    5. Сегменты параметров с подстановочными знаками без ограничений.
  3. В случае привязки маршруты упорядочены по порядковой строке без учета регистра (OrdinalIgnoreCase) шаблона маршрута.

Ниже приведен пример. Предположим, вы определили следующий контроллер:

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

Эти маршруты упорядочены следующим образом.

  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. заказы/ожидающие

Обратите внимание, что "details" является литеральным сегментом и отображается перед "{id}", но "ожидание" отображается последним, так как свойство Order имеет значение 1. (В этом примере предполагается, что нет клиентов с именем details или pending. Как правило, старайтесь избегать неоднозначных маршрутов. В этом примере лучшим шаблоном маршрута для GetByCustomer является "customers/{customerName}" )