次の方法で共有


ASP.NET Web API でのルーティングとアクションの選択

この記事では、ASP.NET Web API がコントローラー上の特定のアクションに HTTP 要求をルーティングする方法について説明します。

Note

ルーティングの概要については、「ASP.NET Web API でのルーティング」を参照してください。

この記事では、ルーティング プロセスの詳細について説明します。 Web API プロジェクトを作成し、一部の要求が期待した方法でルーティングされない場合は、この記事が役立ちます。

ルーティングには、次の 3 つのメインフェーズがあります。

  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}" の 2 つの特別なプレースホルダーがあります。

  • "{controller}" はコントローラーの名前を提供します。
  • "{action}" はアクションの名前を提供します。 Web API では、通常の規則では "{action}" を省略します。

既定

既定値を指定した場合、ルートはそれらのセグメントがない URI と一致します。 次に例を示します。

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

URI http://localhost/api/products/allhttp://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"
  • category: "toys"
  • id: "123"

既定値には、ルート テンプレートのどこにも表示されない値を含めることもできます。 ルートが一致する場合、その値はディクショナリに格納されます。 次に例を示します。

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

URI パスが "api/root/8" の場合、ディクショナリには次の 2 つの値が含まれます。

  • controller: "customers"
  • id: "8"

コントローラーの選択

コントローラーの選択は、IHttpControllerSelector.SelectController メソッドによって処理されます。 このメソッドは HttpRequestMessage インスタンスを受け取り、HttpControllerDescriptor を返します。 既定の実装は、DefaultHttpControllerSelector クラスによって提供されます。 このクラスでは、単純なアルゴリズムを使用します。

  1. ルート ディクショナリでキー "コントローラー" を探します。
  2. このキーの値を取得し、文字列 "Controller" を追加してコントローラーの型名を取得します。
  3. この型名を持つ Web API コントローラーを探します。

たとえば、ルート ディクショナリにキーと値のペア "controller" = "products" が含まれている場合、コントローラーの種類は "ProductsController" になります。 一致する型がない場合、または複数の一致がある場合、フレームワークはクライアントにエラーを返します。

手順 3 では、DefaultHttpControllerSelectorIHttpControllerTypeResolver インターフェイスを使用して、Web API コントローラーの種類の一覧を取得します。 IHttpControllerTypeResolver の既定の実装は、(a) IHttpController を実装するすべてのパブリック クラスを返します。(b) は抽象ではなく、(c) は "Controller" で終わる名前を持ちます。

アクションの選択

コントローラーを選択すると、フレームワークは IHttpActionSelector.SelectAction メソッドを呼び出してアクションを選択します。 このメソッドは HttpControllerContext を受け取り、HttpActionDescriptor を返します。

既定の実装は、ApiControllerActionSelector クラスによって提供されます。 アクションを選択するには、次の内容を確認します。

  • 要求の HTTP メソッド。
  • ルート テンプレート内の "{action}" プレースホルダー (存在する場合)。
  • コントローラー上のアクションのパラメーター。

選択アルゴリズムを見る前に、コントローラーアクションについて理解しておく必要があります。

コントローラー上のどのメソッドが "アクション" と見なされますか? アクションを選択すると、フレームワークはコントローラー上のパブリック インスタンス メソッドのみを参照します。 また、"特殊な名前" メソッド (コンストラクター、イベント、演算子のオーバーロードなど) と、 ApiController クラスから継承されたメソッドも除外されます。

HTTP メソッド。 フレームワークは、要求の HTTP メソッドに一致するアクションのみを選択します。次のように決定されます。

  1. Http メソッドは、AcceptVerbsHttpDeleteHttpGetHttpHeadHttpOptionsHttpPatchHttpPostHttpPut のいずれかの属性で指定できます。
  2. それ以外の場合、コントローラー メソッドの名前が "Get"、"Post"、"Put"、"Delete"、"Head"、"Options"、または "Patch" で始まる場合、規則によってアクションはその HTTP メソッドをサポートします。
  3. 上記のいずれにも該当しない場合、 メソッドは POST をサポートします。

パラメーター バインド。 パラメーター バインドは、Web API がパラメーターの値を作成する方法です。 パラメーター バインドの既定の規則を次に示します。

  • 単純型は URI から取得されます。
  • 複合型は要求本文から取得されます。

