ASP.NET Web API 2 での属性ルーティング

ルーティングは、Web API が URI をアクションに一致させる方法です。 Web API 2 では、属性ルーティングと呼ばれる新しい種類のルーティングがサポートされています。 名前が示すように、属性ルーティングではルートを定義するために属性が使用されます。 属性ルーティングを使用すると、Web API の URI をより詳細に制御できます。 たとえば、リソースの階層を記述する URI を簡単に作成できます。

規則ベースのルーティングと呼ばれる以前のスタイルのルーティングは、引き続き完全にサポートされています。 実際には、同じプロジェクトで両方の手法を組み合わせることができます。

このトピックでは、属性ルーティングを有効にする方法と、属性ルーティングのさまざまなオプションについて説明します。 属性ルーティングを使用するエンド ツー エンドのチュートリアルについては、「Web API 2 で属性ルーティングを使用して REST API を作成する」を参照してください。

前提条件

Visual Studio 2017 (Community、Professional、または Enterprise Edition)

または、NuGet パッケージ マネージャーを使用して、必要なパッケージをインストールします。 Visual Studio の [ツール] メニューから、[NuGet パッケージ マネージャー] を選択し、[パッケージ マネージャー コンソール] を選択します。 [パッケージ マネージャー コンソール] ウィンドウで、次のコマンドを入力します。

Install-Package Microsoft.AspNet.WebApi.WebHost

属性ルーティングを使用する理由

Web API の最初のリリースでは、規則ベースのルーティングが使用されました。 この種類のルーティングでは、基本的にパラメーター化された文字列である 1 つ以上のルート テンプレートを定義します。 フレームワークが要求を受け取ると、URI とルート テンプレートが照合されます。 規則ベースのルーティングの詳細については、「ASP.NET Web API でのルーティング」を参照してください。

規則ベースのルーティングの利点の 1 つは、テンプレートが 1 か所で定義され、ルーティング規則がすべてのコントローラーに一貫して適用される点です。 残念ながら、規則ベースのルーティングでは、RESTful API で一般的な特定の URI パターンをサポートするのが困難になります。 たとえば、リソースには多くの場合、子リソースが含まれます。たとえば、顧客には注文があり、映画には俳優が含まれ、書籍には作成者がいます。 これらの関係を反映する URI を作成するのは自然なことです。

/customers/1/orders

この種類の URI は、規則ベースのルーティングを使用して作成することは困難です。 実行することはできますが、コントローラーやリソースの種類が多い場合、結果は適切にスケーリングされません。

属性ルーティングでは、この URI のルートを定義するのは簡単です。 コントローラー アクションに属性を追加するだけです。

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

属性ルーティングによって容易になるその他のパターンを次に示します。

API のバージョン管理

この例では、"/api/v1/products" は "/api/v2/products" とは異なるコントローラーにルーティングされます。

/api/v1/products /api/v2/products

オーバーロードされた URI セグメント

この例では、"1" は注文番号ですが、"pending" はコレクションにマップされます。

/orders/1 /orders/pending

複数のパラメーター型

この例では、"1" は注文番号ですが、"2013/06/16" は日付を指定します。

/orders/1 /orders/2013/06/16

属性ルーティングの有効化

属性ルーティングを有効にするには、構成中に MapHttpAttributeRoutes を呼び出します。 この拡張メソッドは、System.Web.Http.HttpConfigurationExtensions クラスで定義されています。

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

属性ルーティングは、規則ベースのルーティングと組み合わせることができます。 規則ベースのルートを定義するには、MapHttpRoute メソッドを呼び出します。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

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

Web API の構成の詳細については、「ASP.NET Web API 2 の構成」を参照してください。

注: Web API 1 からの移行

Web API 2 より前は、Web API プロジェクト テンプレートによって次のようなコードが生成されました。

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

属性ルーティングが有効になっている場合、このコードは例外をスローします。 属性ルーティングを使用するように既存の Web API プロジェクトをアップグレードする場合は、この構成コードを次のように更新してください。

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Note

