ASP.NET Web API 2 Odata のルーティング規則

この記事では、SP.NET 4.x の Web API 2 が OData エンドポイントに使用するルーティング規則について説明します。

Web API は、OData 要求を受け取ると、要求をコントローラー名とアクション名にマップします。 マッピングは、HTTP メソッドと URI に基づいて行われます。 たとえば、GET /odata/Products(1)ProductsController.GetProduct にマップします。

この記事のパート 1 では、組み込みの OData ルーティング規則について説明します。 これらの規則は、OData エンドポイント専用に設計されており、既定の Web API ルーティング システムに置き換わる規則です。 (置換は、MapODataRoute の呼び出し時に行われます。)

パート 2 では、カスタム ルーティング規則を追加する方法について説明します。 現時点では、組み込みの規則は OData URI の範囲全体をカバーしていませんが、追加のケースを処理するために規約を拡張できます。

組み込みのルーティング規則

Web API の OData ルーティング規則について説明する前に、OData URI について理解しておくことが役立ちます。 OData URI は次で構成されます。

  • サービス ルート
  • リソース パス
  • クエリ オプション

Screenshot to show what the O Data routing conventions looks like, displaying service root, resource path, and query options from left to right.

ルーティングで重要な部分はリソース パスです。 リソース パスはセグメントに分割されます。 たとえば、/Products(1)/Supplier には次の 3 つのセグメントがあります。

  • Products は、"Products" という名前のエンティティ セットを参照します。
  • 1 はエンティティ キーであり、セットから 1 つのエンティティを選択します。
  • Supplier は、関連エンティティを選択するナビゲーション プロパティです。

そのため、このパスは製品 1 のサプライヤーを選択します。

Note

OData パス セグメントが URI セグメントに常に対応するとは限りません。 たとえば、"1" はパス セグメントと見なされます。

コントローラー名。 コントローラー名は、常にリソース パスのルートにあるエンティティ セットから派生します。 たとえば、リソース パスが /Products(1)/Supplier である場合、Web API は ProductsController という名前のコントローラーを検索します。

アクション名。 アクション名は、次の表に示すように、パス セグメントとエンティティ データ モデル (EDM) から派生します。 場合によっては、アクション名に 2 つの選択肢があります。 たとえば、"Get" や "GetProducts" などです。

エンティティの照会

要求 URI の例 アクション名 アクションの例
GET /entityset /Products GetEntitySet または Get GetProducts
GET /entityset(key) /Products(1) GetEntityType または Get GetProduct
GET /entityset(key)/cast /Products(1)/Models.Book GetEntityType または Get GetBook

詳細については、「読み取り専用 OData エンドポイントを作成する」を参照してください。

エンティティの作成、更新、削除

要求 URI の例 アクション名 アクションの例
POST /entityset /Products 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)/Supplier 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/Suppliers(1) DeleteLink

詳細については、「エンティティ関係の操作」を参照してください。

Properties

Web 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 アクション」を参照してください。

メソッドのシグネチャ

メソッド シグネチャの規則をいくつか次に示します。

  • パスにキーが含まれている場合、アクションには 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)
}

カスタム ルーティング規則

現在、組み込みの規則では、考えられるすべての OData URI がカバーされているわけではありません。 IODataRoutingConvention インターフェイスを実装することで、新しい規則を追加できます。 このインターフェイスには、次の 2 つのメソッドが含まれています。

string SelectController(ODataPath odataPath, HttpRequestMessage request);
string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, 
    ILookup<string, HttpActionDescriptor> actionMap);
  • SelectController はコントローラーの名前を返します。
  • SelectAction はアクションの名前を返します。

どちらのメソッドでも、その要求に規則が適用されない場合、メソッドは null を返す必要があります。

ODataPath パラメーターは、解析された OData リソース パスを表します。 これには、リソース パスのセグメントごとに 1 つずつ、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. そのクラスの SelectController メソッドがこの新しいルーティング規則に適しているため、EntitySetRoutingConvention から派生します。 つまり、SelectController を再実装する必要はありません。
  2. この規則は GET 要求にのみ適用され、パス テンプレートが "~/entityset/key/navigation/key" の場合にのみ適用されます。
  3. アクション名は "Get{EntityType}" です。この場合、{EntityType} はナビゲーション コレクションの型です。 たとえば、"GetSupplier" などです。 任意の名前付け規則を使用できます。コントローラー アクションが一致していることを確認してください。
  4. アクションは、keyrelatedKey という名前の 2 つのパラメーターを受け取ります。 (事前定義済みのパラメーター名の一覧については、「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);

        }
    }
}

調査に役立つその他のサンプル ルーティング規則を次に示します。

もちろん、Web API 自体はオープンソースであるため、組み込みのルーティング規則のソース コードを確認できます。 これらは、System.Web.Http.OData.Routing.Conventions 名前空間で定義されています。