単純型には、すべての .NET Framework プリミティブ型に加えて、DateTimeDecimalGuidStringTimeSpan が含まれます。 各アクションについて、最大 1 つのパラメーターで要求本文を読み取ることができます。

Note

既定のバインド規則をオーバーライドできます。 内部での WebAPI パラメーターのバインドを参照してください。

その背景を持つアクション選択アルゴリズムを次に示します。

  1. HTTP 要求メソッドに一致するすべてのアクションの一覧をコントローラーに作成します。

  2. ルート ディクショナリに "action" エントリがある場合は、名前がこの値と一致しないアクションを削除します。

  3. 次のように、アクション パラメーターを URI と照合してみてください。

    1. 各アクションについて、単純な型であるパラメーターの一覧を取得します。ここで、バインドは URI からパラメーターを取得します。 省略可能なパラメーターを除外します。
    2. この一覧から、ルート ディクショナリまたは URI クエリ文字列で、各パラメーター名の一致を見つけようとします。 一致は大文字と小文字が区別されず、パラメーターの順序に依存しません。
    3. リスト内のすべてのパラメーターが URI で一致するアクションを選択します。
    4. 1 つ以上のアクションがこれらの条件を満たす場合は、パラメーターが最も一致するものを選択します。
  4. [NonAction] 属性を持つアクションは無視します。

手順 3 は、おそらく最も混乱を招くものです。 基本的な考え方は、パラメーターは URI、要求本文、またはカスタム バインドから値を取得できるということです。 URI から取得されるパラメーターの場合は、パス (ルート ディクショナリ経由) またはクエリ文字列内で、URI にそのパラメーターの値が実際に含まれていることを確認します。

たとえば、次のようなアクションについて考えてみてください。

public void Get(int id)

id パラメーターは URI にバインドされます。 したがって、このアクションは、ルート ディクショナリまたはクエリ文字列内の "id" の値を含む URI にのみ一致できます。

省略可能なパラメーターは省略可能であるため、例外です。 省略可能なパラメーターの場合は、バインドが 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" という名前のルートと一致します。 ルート ディクショナリには、次のエントリが含まれています。

  • controller: "products"
  • id: "1"

ルート ディクショナリには、クエリ文字列パラメーター "version" と "details" は含まれていませんが、これらはアクションの選択時に引き続き考慮されます。

コントローラーの選択

ルート ディクショナリの "コントローラー" エントリから、コントローラーの種類は ProductsController です。

アクションの選択

HTTP 要求は GET 要求です。 GET をサポートするコントローラー アクションは、GetAllGetById、および FindProductsByName です。 ルート ディクショナリには "action" のエントリが含まれていないため、アクション名と一致する必要はありません。

次に、GET アクションのみを確認して、アクションのパラメーター名を照合します。

アクション 一致するパラメーター
GetAll なし
GetById "ID"
FindProductsByName "name"

version パラメーターは省略可能なパラメーター GetById であるため、考慮されないことに注意してください。

このメソッドは GetAll 単純に一致します。 ルート ディクショナリに GetById "id" が含まれているため、GetById メソッドも一致します。 FindProductsByName メソッドが一致しません。

GetById メソッドは 1 つのパラメーターに一致し、GetAll のパラメーターは一致しないため、優先されます。 メソッドは、次のパラメーター値を使用して呼び出されます。

  • id = 1
  • バージョン = 1.5

選択アルゴリズムで バージョン が使用されていない場合でも、パラメーターの値は URI クエリ文字列から取得されます。

拡張ポイント

Web API には、ルーティング プロセスの一部の部分に拡張ポイントが用意されています。

Interface 説明
IHttpControllerSelector コントローラーを選択します。
IHttpControllerTypeResolver コントローラーの種類の一覧を取得します。 DefaultHttpControllerSelector は、この一覧からコントローラーの種類を選択します。
IAssembliesResolver プロジェクト アセンブリの一覧を取得します。 IHttpControllerTypeResolver インターフェイスは、このリストを使用してコントローラーの種類を検索します。
IHttpControllerActivator 新しいコントローラー インスタンスを作成します。
IHttpActionSelector アクションを選択します。
IHttpActionInvoker アクションを呼び出します。

これらのインターフェイスに独自の実装を提供するには、HttpConfiguration オブジェクトの Services コレクションを使用します。

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