Выбор маршрутизации и действий в веб-API ASP.NET

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

Примечание

Общие сведения о маршрутизации см. в статье Маршрутизация в веб-API ASP.NET.

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

Маршрутизация состоит из трех main этапов:

  1. Сопоставление URI с шаблоном маршрута.
  2. Выбор контроллера.
  3. Выбор действия.

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

Шаблоны маршрутов

Шаблон маршрута похож на путь URI, но он может иметь значения заполнителей, обозначаемые фигурными скобками:

"api/{controller}/public/{category}/{id}"

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

defaults: new { category = "all" }

Можно также указать ограничения, ограничивающие, как сегмент URI может соответствовать заполнителю:

constraints: new { id = @"\d+" }   // Only matches if "id" is one or more digits.

Платформа пытается сопоставить сегменты в пути URI с шаблоном. Литералы в шаблоне должны точно совпадать. Заполнитель соответствует любому значению, если не указаны ограничения. Платформа не соответствует другим частям URI, таким как имя узла или параметры запроса. Платформа выбирает первый маршрут в таблице маршрутов, соответствующий URI.

Существует два специальных заполнителя: "{controller}" и "{action}".

  • "{controller}" указывает имя контроллера.
  • "{action}" предоставляет имя действия. В веб-API обычное соглашение заключается в пропуске "{action}".

Умолчания;

Если указать значения по умолчанию, маршрут будет соответствовать универсальному коду ресурса (URI), в котором отсутствуют эти сегменты. Пример:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}",
    defaults: new { category = "all" }
);

URI http://localhost/api/products/all и http://localhost/api/products соответствуют предыдущему маршруту. В последнем универсальном коде ресурса (URI) отсутствующим {category} сегментам присваивается значение allпо умолчанию .

Словарь маршрутов

Если платформа находит совпадение для URI, она создает словарь, содержащий значение для каждого заполнителя. Ключи — это имена заполнителей, не включая фигурные скобки. Значения берутся из пути URI или из значений по умолчанию. Словарь хранится в объекте IHttpRouteData .

На этом этапе сопоставления маршрутов специальные заполнители "{controller}" и "{action}" обрабатываются так же, как и другие заполнители. Они просто хранятся в словаре с другими значениями.

Значение по умолчанию может иметь специальное значение RouteParameter.Optional. Если заполнителю присваивается это значение, значение не добавляется в словарь маршрутов. Пример:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}/{id}",
    defaults: new { category = "all", id = RouteParameter.Optional }
);

Для пути URI "api/products" словарь маршрутов будет содержать:

  • controller: "products"
  • category: "all"

Однако для api/products/toys/123 словарь маршрутов будет содержать:

  • controller: "products"
  • категория: "toys"
  • id: "123"

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

routes.MapHttpRoute(
    name: "Root",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "customers", id = RouteParameter.Optional }
);

Если путь URI — api/root/8, словарь будет содержать два значения:

  • контроллер: "клиенты"
  • id: "8"

Выбор контроллера

Выбор контроллера обрабатывается методом IHttpControllerSelector.SelectController . Этот метод принимает экземпляр HttpRequestMessage и возвращает httpControllerDescriptor. Реализация по умолчанию предоставляется классом DefaultHttpControllerSelector . В этом классе используется простой алгоритм:

  1. В словаре маршрутов найдите ключ "контроллер".
  2. Возьмите значение для этого ключа и добавьте строку Controller, чтобы получить имя типа контроллера.
  3. Найдите контроллер веб-API с этим именем типа.

Например, если словарь маршрутов содержит пару "ключ-значение" controller = "products", то тип контроллера — "ProductsController". Если совпадающий тип отсутствует или несколько совпадений, платформа возвращает клиенту ошибку.

На шаге 3 defaultHttpControllerSelector использует интерфейс IHttpControllerTypeResolver для получения списка типов контроллеров веб-API. Реализация IHttpControllerTypeResolver по умолчанию возвращает все открытые классы, которые (а) реализуют IHttpController, (b) не являются абстрактными и (c) имеют имя, которое заканчивается на Controller.

Выбор действия

После выбора контроллера платформа выбирает действие, вызывая метод IHttpActionSelector.SelectAction . Этот метод принимает HttpControllerContext и возвращает httpActionDescriptor.

Реализация по умолчанию предоставляется классом ApiControllerActionSelector . Чтобы выбрать действие, оно выглядит следующим образом:

  • Метод HTTP, используемый для запроса.
  • Заполнитель "{action}" в шаблоне маршрута, если он имеется.
  • Параметры действий на контроллере.

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

Какие методы на контроллере считаются действиями? При выборе действия платформа просматривает только открытые методы экземпляра на контроллере. Кроме того, он исключает методы "специального имени" (конструкторы, события, перегрузки операторов и т. д.), а также методы, унаследованные от класса ApiController .

Методы HTTP. Платформа выбирает только действия, соответствующие методу HTTP запроса, которые определяются следующим образом:

  1. Метод HTTP можно указать с атрибутом AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost или HttpPut.
  2. В противном случае, если имя метода контроллера начинается с "Get", "Post", "Put", "Delete", "Head", "Options" или "Patch", то по соглашению действие поддерживает этот метод HTTP.
  3. Если ни одно из указанных выше действий не указано, метод поддерживает POST.

