miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET Web API 2 Attribute Routing

~ 2013/10/23 リリースバージョン対応版に更新しました。 ~

リリースノート]によると ASP.NET Web API 2 では、Attribute Routing、OData(サポートの追加)、CORS、IHttpActionResult、AuthenticationFilters、Filter overrides、OWIN、等が追加されたようです。
今回は、そのうちの1つ、Attribute Routing ―属性ベースのルーティングについて触ってみました。

参考サイト:Attribute Routing in MVC and Web API
(Attribute Routing は、ASP.NET MVC 5 にも含まれています。)

Hello World

まずは簡単に試してみます。
Visual Studio 2013 の胸熱なASP.NET テンプレートから、ASP.NET Web API を選択します。

App_Start フォルダにある WebApiConfig.cs に config.MapHttpAttributeRoutes();
が記述されていることを確認します。 
これにより、Attribute Routing が有効になります。

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

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

次に、Controllers フォルダにある ValuesController.cs の Get() メソッドの上に、
以下のように Route 属性を追加します。

public class ValuesController : ApiController
{
    [Route("api/values/hello")]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

この Route 属性クラスの引数には、マッピングさせたい URL を定義します。
ここでは、「~/api/values/hello」を指定しました。

以上で、設定は完了です。
デバッグを実行した後、ブラウザ、または Fiddler で「api/values/hello」に GET でアクセスします。

"value1" "value2" の値が返ってくることが確認できます。

今までのルーティング設定との併用について

Route 属性で設定した場合は、Route 属性で指定した URL のみマッピングが有効になります。

例えば先のコードにて、デフォルトのルーティング設定に沿う形で「~/api/values」に GET でアクセスしても、マッピングは成功しません

public class ValuesController : ApiController
{
    // GET ~/api/values/hello でマッピングされる。
    // GET ~/api/values は マッピングされずエラーとなる。

    [Route("api/values/hello")]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

Attribute Routing の Route 属性を付与したアクションメソッドには、今までのルーティング設定( WebApiConfig.cs の config.Routes.MapHttpRoute()で設定する方法)でマッピングすることができません。

ただ、下のように Route 属性と今までのルーティング設定を併用することは可能です。

public class ValuesController : ApiController
{
	[Route("test")]
	public int Get() {}

	public void Post() {}
}

この場合、GET ~/test でアクセスすると、Get() メソッドが、
POST ~/api/values でアクセスすると、Post() メソッドが呼び出されます。
(GET ~/api/values でアクセスするとエラーになります。)

記事 Attribute Routing in Web API 2 では、今までのルーティング設定方法を convention-based(規則ベース)と呼んでいるので、ここでも同じように convention-based と呼ぶことにします。(Web API 2 で追加されたルーティング設定方法は、このまま Attribute Routing と呼ぶことにします。)

HTTP メソッドについて

Route 属性でルーティング設定を行ったとしても、HTTP メソッドによるアクションメソッドの選択方法は今までと変わりありません。

例えば下のように、Find() メソッドを定義して Route 属性を付与したとしても、
GET ~/test では、Find() メソッドにマッピングされません。

public class ValuesController : ApiController
{
	// GET ~/test ではマッピングされない。
	[Route("test")]
	public string Find()
	{
		return "message";
	}
}

HTTP メソッドによるマッピング規約に従い、HTTP メソッドの名前から始まる必要があります。
メソッド名を例えば GetMessage() にすると、マッピングされます。

public class ValuesController : ApiController
{
	// GET ~/test でマッピングされる。
	[Route("test")]
	public string GetMessage()
	{
		return "message";
	}
}

または、HttpGet 属性や HttpPost 属性でもマッピング可能になります。

public class ValuesController : ApiController
{
	// GET ~/test でマッピングされる。
	//   メソッド名は、Find だけど、HttpGet 属性がついているのでOK。
	[Route("test")]
	[HttpGet]
	public string Find()
	{
		return "message";
	}
}

ルーティング設定における Route 属性は、あくまでも URL を設定しているのであって、
HTTP メソッドは、今まで通りの規約に従う必要がある、ということです。

階層構造のリソース

階層構造であるリソースを、「~/api/categories/3/items/1」といった RESTful API では一般的な形式である URL にて定義してみます。

例として、Customers は Orders を持つ、というよくある親子関係のリソースを使い、
GET 「~/api/customers/{customerId}/orders」と
GET 「~/api/customers/{customerId}/orders/{orderId}」の2つのAPIを用意してみました。

API コントローラは以下のようなコードになります。

public class OrdersController : ApiController
{
    //  ~/api/customers/{customerId}/orders
    public IEnumerable<Order> GetOrdersByCustomer(int customerId) { }

