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 です。
合計順序の決定方法を次に示します。
ルート属性の Order プロパティを比較します。
ルート テンプレート内の各 URI セグメントを確認します。 セグメントごとに、次のように順序を指定します。
- リテラル セグメント。
- 制約のあるルート パラメーター。
- 制約のないルート パラメーター。
- 制約のあるワイルドカード パラメーター セグメント。
- 制約のないワイルドカード パラメーター セグメント。
順序が同じになる場合、ルートの順序は、ルート テンプレートの大文字と小文字を区別しない序数文字列比較 (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) { ... }
}
これらのルートの順序は次のようになります。
- orders/details
- orders/{id}
- orders/{customerName}
- orders/{*date}
- orders/pending
"details" はリテラル セグメントであり、"{id}" の前に表示されますが、Order プロパティが 1 であるため、"pending" が最後に表示されます。 (この例では、"details" または "pending" という名前の顧客がないことを前提としています。一般に、あいまいなルートは避けるようにしてください。この例では、GetByCustomer
のより適切なルート テンプレートは "customers/{customerName}" です)。