SharePoint アドイン製品一覧
SharePoint 2010 開発のステップ・バイ・ステップ
Windows Azure 入門
Windows Azure How-To 集
WCF / WF 入門
環境 :Visual Studio 2010
REST サービス / Web Api の実践
ここでは、応用的なテーマをとりあげます。基本的な構築手順については、「REST サービスの作成」 (WCF の場合)、または 「Getting Started with ASP.NET Web Api」 (ASP.NET の場合) を参照してください。(2012/02 変更 : 「WCF Web Api」は、今後、「ASP.NET Web API」としてリリース予定です。)
こんにちは。
今回は、WCF REST サービスで、同時実行性をどのように制御すべきか、といった点を再整理してみます。
Web の世界における同時実行性の制御
HTTP に忠実な REST の方式では、処理は stateless を前提としているため、Pessimistic ではなく、Optimistic な Concurrency 制御 (すなわち、楽観的同時実行制御) が好まれます。実際、W3C では、こうした制御のための仕様を既に策定しており、HTML/1.1 では、以前、こちら でも説明した ETag (Entity Tag の略)、If-Match などの HTTP ヘッダーを使ってこれらを制御するよう定めています。(したがって、OData などでも、この仕様を使って同時実行制御を解決します。)例えば、あるコンテンツを参照した際の ETag ヘッダーの値が "1" で、2 回目に参照した際にも ETag ヘッダーの値が "1" の場合には、そのコンテンツは「変更されていない」ということを意味しています。(ETag はクォートされた文字列であれば良く、 "1"、"2" などのバージョン番号を表す文字列や、"aaaa-aaaa-aaaa-aaaa" 形式のような GUID 形式の文字列でも構いません。)
補足 : ETag には strong と weak があり、Strong ETag は、コンテンツが変更されない前提で、コンテンツのすべての内容 (Header、Body) が同一であることを意味します。一方、Weak ETag は、同じ URI でコンテンツが変更される前提で (例えば、Web 上の参照カウンターなど)、そのコンテンツが、(内容としてまったく一致していなくても) 意味的に同一であることを意図しています。Weak ETag は、W/"1" のように、W/ のプレフィックスが付与されます。なお、SharePoint などでは、この Weak ETag が採用されているので、アプリケーション構築などの際には注意してください。
この ETag の値を使用して、下記の HTTP ヘッダーを使用した Request を送信することで、条件付きの抽出や、条件付きの更新処理を要求することができます。もちろん、今回テーマとしている同時実行制御の目的でも使用することができます。 (下記のヘッダーも、HTTP/1.1 の仕様です。)
なお、上記で、確認の結果、処理されなかった場合は、その内容に応じたステータス コード (HTTP の StatusCode) を返します。例えば、If-None-Match ヘッダーを使用したリクエストで、ETag が一致していて結果 (Response の Body) を返さなかった場合は、ステータス コード 304 (Not Modified) を返すのが礼儀です。
補足 : 以前、こちら でも説明しましたが、If-Match ヘッダーで「*」(アスタリスク) を指定した場合は、「Any」を意味し、上記のようなチェックをおこなわず、常に Match したものとして処理されます。
補足 : なお、ここでは詳細を述べませんが、Web Api で同時実行を制御する他の手段として、OData の Batch Update を活用する方法もあります。Batch Update は、MIME の Multipart によって複数のアイテムを同時に処理する手法で、OData の仕様に従えば、これは順序の保障や、トランザクションを管理するためものではなく、あくまでも、 throughput を向上させるための機能です。しかし、サービス側の実装によっては、この 1 つの Batch Update を 1 トランザクションとして処理するように実装することもできます。(つまり、複数の更新を 1 つのまとまりとして、short-term での一貫性を保持する目的で使用できます。) どのように処理するかは、サービスの実装に依存します。
Web Api における実装
さて、以上は、単に、W3C (HTTP Specification) の仕様の話ですが、Web Api (REST サービス) で、こうした制御を実装するにはどうすれば良いでしょうか ?
基本的には、上記の仕様に沿って、開発者自身が処理する (コードを記述する) 必要があります。つまり、上記で、「・・・のように処理します」、「・・・されます」と記載していますが、こうした仕様に準じたサービスの振る舞いを実装するのは、サービスを構築する開発者の皆さん自身です。(Web サーバーなどが自動化してくれるわけではありません。)
ただし、ASP.NET Web API WCF Web Api を使用している場合には、「REST サービス (Web Api) における IoC (関心事の分割)」で解説する方法を使って、ビジネス ロジックとシステム ロジックなどを分離して構築できるので、こうした手法を活用して、ETag の処理を、再利用性の高い形で分離して組み合わせると良いでしょう。(ここでは、このサンプル コードの紹介は省略します。)
ご参考 : WCF REST サービスで使える ヘルパー関数
また、WCF を使用している場合には、いくつかのヘルパー メソッドが提供されていますので、必要に応じ、適宜、利用して頂くと良いでしょう。(以下のメソッドは、ASP.NET Web Api WCF Web Api では使用できません。)
補足 : これらのヘルパー メソッドについて、下記でサンプル コードを使って解説しますが、詳細は「MSDN : Conditional Get / Conditional Put」に書かれています。
例えば、PUT メソッドで、下記太字の通り CheckConditionalUpdate メソッドを使用すると、If-Match ヘッダーが "2" の場合、以降の処理が実行されます。もし、If-Match ヘッダーが "2" 以外の場合には、ここで WebFaultException が発生し、ブラウザー側にはステータス コード 412 (Precondition Failed) が返されます。(以降の処理はおこなわれません。)
補足 : このため、デバッグ実行をおこなうと、例外が発生してコードの実行が停止しますので注意してください。ステータス コードを確認するには、デバッグなしでサービスを開始し、Fiddler や、WCF Web API の Test Client などのクライアントで確認すると良いでしょう。(2012/02 追記 : ASP.NET Web Api では、Test Client は提供されていませんので注意してください。)
. . .[ServiceContract]public class Service1{ [OperationContract] [WebInvoke(Method = "PUT", UriTemplate = "orders/{id}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Bare)] public void UpdateOne(OrderItem item, string id) { WebOperationContext.Current.IncomingRequest.CheckConditionalUpdate("2"); // 更新処理を実行 (コードは省略) . . . }}public class OrderItem{ public int Id; public DateTime Time;}. . .
実行結果は、下記の通りです。なお、If-Match で * (アスタリスク) を指定しても、ちゃんと合格 (StatusCode 200) します。
[実行結果 1]
PUT http://localhost:34198/Service1.svc/orders/3 HTTP/1.1User-Agent: FiddlerHost: localhost:34198Accept: application/jsonIf-Match: "2"Content-Type: application/jsonContent-Length: 46{"Id":3,"Time":"\/Date(1317282271654+0900)\/"}
HTTP/1.1 200 OK. . .
[実行結果 2]
PUT http://localhost:34198/Service1.svc/orders/3 HTTP/1.1User-Agent: FiddlerHost: localhost:34198Accept: application/jsonIf-Match: "1"Content-Type: application/jsonContent-Length: 46{"Id":3,"Time":"\/Date(1317282271654+0900)\/"}
HTTP/1.1 412 Precondition FailedETag: "1". . .
補足 : 上記のサンプルでは、成功時に 200 を返していますが、現実の開発では、201 など、状況に応じた正しいステータス コードを返すようにしてください。(上記は、サンプルです。)
同様に、GET の際には、CheckConditionalRetrieve メソッドを使用して、If-None-Match ヘッダーに応じた例外を処理できます。例えば、GET メソッドで下記の通り記述すると、If-None-Match ヘッダーが "2" の場合に WebFaultException が発生し、ブラウザー側にはステータス コード 304 (Not Modified) が返されます。(それ以外の場合は、GET に成功し、Json の Body が返されます。)
. . .[OperationContract][WebGet(UriTemplate = "orders/{id}", ResponseFormat = WebMessageFormat.Json)]public OrderItem GetOne(string id){ WebOperationContext.Current.IncomingRequest.CheckConditionalRetrieve("2"); WebOperationContext.Current.OutgoingResponse.SetETag("2"); return new OrderItem { Id = int.Parse(id), Time = TimeZoneInfo.ConvertTimeBySystemTimeZoneId( DateTime.UtcNow, "Tokyo Standard Time") };}. . .
GET http://localhost:34198/Service1.svc/orders/3 HTTP/1.1Accept: */*If-None-Match: "1"Host: localhost:34198
HTTP/1.1 200 OKContent-Length: 46ETag: "2"Cache-Control: privateContent-Type: application/json; charset=utf-8. . .{"Id":3,"Time":"\/Date(1317286342655+0900)\/"}
GET http://localhost:34198/Service1.svc/orders/3 HTTP/1.1Accept: */*If-None-Match: "2"Host: localhost:34198
HTTP/1.1 304 Not ModifiedETag: "2". . .
なお、サービス構築の際、ETag の情報 (バージョン情報) をどのように管理するか、という点も課題となるでしょう。アプリケーション側で独自に管理しても良いですが、SQL Server を使用している場合は、rowversion 型 (ただし、バイナリー データなので、エンコードなどをおこなってください) や timestamp 型なども使用できますので、使用するアプリケーションの内容に応じて、適切な管理をおこなって頂くと良いでしょう。
Web の世界は、契約に基づく世界です。WCF Data Services や SharePoint などを使用した場合は、こうした処理はツールが自動化してくれますが、WCF では「処理の自由度」が高い分、こうした細かなマナーも開発者自身が意識して実装する必要があります。仕様に忠実な (クリーンで、礼儀正しい) RESTful サービスを、是非、志してください。