ASP.NET Web Api を検索 (Query) 可能にする (および、OData への対応)

(2012/03 : サンプル コードを、WCF Web Api から ASP.NET Web Api に変更)

(2012/06 : サンプル コードを、ASP.NET Web API RC 版にあわせて変更)

(2013/05 : OData 対応のサンプルを、古いものから最新の ASP.NET Web API にあわせて変更)

環境 : Visual Studio 2010, ASP.NET Web API RC (ASP.NET MVC 4 RC)

REST サービス / Web Api の実践

ここでは、応用的なテーマをとりあげます。基本的な構築手順については、「Getting Started with ASP.NET Web Api」 (ASP.NET の場合)、または「REST サービスの作成」 (WCF の場合) を参照してください。

こんにちは。

今回は、REST で返すデータを、クライアント側から検索 (Query) 可能にする方法について、具体的なサンプル コードを使って説明します。なお、今回も、前回同様、ASP.NET Web API を使います。

 

プロジェクトの準備

前回同様、準備として、まず、ASP.NET Web API を使用した簡単な REST サービスを構築しておきましょう。(この投稿では、詳細の手順は省略します。)

今回のサービスでは、コレクションを扱うようにしたいので、下記の通り、Contact クラスの List を返すようにします。

. . .

public class ContactController : ApiController
{
    private static List<Contact> contacts = new List<Contact>()
    {
        new Contact { Id = 1, Name = "Ichiro Matsuzaki", Rating = 3},
        new Contact { Id = 2, Name = "Jiro Matsuzaki", Rating = 1 },
        new Contact { Id = 3, Name = "Saburo Matsuzaki", Rating = 3 },
        new Contact { Id = 4, Name = "Ichiro Suzuki", Rating = 2 },
        new Contact { Id = 5, Name = "Jiro Suzuki", Rating = 1 }
    };

    // GET /api/contact
    public List<Contact> Get()
    {
        return contacts;
    }
}

public class Contact
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Rating { get; set; }
}
. . .

 

Query 可能な REST Services

上記のサービス (ContactController) を、クライアント側からクエリー可能となるように変更してみましょう。

ASP.NET Web API で、クエリーをサポートする方法は、驚くほど簡単です!
NuGet から Microsoft ASP.NET Web API for OData (microsoft.aspnet.webapi.odata) パッケージを取得して、上記のコードを下記太字の通り変更すれば完了です。

. . .

public class ContactController : ApiController
{
    . . .

    // GET /api/contact
    [Queryable]
 [System.Web.Http.OData.EnableQuery]
    public IQueryable<Contact> Get()
    {
        return contacts.AsQueryable();
    }
}
. . .

上記のコードを見ると、いったんデータを全件取得して、そのあとで絞り込むように動作するように思われますが、そうではありません。
Linq を使ったことがある方はご存じの通り、IQuerable を使うと、yield return により検索条件が遅延評価され、その結果が一覧として返されます。(上記の場合、List の Generic クラスが、内部で、そうした処理を実行しています。) クライアントにすべてのデータを転送してから検索をおこなうのとは異なり、無駄のない、最適なスループットを実現できます。

では、実際に動作を確認してみましょう。

まず、Id が 4 の Contact を取得してみましょう。下記で、eq は、Equal を意味しています。(下記で、%20 は、空白文字を URL エンコードした文字列です。以降も同様です。)

GET http://localhost:16826/api/contact?$filter=Id%20eq%204 HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 05 Mar 2012 06:59:01 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 44

[
  {"Id":4,"Name":"Ichiro Suzuki","Rating":2}
]

つぎに、Rating が 3 未満 (2 以下) の Contact の一覧を取得してみます。下記で、lt は、”Less Than” を意味しています。

GET http://localhost:16826/api/contact?$filter=Rating%20lt%203 HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 05 Mar 2012 07:04:12 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 129

[
  {"Id":2,"Name":"Jiro Matsuzaki","Rating":1},
  {"Id":4,"Name":"Ichiro Suzuki","Rating":2},
  {"Id":5,"Name":"Jiro Suzuki","Rating":1}
]

つぎに、Rating が 3 未満で、かつ、Name に “Matsu” という文字列が含まれているデータに絞り込んでみましょう。下記の通り、and や or を組み合わせることができます。

GET http://localhost:16826/api/contact?
  $filter=Rating%20lt%203%20and%20substringof('Matsu',Name)
  HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 05 Mar 2012 07:11:37 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 45