詳細については、「ASP.NET ホスティングを使用した Web API の構成」を参照してください。

ルート属性の追加

属性を使用して定義されたルートの例を次に示します。

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

文字列 "customers/{customerId}/orders" はルートの URI テンプレートです。 Web API は、要求 URI とテンプレートの照合を試みます。 この例では、"customers" と "orders" はリテラル セグメントであり、"{customerId}" は変数パラメーターです。 次の URI はこのテンプレートと一致します。

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

このトピックで後述する制約を使用して、照合を制限できます。

ルート テンプレートの "{customerId}" パラメーターが、メソッド内の customerId パラメーターの名前と一致していることに注意してください。 Web API がコントローラー アクションを呼び出すと、ルート パラメーターのバインドが試みられます。 たとえば、URI が http://example.com/customers/1/orders の場合、Web API はアクションの customerId パラメーターに値 "1" をバインドしようとします。

URI テンプレートには、いくつかのパラメーターを指定できます。

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

ルート属性を持たないコントローラー メソッドは、規則ベースのルーティングを使用します。 そうすることで、同じプロジェクトで両方の種類のルーティングを組み合わせることができます。

HTTP メソッド

Web API では、要求の HTTP メソッド (GET、POST など) に基づいてアクションも選択されます。 既定では、Web API は、コントローラー メソッド名の先頭で、大文字と小文字が区別されない一致を検索します。 たとえば、PutCustomers という名前のコントローラー メソッドが HTTP PUT 要求と一致するとします。

この規則をオーバーライドするには、次のいずれかの属性を使用してメソッドを修飾します。

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

次の例では、Web API によって CreateBook メソッドが HTTP POST 要求にマップされます。

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

標準以外のメソッドを含む他のすべての HTTP メソッドでは、HTTP メソッドの一覧を受け取る AcceptVerbs 属性を使用します。

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

ルート プレフィックス

多くの場合、コントローラー内のルートはすべて同じプレフィックスで始まります。 次に例を示します。

public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }

    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }

    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}

[RoutePrefix] 属性を使用して、コントローラー全体に共通のプレフィックスを設定できます。

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }

    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }

    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}

メソッド属性でチルダ (~) を使用して、ルート プレフィックスをオーバーライドします。

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

ルート プレフィックスには、パラメーターを含めることができます。

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

ルート制約

ルート制約を使用すると、ルート テンプレート内のパラメーターの照合方法を制限できます。 一般的な構文は "{parameter:constraint}" です。 次に例を示します。

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

ここでは、URI の "id" セグメントが整数の場合にのみ、最初のルートが選択されます。 それ以外の場合は、2 番目のルートが選択されます。

次の表に、サポートされている制約の一覧を示します。

制約 説明
alpha 大文字または小文字のラテン アルファベット文字 (a - z、A - Z) と一致します。 {x:alpha}
[bool] ブール値と一致します。 {x:bool}
datetime DateTime 値と一致します。 {x:datetime}
小数 10 進値と一致します。 {x:decimal}
倍精度浮動小数点 64 ビットの浮動小数点値と一致します。 {x:double}
float 32 ビットの浮動小数点値に一致します。 {x:float}
guid GUID 値と一致します。 {x:guid}
int 32 ビットの整数値に一致します。 {x:int}
length 指定した長さの文字列、または指定した長さの範囲内の文字列に一致します。 {x:length(6)} {x:length(1,20)}
long 64 ビットの整数値に一致します。 {x:long}
最大 最大値を持つ整数に一致します。 {x:max(10)}
maxlength 最大長の文字列に一致します。 {x:maxlength(10)}
min 最小値を持つ整数に一致します。 {x:min(10)}
minlength 最小長の文字列に一致します。 {x:minlength(10)}
範囲 値の範囲内の整数に一致します。 {x:range(10,50)}
regex 正規表現に一致します。 {x:regex(^\d{3}-\d{3}-\d{4}$)}

