Соглашения о маршрутизации в Odata веб-API ASP.NET 2

В этой статье описываются соглашения о маршрутизации, которые веб-API 2 в ASP.NET 4.x использует для конечных точек OData.

Когда веб-API получает запрос OData, он сопоставляет запрос с именем контроллера и именем действия. Сопоставление основано на методе HTTP и универсальном коде ресурса (URI). Например, GET /odata/Products(1) сопоставляется с ProductsController.GetProduct.

В части 1 этой статьи я описываю встроенные соглашения о маршрутизации OData. Эти соглашения разработаны специально для конечных точек OData и заменяют систему маршрутизации веб-API по умолчанию. (Замена происходит при вызове MapODataRoute.)

В части 2 я показываю, как добавить пользовательские соглашения о маршрутизации. В настоящее время встроенные соглашения не охватывают весь диапазон URI OData, но их можно расширить для обработки дополнительных случаев.

Встроенные соглашения о маршрутизации

Прежде чем описывать соглашения о маршрутизации OData в веб-API, полезно понять URI OData. URI OData состоит из следующих элементов:

  • Корневой каталог службы
  • Путь к ресурсу
  • Параметры запроса

Снимок экрана, на котором показано, как выглядят соглашения о маршрутизации данных O с отображением корневого каталога службы, пути к ресурсу и параметров запроса слева направо.

Для маршрутизации важной частью является путь к ресурсам. Путь к ресурсу делится на сегменты. Например, /Products(1)/Supplier имеет три сегмента:

  • Products ссылается на набор сущностей с именем "Products".
  • 1 — это ключ сущности, который выбирает одну сущность из набора.
  • Supplier — это свойство навигации, которое выбирает связанную сущность.

Таким образом, этот путь выбирает поставщика продукта 1.

Примечание

Сегменты пути OData не всегда соответствуют сегментам URI. Например, "1" считается сегментом пути.

Имена контроллеров. Имя контроллера всегда является производным от набора сущностей в корне пути к ресурсу. Например, если путь к ресурсу — /Products(1)/Supplier, веб-API ищет контроллер с именем ProductsController.

Имена действий. Имена действий являются производными от сегментов пути и модели данных сущности (EDM), как указано в следующих таблицах. В некоторых случаях имя действия можно выбрать двумя вариантами. Например, Get или GetProducts.

Запрос сущностей

Запрос Пример URI Название действия Пример действия
GET /entityset /Продукты GetEntitySet или Get GetProducts
GET /entityset(key) /Products(1) GetEntityType или Get GetProduct
GET /entityset(key)/cast /Products(1)/Models.Book GetEntityType или Get GetBook

Дополнительные сведения см. в статье Создание конечной точки OData Read-Only.

Создание, обновление и удаление сущностей

Запрос Пример URI Название действия Пример действия
POST /entityset /Продукты PostEntityType или Post PostProduct
PUT /entityset(key) /Products(1) PutEntityType или Put PutProduct
PUT /entityset(key)/cast /Products(1)/Models.Book PutEntityType или Put PutBook
PATCH /entityset(key) /Products(1) PatchEntityType или Patch PatchProduct
PATCH /entityset(key)/cast /Products(1)/Models.Book PatchEntityType или Patch PatchBook
DELETE /entityset(key) /Products(1) DeleteEntityType или Delete DeleteProduct
DELETE /entityset(key)/cast /Products(1)/Models.Book DeleteEntityType или Delete DeleteBook

Запрос свойства навигации

Запрос Пример URI Название действия Пример действия
GET /entityset(key)/navigation /Products(1)/Поставщик GetNavigationFromEntityType или GetNavigation GetSupplierFromProduct
GET /entityset(key)/cast/navigation /Products(1)/Models.Book/Author GetNavigationFromEntityType или GetNavigation GetAuthorFromBook

Дополнительные сведения см. в разделе Работа с отношениями сущностей.

Создание и удаление ссылок