    //  ~/api/customers/{customerId}/orders/{orderId}
    public Order GetOrderByCustomer(int customerId, int orderId) { }
}

Attribute Routing と、convention-based による設定方法を比較してみます。

Attribute Routing の場合

Attribute Routing では、今までの convention-based と同じように{}でパラメータを指定できます。

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

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

URLの{customerId}や{orderId}に対応する値が、引数にバインドされます。

convention-based の場合

ちなみに、convention-based の場合で対応する場合は、WebApiConfig.cs にルーティング設定を追加します。

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

		config.Routes.MapHttpRoute(
			name: "GetOrder",
			routeTemplate: "api/customers/{customerId}/orders/{orderId}",
			defaults: new { controller = "orders", orderId = RouteParameter.Optional }
		);

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

API コントローラ は、以下のようになります。

public class OrdersController : ApiController
{
	public IEnumerable<Order> GetOrdersByCustomer(int customerId) {}
	
	public Order GetOrderByCustomer(int customerId, int orderId) { }
}

ルーティング設定情報が、convention-based では、WebApiConfig.cs に1ヶ所に記述されるのに対し、Attribute Routing では、各コントローラの各メソッドと複数の箇所に記述されることになります。

Route Prefix

先ほどの2つの API の URL は、どちらも「api/customers/{customerId}..」で始まります。
メソッド毎に定義していましたが、コントローラ毎にまとめて URL のプレフィックスを定義することができます。

以下の用にコントローラクラスに RoutePrefix 属性をつけることで可能になります。

[RoutePrefix("api/customers/{customerId}")]
public class OrdersController : ApiController
{
    //  ~/api/customers/{customerId}/orders
    [Route("orders")]
    public IEnumerable<Order> GetOrdersByCustomer(int customerId) { }

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

パラメータもプレフィックスとして定義できるので、Orders コントローラのように、親のリソース識別子 customerId のパラメータが常に必要になる場合は、とても有効な使い方になります。
もし、convention-based で定義すると、WebApiConfig.cs でたくさんルーティング設定を記述する必要があります。

パラメータの制約

Attribute Routing は様々なパラメータの制約の方法が用意されています。

[Route("api/getData/{number:int}")]
public string GetData(int number) { }

このように指定すると、「~/api/getData/stringValue」のように int 以外の値が指定された URL はマッピングされません。
この他「{x:bool}」や「{x:length(1,20)}」などの指定の仕方があります。
[Route("api/values/{number=1}")]でデフォルト値も指定できます。
Attribute Routing in MVC and Web API に一覧が載っています。

複数のマッピング

[Route("say")]
[Route("api/students/hello")]
public String Hello()
{
   return "Hello, Web API 2 !";
}

このように複数の属性で指定すると、「~/say/」「~/api/students/hello」のどちらのURLでもマッピングされます。

ヘルプページ、RouteDebuggerにも対応

嬉しいことに、Attrribute Routingでのルーティング設定は、ヘルプページの自動生成や、RouteDebugger にも反映されます!

さっきの Customers-Orders の2つの API の例を利用した場合です。
convention-based と Attribute Routing の両方の設定が生きていることが確認できます。


RouteDebugger を使って試すこともできます。

RouteDeugger については、前の記事 ASP.NET Web API の Route Debugger が凄かった をご参考下さい。

※RouteDebugger を Nuget で取り込んだ場合、バージョン関係のエラーがでました。
Area/RouteDebugger/Views/Web.config の先頭を、下のようにバージョン3を指定することで動きました。

<configSections>   
	<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">      
		<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />      
		<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />    
	</sectionGroup>  
</configSections>

おしまい

ルーティング設定は、WebApiConfig.cs に1つにまとめるべきか、それとも各エンドポイントの傍に設定するか…。または両方を駆使するか…。迷いそうです。
ただ、パラメータの制約方法や、階層構造のリソースの場合は、Attribute Routing が便利そうです。