Привязки параметров. Привязка параметра — это то, как веб-API создает значение для параметра. Ниже приведено правило по умолчанию для привязки параметров:

  • Простые типы берутся из универсального кода ресурса (URI).
  • Сложные типы берутся из текста запроса.

Простые типы включают все платформа .NET Framework примитивные типы, а также DateTime, Decimal, Guid, String и TimeSpan. Для каждого действия считывает текст запроса не более одного параметра.

Примечание

Можно переопределить правила привязки по умолчанию. См. статью Привязка параметров WebAPI.

С таким фоном ниже приведен алгоритм выбора действий.

  1. Создайте список всех действий на контроллере, которые соответствуют методу HTTP-запроса.

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

  3. Попробуйте сопоставить параметры действия с универсальным кодом ресурса (URI) следующим образом:

    1. Для каждого действия получите список параметров простого типа, где привязка получает параметр из URI. Исключите необязательные параметры.
    2. В этом списке попробуйте найти совпадение для каждого имени параметра в словаре маршрутов или в строке запроса URI. Совпадения не учитывают регистр и не зависят от порядка параметров.
    3. Выберите действие, в котором каждому параметру в списке соответствует универсальный код ресурса (URI).
    4. Если этим критериям соответствует несколько действий, выберите одно действие с наибольшим соответствием параметров.
  4. Игнорировать действия с атрибутом [NonAction] .

Шаг 3, вероятно, является наиболее запутанным. Основная идея заключается в том, что параметр может получить свое значение из URI, из текста запроса или из пользовательской привязки. Для параметров, поступающих из URI, мы хотим убедиться, что URI фактически содержит значение для этого параметра либо в пути (через словарь маршрутов), либо в строке запроса.

Например, рассмотрим следующее действие:

public void Get(int id)

Параметр id привязывается к URI. Таким образом, это действие может совпадать только с универсальным кодом ресурса (URI), содержащим значение id, либо в словаре маршрутов, либо в строке запроса.

Необязательные параметры являются исключением, так как они являются необязательными. Если привязке не удается получить значение из универсального кода ресурса (URI), это нормально.

Сложные типы являются исключением по другой причине. Сложный тип может привязаться к URI только с помощью пользовательской привязки. Но в этом случае платформа не может заранее знать, будет ли параметр привязан к конкретному URI. Чтобы узнать это, необходимо вызвать привязку. Цель алгоритма выбора — выбрать действие из статического описания перед вызовом каких-либо привязок. Поэтому сложные типы исключаются из алгоритма сопоставления.

После выбора действия вызываются все привязки параметров.

Сводка:

  • Действие должно соответствовать методу HTTP запроса.
  • Имя действия должно соответствовать записи action в словаре маршрутов, если оно имеется.
  • Для каждого параметра действия, если параметр взят из URI, имя параметра должно находиться либо в словаре маршрутов, либо в строке запроса URI. (Необязательные параметры и параметры со сложными типами исключаются.)
  • Старайтесь соответствовать наибольшему количеству параметров. Лучшим совпадением может быть метод без параметров.

Расширенный пример

Маршруты:

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

Контроллер:

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

HTTP-запрос:

GET http://localhost:34701/api/products/1?version=1.5&details=1

Сопоставление маршрутов

Универсальный код ресурса (URI) соответствует маршруту DefaultApi. Словарь маршрутов содержит следующие записи:

  • контроллер: "products"
  • id: "1"

Словарь маршрутов не содержит параметров строки запроса " version" и "details", но они по-прежнему будут учитываться при выборе действия.

Выбор контроллера

Из записи "контроллер" в словаре маршрутов используется ProductsControllerтип контроллера .

Выбор действия

HTTP-запрос является запросом GET. Действия контроллера, поддерживающие GET GetAll: , GetByIdи FindProductsByName. Словарь маршрутов не содержит записи для "action", поэтому нам не нужно сопоставлять имя действия.

Далее мы пытаемся сопоставить имена параметров для действий, просматривая только действия GET.

Действие Параметры для сопоставления
GetAll нет
GetById идентификатор
FindProductsByName "name"

Обратите внимание, что параметр GetByIdверсии не учитывается, так как он является необязательным параметром.

Метод GetAll тривиально соответствует. Метод GetById также совпадает, так как словарь маршрутов содержит "id". Метод FindProductsByName не соответствует.

Метод GetById выигрывает, так как он соответствует одному параметру и не имеет параметров для GetAll. Метод вызывается со следующими значениями параметров:

  • id = 1
  • версия = 1.5

Обратите внимание, что даже если версия не использовалась в алгоритме выбора, значение параметра поступает из строки запроса URI.

Точки расширения

Веб-API предоставляет точки расширения для некоторых частей процесса маршрутизации.

Интерфейс Описание
IHttpControllerSelector Выбирает контроллер.
IHttpControllerTypeResolver Возвращает список типов контроллеров. DefaultHttpControllerSelector выбирает тип контроллера из этого списка.
IAssembliesResolver Возвращает список сборок проекта. Интерфейс IHttpControllerTypeResolver использует этот список для поиска типов контроллеров.
IHttpControllerActivator Создает новые экземпляры контроллера.
IHttpActionSelector Выбирает действие.
IHttpActionInvoker Вызывает действие .

Чтобы предоставить собственную реализацию для любого из этих интерфейсов, используйте коллекцию Services в объекте HttpConfiguration :

var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));