Запрос Пример URI Название действия
POST /entityset(key)/$links/navigation /Products(1)/$links/Supplier CreateLink
PUT /entityset(key)/$links/navigation /Products(1)/$links/Supplier CreateLink
DELETE /entityset(key)/$links/navigation /Products(1)/$links/Supplier DeleteLink
DELETE /entityset(key)/$links/navigation(relatedKey) /Products/(1)/$links/Providers(1) DeleteLink

Дополнительные сведения см. в разделе Работа с отношениями сущностей.

Свойства

Требуется веб-API 2

Запрос Пример URI Название действия Пример действия
GET /entityset(key)/property /Products(1)/Name GetPropertyFromEntityType или GetProperty GetNameFromProduct
GET /entityset(key)/cast/property /Products(1)/Models.Book/Author GetPropertyFromEntityType или GetProperty GetTitleFromBook

Действия

Запрос Пример URI Название действия Пример действия
POST /entityset(key)/action /Products(1)/Rate ActionNameOnEntityType или ActionName RateOnProduct
POST /entityset(key)/cast/action /Products(1)/Models.Book/CheckOut ActionNameOnEntityType или ActionName CheckOutOnBook

Дополнительные сведения см. в разделе OData Actions.

Сигнатуры методов

Ниже приведены некоторые правила для сигнатур методов.

  • Если путь содержит ключ, действие должно иметь параметр с именем key.
  • Если путь содержит ключ в свойстве навигации, действие должно иметь параметр relatedKey.
  • Укажите параметры key и relatedKey с помощью параметра [FromODataUri] .
  • Запросы POST и PUT принимают параметр типа сущности.
  • Запросы PATCH принимают параметр типа Delta<T>, где T — это тип сущности.

Для справки ниже приведен пример сигнатуры методов для каждого встроенного соглашения о маршрутизации OData.

public class ProductsController : ODataController
{
    // GET /odata/Products
    public IQueryable<Product> Get()