[
  {"Id":2,"Name":"Jiro Matsuzaki","Rating":1}
]

また、検索 (クエリー) だけでなく、ソートもおこなうことができます。
以下は、Rating でソートしています。

GET http://localhost:16826/api/contact?$orderby=Rating HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 05 Mar 2012 07:13:18 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 221

[
  {"Id":2,"Name":"Jiro Matsuzaki","Rating":1},
  {"Id":5,"Name":"Jiro Suzuki","Rating":1},
  {"Id":4,"Name":"Ichiro Suzuki","Rating":2},
  {"Id":1,"Name":"Ichiro Matsuzaki","Rating":3},
  {"Id":3,"Name":"Saburo Matsuzaki","Rating":3}
]

また、以下は、Rating でソートしたうちの、上位 3 件を取得しています。

GET http://localhost:16826/api/contact?$orderby=Rating&$top=3 HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 05 Mar 2012 07:14:38 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 129

[
  {"Id":2,"Name":"Jiro Matsuzaki","Rating":1},
  {"Id":5,"Name":"Jiro Suzuki","Rating":1},
  {"Id":4,"Name":"Ichiro Suzuki","Rating":2}
]

 

Microsoft のテクノロジーに詳しい方は、もうお分かりと思いますが、これらのクエリー オプション (上記の $filter、$orderby など) は、すべて、OData の仕様で定義されるクエリー オプションそのものです。(下記に記載されています。)

[MSDN] REST エンドポイントを使用する OData システム クエリ オプション

http://msdn.microsoft.com/ja-jp/library/gg309461.aspx

 

OData フォーマットへの対応 (2013/05 変更)

OData に準拠した、Full Customize の (つまり、WCF Data Services を使用しない) REST サービスも簡単に構築できます。

まず、準備として、ASP.NET and Web Tools 2012.2 Update 以降がインストールされていない場合には、NuGet で Microsoft.AspNet.WebApi.OData を取得します。

つぎに、上記のコードの ApiController (継承元) を、下記コードの通り EntitySetController に変更し、このクラスの override メソッドとして各メソッドを実装します。
なお、「ASP.NET : Create a Read-Only OData Endpoint with ASP.NET Web API」で紹介されているように、この EntitySetController<TEntity, TKey> クラスは、ApiController クラスから派生した ODataController の派生クラス (つまり、孫) です。

. . .
using System.Web.Http.OData;

public class ContactController : EntitySetController<Contact, int>
{
  . . .
  // GET /api/contact
  [System.Web.Http.OData.EnableQuery]
  public override IQueryable<Contact> Get()
  {
    return contacts.AsQueryable();
  }