"min" などの一部の制約は、かっこで囲まれた引数を取ります。 1 つのパラメーターに複数の制約をコロンで区切って適用できます。

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

カスタム ルート制約

IHttpRouteConstraint インターフェイスを実装することで、カスタム ルート制約を作成できます。 たとえば、次の制約では、パラメーターを 0 以外の整数値に制限します。

public class NonZeroConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            long longValue;
            if (value is long)
            {
                longValue = (long)value;
                return longValue != 0;
            }

            string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (Int64.TryParse(valueString, NumberStyles.Integer, 
                CultureInfo.InvariantCulture, out longValue))
            {
                return longValue != 0;
            }
        }
        return false;
    }
}

次のコードは、制約を登録する方法を示しています。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

これで、ルートに制約を適用できます。

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

IInlineConstraintResolver インターフェイスを実装することで、DefaultInlineConstraintResolver クラス全体を置き換えることもできます。 これにより、IInlineConstraintResolver の実装によって明示的に追加されない限り、すべての組み込み制約が置き換えられます。

省略可能な URI パラメーターと既定値

ルート パラメーターに疑問符を追加することで、URI パラメーターを省略可能にすることができます。 ルート パラメーターが省略可能な場合は、メソッド パラメーターの既定値を定義する必要があります。

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

この例では、/api/books/locale/1033/api/books/locale は同じリソースを返します。

または、次のように、ルート テンプレート内で既定値を指定することもできます。

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

これは前の例とほぼ同じですが、既定値を適用した場合の動作に若干の違いがあります。

  • 最初の例の ("{lcid:int?}") では、既定値の 1033 がメソッド パラメーターに直接割り当てられるため、パラメーターにはこの正確な値が割り当てられます。
  • 2 番目の例の ("{lcid:int=1033}") では、既定値の "1033" がモデルバインド プロセスを通過します。 既定のモデルバインダーは、"1033" を数値 1033 に変換します。 ただし、カスタム モデル バインダーをプラグインすることもできます。これは、別の処理を行う可能性があります。

(ほとんどの場合、パイプラインにカスタム モデル バインダーがない限り、2 つのフォームは同等になります)。

ルート名

Web API では、すべてのルートに名前があります。 ルート名は、HTTP 応答にリンクを含めることができるため、リンクを生成するのに役立ちます。

ルート名を指定するには、属性に Name プロパティを設定します。 次の例は、ルート名を設定する方法と、リンクを生成するときにルート名を使用する方法を示しています。

public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }

    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)

        var response = Request.CreateResponse(HttpStatusCode.Created);

        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

ルートの順序

URI とルートの照合を試みるときに、フレームワークはルートを特定の順序で評価します。 順序を指定するには、ルート属性に Order プロパティを設定します。 低い値が最初に評価されます。 既定の順序値は 0 です。

合計順序の決定方法を次に示します。

  1. ルート属性の Order プロパティを比較します。

  2. ルート テンプレート内の各 URI セグメントを確認します。 セグメントごとに、次のように順序を指定します。

    1. リテラル セグメント。
    2. 制約のあるルート パラメーター。
    3. 制約のないルート パラメーター。
    4. 制約のあるワイルドカード パラメーター セグメント。
    5. 制約のないワイルドカード パラメーター セグメント。
  3. 順序が同じになる場合、ルートの順序は、ルート テンプレートの大文字と小文字を区別しない序数文字列比較 (OrdinalIgnoreCase) によって決まります。

次に例を示します。 次のコントローラーを定義するとします。

[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }

    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }

    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }

    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }

    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}

これらのルートの順序は次のようになります。

  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending

"details" はリテラル セグメントであり、"{id}" の前に表示されますが、Order プロパティが 1 であるため、"pending" が最後に表示されます。 (この例では、"details" または "pending" という名前の顧客がないことを前提としています。一般に、あいまいなルートは避けるようにしてください。この例では、GetByCustomer のより適切なルート テンプレートは "customers/{customerName}" です)。