    // GET /odata/Products(1)
    public Product Get([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book
    public Book GetBook([FromODataUri] int key)

    // POST /odata/Products 
    public HttpResponseMessage Post(Product item)

    // PUT /odata/Products(1)
    public HttpResponseMessage Put([FromODataUri] int key, Product item)

    // PATCH /odata/Products(1)
    public HttpResponseMessage Patch([FromODataUri] int key, Delta<Product> item)

    // DELETE /odata/Products(1)
    public HttpResponseMessage Delete([FromODataUri] int key)

    // PUT /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage PutBook([FromODataUri] int key, Book item)

    // PATCH /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage PatchBook([FromODataUri] int key, Delta<Book> item)

    // DELETE /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage DeleteBook([FromODataUri] int key)

    //  GET /odata/Products(1)/Supplier
    public Supplier GetSupplierFromProduct([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book/Author
    public Author GetAuthorFromBook([FromODataUri] int key)

    // POST /odata/Products(1)/$links/Supplier
    public HttpResponseMessage CreateLink([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)

    // DELETE /odata/Products(1)/$links/Supplier
    public HttpResponseMessage DeleteLink([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)

    // DELETE /odata/Products(1)/$links/Parts(1)
    public HttpResponseMessage DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty)

    // GET odata/Products(1)/Name
    // GET odata/Products(1)/Name/$value
    public HttpResponseMessage GetNameFromProduct([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book/Title
    // GET /odata/Products(1)/ODataRouting.Models.Book/Title/$value
    public HttpResponseMessage GetTitleFromBook([FromODataUri] int key)
}

Настраиваемые соглашения о маршрутизации

В настоящее время встроенные соглашения не охватывают все возможные URI OData. Вы можете добавить новые соглашения, реализовав интерфейс IODataRoutingConvention . Этот интерфейс имеет два метода:

string SelectController(ODataPath odataPath, HttpRequestMessage request);
string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, 
    ILookup<string, HttpActionDescriptor> actionMap);
  • SelectController возвращает имя контроллера.
  • SelectAction возвращает имя действия.

Для обоих методов, если соглашение не применяется к такому запросу, метод должен возвращать значение NULL.

Параметр ODataPath представляет проанализированный путь к ресурсу OData. Он содержит список экземпляров ODataPathSegment , по одному для каждого сегмента пути к ресурсу. ODataPathSegment — это абстрактный класс; Каждый тип сегмента представлен классом, производным от ODataPathSegment.

Свойство ODataPath.TemplatePath — это строка, представляющая объединение всех сегментов пути. Например, если URI имеет значение /Products(1)/Supplier, шаблон пути — "~/entityset/key/navigation". Обратите внимание, что сегменты не соответствуют напрямую сегментам URI. Например, ключ сущности (1) представлен как собственный ODataPathSegment.

Как правило, реализация IODataRoutingConvention выполняет следующие действия:

  1. Сравните шаблон пути, чтобы узнать, применяется ли это соглашение к текущему запросу. Если он не применяется, возвращается значение NULL.
  2. Если применяется соглашение, используйте свойства экземпляров ODataPathSegment для получения имен контроллеров и действий.
  3. Для действий добавьте все значения в словарь маршрутов, которые должны быть привязаны к параметрам действия (обычно это ключи сущностей).

Рассмотрим конкретный пример. Встроенные соглашения о маршрутизации не поддерживают индексирование в коллекции навигации. Иными словами, для URI не существует соглашения, подобного следующему:

/odata/Products(1)/Suppliers(1)

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

using Microsoft.Data.Edm;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;

namespace ODataRouting
{
    public class NavigationIndexRoutingConvention : EntitySetRoutingConvention
    {
        public override string SelectAction(ODataPath odataPath, HttpControllerContext context, 
            ILookup<string, HttpActionDescriptor> actionMap)
        {
            if (context.Request.Method == HttpMethod.Get && 
                odataPath.PathTemplate == "~/entityset/key/navigation/key")
            {
                NavigationPathSegment navigationSegment = odataPath.Segments[2] as NavigationPathSegment;
                IEdmNavigationProperty navigationProperty = navigationSegment.NavigationProperty.Partner;
                IEdmEntityType declaringType = navigationProperty.DeclaringType as IEdmEntityType;

                string actionName = "Get" + declaringType.Name;
                if (actionMap.Contains(actionName))
                {
                    // Add keys to route data, so they will bind to action parameters.
                    KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
                    context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;

                    KeyValuePathSegment relatedKeySegment = odataPath.Segments[3] as KeyValuePathSegment;
                    context.RouteData.Values[ODataRouteConstants.RelatedKey] = relatedKeySegment.Value;

                    return actionName;
                }
            }
            // Not a match.
            return null;
        }
    }
}

Примечания.

  1. Я являюсь производным от EntitySetRoutingConvention, так как метод SelectController в этом классе подходит для этого нового соглашения о маршрутизации. Это означает, что мне не нужно повторно реализовывать SelectController.
  2. Соглашение применяется только к запросам GET и только в том случае, если шаблон пути — "~/entityset/key/navigation/key".
  3. Имя действия — Get{EntityType}, где {EntityType} — это тип коллекции навигации. Например, GetSupplier. Вы можете использовать любое соглашение об именовании, которое вам нравится— просто убедитесь, что действия контроллера совпадают.
  4. Действие принимает два параметра с именами key и relatedKey. (Список некоторых стандартных имен параметров см. в разделе ODataRouteConstants.)

Следующий шаг — добавление нового соглашения в список соглашений о маршрутизации. Это происходит во время настройки, как показано в следующем коде:

using ODataRouting.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;

namespace ODataRouting
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
            // Create EDM (not shown).

            // Create the default collection of built-in conventions.
            var conventions = ODataRoutingConventions.CreateDefault();
            // Insert the custom convention at the start of the collection.
            conventions.Insert(0, new NavigationIndexRoutingConvention());

            config.Routes.MapODataRoute(routeName: "ODataRoute",
                routePrefix: "odata",
                model: modelBuilder.GetEdmModel(),
                pathHandler: new DefaultODataPathHandler(),
                routingConventions: conventions);

        }
    }
}

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

И, конечно, сам веб-API имеет открытый код, поэтому вы можете просмотреть исходный код для встроенных соглашений о маршрутизации. Они определены в пространстве имен System.Web.Http.OData.Routing.Conventions .