Роутинг атрибутами в ASP.NET WebAPI

Роутинг в ASP.NET MVC и WebAPI позволяет сопоставить каждый HTTP-запрос с конкретным контроллером и действием. Традиционно для определения маршрутов используется таблица, описывающая схему маршрутизации. Во второй версии WebAPI появилась альтернативная возможность — задавать маршруты атрибутами, непосредственно при определении контроллера и действия.

Атрибуты позволяют определить сложные маршруты без лишнего усложнения основной таблицы маршрутов. Для этого каждое действие размечается атрибутом [Route].

Конфигурация

Для использования атрибутов маршрутизации следует явно указать это при конфигурировании маршрутов. При создании ASP.NET WebAPI проекта, из шаблона создается файл App_Start/WebApiConfig.cs, содержащий записи о маршрутах для WebAPI. Здесь следует вставить вызов MapHttpAttributeRoutes() — это заставит инфраструктуру просмотреть все контроллеры/действия и учитывать их конфигурацию при роутинге тоже. Обычно код для настройки маршрутов сводится к такому:

public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes();

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

Определение маршрутов

Для задания маршрута нужно воспользоваться атрибутом [Route], в параметрах которого указать URL, который нужно связать с действием контроллера:

public class NodesController : ApiController
{
  [Route("api/nodes/byowner/{ownerId}")]
  public IEnumerable<node> GetByOwner(int ownderId)
  {
  }
}

Дополнительно может потребоваться указать HTTP-метод, при помощи которого предполагается обрабатывать запрос. Для этого есть универсальный атрибут [AcceptVerbs], либо его алиасы — [HttpGet], [HttpPost] и т.п.

Префиксы

Используя атрибуты легко прийти к ситуации, когда в контроллере большое количество действий, но URL для каждого из них начинается с одного и того же значения. Например:

public class NodesController : ApiController
{
  [Route("api/nodes/list")]
  public IEnumerable<node> GetAll()
  {
  }

  [Route("api/nodes/byowner/{ownerId}")]
  public IEnumerable<node> GetByOwner(int ownderId)
  {
  }

  [Route("api/nodes/byresource/{resourceId}")]
  public IEnumerable<node> GetByResource(int resourceId)
  {
  }
}</node>

Для упрощения схемы роутинга здесь можно использовать префиксы — [RoutePrefix]. Префиксы ассоциируются с контроллером и означают, что для каждого из действий к URL будет добавлен этот префикс. Переписать код, приведенный выше можно так:

[RoutePrefix("api/nodes")]
public class NodesController : ApiController
{
  [Route("list")]
  public IEnumerable<node> GetAll()
  {
  }

  [Route("byowner/{ownerId}")]
  public IEnumerable<node> GetByOwner(int ownderId)
  {
  }

  [Route("byresource/{resourceId}")]
  public IEnumerable<node> GetByResource(int resourceId)
  {
  }
}

Кстати, если какое-то из действий не укладывается в схему адресов с префиксом, это тоже решаемо — нужно указать полный URL и в начале добавить ~:

[RoutePrefix("api/nodes")]
public class NodesController : ApiController
{
  [Route("list")]
  public IEnumerable<Node> GetAll()
  {
  }

  [Route("~/aliases/bynode/{nodeId}")]
  public IEnumerable<Node> GetAliases(int nodeId)
  {
  }
}

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

Ограничения - важнейший механизм в маршрутах ASP.NET, позволяющий определить содержимое каждого параметра в URL. При маршрутизации, построенной на атрибутах есть два способа задания ограничений - в строке, при определении шаблона URL и отдельными объектами RouteConstraint.

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

[RoutePrefix("api/nodes")]
public class NodesController : ApiController
{
  [Route("list")]
  public IEnumerable<Node> GetAll()
  {
  }

  [Route("byowner/{ownerId:int}")]
  public IEnumerable<Node> GetByOwner(int ownerId)
  {
  }
  [Route("byowner/name/{owner:maxlength(32)}")]
  public IEnumerable<Node> GetByOwnerName(int owner)
  {
  }

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

public class ProductIdConstraint : IHttpRouteConstraint
{
  public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
                      HttpRouteDirection routeDirection)
  {
    bool result = false;

    // логика

    return result;
  }
}

Чтобы использовать этот класс в атрибутах, нужно его зарегистрировать:

public static void Register(HttpConfiguration config)
{
  var constraints = new DefaultInlineConstraintResolver();
  constraints.ConstraintMap.Add("productId", typeof(ProductIdConstraint));

  config.MapHttpAttributeRoutes(constraints);

Тогда его использование сводится к добавлению алиаса этого ограничения в URL:

[RoutePrefix("api/nodes")]
public class NodesController : ApiController
{
  [Route("byproduct/{id:productId}")]
  public IEnumerable<Node> ByProduct(int id)
  {
  }

Опциональные параметры

Некоторые параметры URL могут быть опциональными. Чтобы определить опциональные параметры можно воспользоваться двумя способами.

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

[RoutePrefix("api/nodes")]
public class NodesController : ApiController
{
  [Route("category/{id:int?}")]
  public IEnumerable<Node> ByCategory(int id = -1)
  {
  }

Другой способ — определить значение по умолчанию с самом шаблоне URL:

[RoutePrefix("api/nodes")]
public class NodesController : ApiController
{
  [Route("category/{id:int?=-1}")]
  public IEnumerable<Node> ByCategory(int id)
  {
  }