Маршрутизация атрибутов в веб-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 в атрибуте маршрута. Сначала вычисляются более низкие значения. Значение порядка по умолчанию равно нулю.
Вот как определяется общий порядок.
Сравните свойство Order атрибута маршрута.
Просмотрите каждый сегмент URI в шаблоне маршрута. Для каждого сегмента упорядочено следующим образом:
- Литеральные сегменты.
- Параметры маршрута с ограничениями.
- Параметры маршрута без ограничений.
- Сегменты параметров с подстановочными знаками с ограничениями.
- Сегменты параметров с подстановочными знаками без ограничений.
В случае привязки маршруты упорядочены по порядковой строке без учета регистра (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) { ... }
}
Эти маршруты упорядочены следующим образом.
- orders/details
- orders/{id}
- orders/{customerName}
- orders/{*date}
- заказы/ожидающие
Обратите внимание, что "details" является литеральным сегментом и отображается перед "{id}", но "ожидание" отображается последним, так как свойство Order имеет значение 1. (В этом примере предполагается, что нет клиентов с именем details или pending. Как правило, старайтесь избегать неоднозначных маршрутов. В этом примере лучшим шаблоном маршрута для GetByCustomer
является "customers/{customerName}" )