  protected override Contact GetEntityByKey(int key)
  {
    return contacts.FirstOrDefault(p => p.Id == key);
  }
  . . .

さいごに、OData の応答を可能にするため、App_Statrt/WebApiConfig.cs に下記の通り追記します。
この設定により、この OData のサービスに、/odata/Contact のルート Uri でアクセスできるようになります。(なお、下記の「Contact」は、Controller の名前にあわせておいてください。)

. . .

public static void Register(HttpConfiguration config)
{
  . . .

  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  modelBuilder.EntitySet<WebApiSample.Controllers.Contact>("Contact");
  Microsoft.Data.Edm.IEdmModel model = modelBuilder.GetEdmModel();
  config.Routes.MapODataRoute("ODataRoute", "odata", model);
  . . .

このサービスにアクセスすると、下記の通り、メタデータを含んだ OData の Json フォーマットが返ってきます。(もちろん、上記のクエリーもすべて可能です。)

GET http://localhost:16826/odata/Contact HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 08 May 2013 02:53:33 GMT
Content-Length: 383

{
  "odata.metadata":"http://localhost:16826/odata/$metadata#Contact","value":[
    {
      "Id":1,"Name":"Ichiro Matsuzaki","Rating":3
    },{
      "Id":2,"Name":"Jiro Matsuzaki","Rating":1
    },{
      "Id":3,"Name":"Saburo Matsuzaki","Rating":3
    },{
      "Id":4,"Name":"Ichiro Suzuki","Rating":2
    },{
      "Id":5,"Name":"Jiro Suzuki","Rating":1
    }
  ]
}

エンベロープが長くなってしまいますが、もちろん、atom の OData フォーマットで取得することも可能です。

GET http://localhost:16826/odata/Contact HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
Accept: application/atom+xml
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atom+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 08 May 2013 02:59:25 GMT
Content-Length: 3968

<?xml version="1.0" encoding="utf-8"?>
<feed xml:base="http://localhost:16826/odata/" xmlns=. . .>
  <id>http://schemas.datacontract.org/2004/07/</id>
  <title />
  <updated>2013-05-08T02:59:25Z</updated>
  <link rel="self" href="http://localhost:16826/odata/Contact" />
  <entry>
    <id>http://localhost:16826/odata/Contact(1)</id>
    <category term="WebApiSample.Controllers.Contact" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <link rel="edit" href="http://localhost:16826/odata/Contact(1)" />
    <link rel="self" href="http://localhost:16826/odata/Contact(1)" />
    <title />
    <updated>2013-05-08T02:59:25Z</updated>
    <author>
      <name />
    </author>
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">1</d:Id>
        <d:Name>Ichiro Matsuzaki</d:Name>
        <d:Rating m:type="Edm.Int32">3</d:Rating>
      </m:properties>
    </content>
  </entry>
  <entry>
    <id>http://localhost:16826/odata/Contact(2)</id>
    <category term="WebApiSample.Controllers.Contact" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <link rel="edit" href="http://localhost:16826/odata/Contact(2)" />
    <link rel="self" href="http://localhost:16826/odata/Contact(2)" />
    <title />
    <updated>2013-05-08T02:59:25Z</updated>
    <author>
      <name />
    </author>
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">2</d:Id>
        <d:Name>Jiro Matsuzaki</d:Name>
        <d:Rating m:type="Edm.Int32">1</d:Rating>
      </m:properties>
    </content>
  </entry>
  <entry>
    <id>http://localhost:16826/odata/Contact(3)</id>
    <category term="WebApiSample.Controllers.Contact" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <link rel="edit" href="http://localhost:16826/odata/Contact(3)" />
    <link rel="self" href="http://localhost:16826/odata/Contact(3)" />
    <title />
    <updated>2013-05-08T02:59:25Z</updated>
    <author>
      <name />
    </author>
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">3</d:Id>
        <d:Name>Saburo Matsuzaki</d:Name>
        <d:Rating m:type="Edm.Int32">3</d:Rating>
      </m:properties>
    </content>
  </entry>
  <entry>
    <id>http://localhost:16826/odata/Contact(4)</id>
    <category term="WebApiSample.Controllers.Contact" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <link rel="edit" href="http://localhost:16826/odata/Contact(4)" />
    <link rel="self" href="http://localhost:16826/odata/Contact(4)" />
    <title />
    <updated>2013-05-08T02:59:25Z</updated>
    <author>
      <name />
    </author>
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">4</d:Id>
        <d:Name>Ichiro Suzuki</d:Name>
        <d:Rating m:type="Edm.Int32">2</d:Rating>
      </m:properties>
    </content>
  </entry>
  <entry>
    <id>http://localhost:16826/odata/Contact(5)</id>
    <category term="WebApiSample.Controllers.Contact" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <link rel="edit" href="http://localhost:16826/odata/Contact(5)" />
    <link rel="self" href="http://localhost:16826/odata/Contact(5)" />
    <title />
    <updated>2013-05-08T02:59:25Z</updated>
    <author>
      <name />
    </author>
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">5</d:Id>
        <d:Name>Jiro Suzuki</d:Name>
        <d:Rating m:type="Edm.Int32">1</d:Rating>
      </m:properties>
    </content>
  </entry>
</feed>

データ フォーマットだけでなく、OData 仕様の、括弧付きの問い合わせなどにも対応しています。

GET http://localhost:16826/odata/Contact(3) HTTP/1.1
User-Agent: Fiddler
Host: localhost:16826
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 08 May 2013 03:03:01 GMT
Content-Length: 126

{
  "odata.metadata":"http://localhost:16826/odata/$metadata#Contact/@Element",
  "Id":3,
  "Name":"Saburo Matsuzaki",
  "Rating":3
}

 

追記 :
WCF Data Services のような Model First (Data Driven) の ASP.NET Web API サービスを構築する際は、 Visual Studio 2013 から使用可能なスキャフォールディング (Scaffold) を使用してください。
スキャフォールディング (Scaffold) の使用方法については、「Visual Studio 2013 の Single Page Application (SPA) テンプレートを使った開発 (Knockout.js)」を参照してください。

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s