2014/03/22 記載 :
WCF Data Services では OData v4.0 以降はサポートされない予定です。これからはじめられる方は、ASP.NET Web API の使用を検討してください。(詳細は「OData Team Blog : OData core libraries now support OData v4」を参照してください。)
こんにちは。
もう 1 週間以上前ですが、WCF Data Services チーム (Astoria チーム) のブログでも紹介されているように、これまで CTP として出ていた WCF Data Services の次期バージョンが、いよいよリリースされました。
[ダウンロード センター] WCF Data Services 5.0 for OData v3
http://www.microsoft.com/downloads/ja-jp/details.aspx?FamilyID=5b1a903e-59e2-46e8-b63b-a9421bbb6bf9
WCF Data Services 5.0 では、OData の最新仕様である OData v3 に対応しています。今週は、その魅力 (いくつかは、OData v3 の魅力でもありますが) を、ざっと復習してみたいと思います。
(いつの間にか各週のブログになってしまいましたが、暇をみて、マメに書くようにします . . .)
補足 (2012/09 追記) : Windows Store Apps (Windows 8 Modern UI) 用の WCF Data Services Tools (OData v3 対応) は、ダウンロード センターの WCF Data Services Tools for Windows Store Apps で公開されています。
DbContext のサポート
実は、これが一番うれしいです . . . (私は)
DataService のソースとして、今回から DbContext が使用できます。Code First で構築された DbContext が、このバージョンから、簡単に WCF Data Services を使って公開できるわけです。
では、簡単に確認してみましょう。
補足 : なお、Entity Framework の Code First が扱えるように、あらかじめ、NuGet などで EntityFramwork 4.1 以降をインストールしておいてください。(ASP.NET MVC 3 以降では、既にプロジェクトに含まれています。)
例えば、ASP.NET MVC アプリケーションを作成し、以下の通り、Person クラス、PersonContext クラスを作成します。
. . .using System.Data.Entity;. . .public class Person{public int SocialId { get; set; }public string Name { get; set; }public int Age { get; set; }}public class PersonContext : DbContext{public PersonContext() : base("MyPersonContext") { }protected override void OnModelCreating( DbModelBuilder modelBuilder){// describe how to create database. (This time, key setting)modelBuilder.Entity<Person>().HasKey(p => p.SocialId);}public DbSet<Person> Persons { get; set; }}// this is for debug ... (use CreateDatabaseIfNotExists in real apps)public class PersonDBCreate : DropCreateDatabaseAlways<PersonContext>{protected override void Seed(PersonContext context){base.Seed(context);// create initial data ...var p1 = new Person{SocialId = 1,Name = "Tsuyoshi Matsuzaki",Age = 43};context.Persons.Add(p1);context.SaveChanges();}}. . .
今回は、Global.asax の Application_Start で初期化をおこないます。(上記のクラス構成にあわせて、データベースが作成されます。)
. . .using System.Data.Entity;. . .protected void Application_Start(){. . .Database.SetInitializer(new PersonDBCreate());}
補足 : ここではさぼって書いていますが、上記のコードでは、データベースへのアクセス権限に注意してください。特に IIS では、アプリケーション プールの ID (アカウント) でデータベースを作成するため、何も設定しないと、データベース作成に失敗してエラーとなるでしょう。(また、データベース作成時のタイミングにも注意してください。上記の通り Application_Start に記入していますが、Web アプリケーション開始時ではなく、実際には、クエリーや操作の際などに初期のデータベースが作成されます。)
つぎに、プロジェクトに WCF Data Service を追加します。
Visual Studio 2012 を使用している方は、既定で WCF Data Services 5.0 が使用されるので、普通に、[追加] – [新しい項目] メニューで [WCF Data Services] のアイテムを追加してください。
Visual Studio 2010 を使用してアイテムを追加する場合は、普通に追加すると以前のバージョンの WCF Data Services のライブラリー (System.Data.Services.dll) が使用されてしまいます。このため、この場合には、WCF サービスを追加し、ここに WCF Data Services 用のコードをカスタムに作成していきます。
まず、プロジェクトに [WCF サービス] (Service1.svc ファイル) を追加します。WCF Data Services では、DataService クラス、DataServiceHostFactory クラスを使用して必要な処理をおこなうので、Web.config に特別な設定は必要ありません。このため、Web.config を開き、下記 (太字) の通り、煩雑な構成を削除しておいてください。
<configuration>. . . <system.serviceModel><serviceHostingEnvironmentaspNetCompatibilityEnabled="true" /> </system.serviceModel></configuration>
また、WCF Data Services では、内部で WCF の WebHttp を使用しているため、System.ServiceModel.Web.dll を参照追加しておきます。
さらに、WCF Data Services 5.0 のコアのライブラリーである %programfiles%\Microsoft WCF Data Services\5.0\bin\.NETFramework\Microsoft.Data.Services.dll、%programfiles%\Microsoft WCF Data Services\5.0\bin\.NETFramework\Microsoft.Data.Services.Client.dll を参照追加します。
Service1.svc のマークアップを表示し、下記 (太字) の通り、WCF Data Services 5.0 の DataServiceHostFactory を設定します。
<%@ ServiceHost Language="C#" Service="MvcApplication1.Service1" CodeBehind="Service1.svc.cs" Factory="System.Data.Services.DataServiceHostFactory, Microsoft.Data.Services, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
なお、IService1.svc は使いませんので削除してください。
さて、準備が完了したら、サービスのコード ファイル (今回は、Service1.svc.cs とします) に、下記の通り実装します。
ここが、上記で説明したポイントです ! 下記の通り、WCF Data Services 5.0 では、DataService の Generic (データのソース) として、DbContext (上記の PersonContext) が設定できます。
. . .using System.Data.Services;using System.Data.Services.Common;. . .public class Service1 : DataService<PersonContext>{public static void InitializeService(DataServiceConfiguration config){config.SetEntitySetAccessRule("Persons", EntitySetRights.All);config.DataServiceBehavior.MaxProtocolVersion =DataServiceProtocolVersion.V3;// Set true for debugging//config.UseVerboseErrors = false;}}
さいごに、ASP.NET MVC アプリケーションの場合、Routing が有効になっていると思うので、App_Start/RouteConfig.cs (ASP.NET MVC 4 の場合) に下記 (太字) の通り追記してください。
. . .public static void RegisterRoutes(RouteCollection routes){ routes.IgnoreRoute("Service1.svc/{*pathInfo}"); . . .}. . .
以上でサービスは完成です。
クライアント側は、これまでと同様です。(例えば、ブラウザーを使って http://localhost/MvcApplication4/Service1.svc/Persons に接続すると、結果が返ってきます。) Visual Studio 2012 を使用している方は、これまで通り、[サービス参照の追加] (Add Service Reference) を実行すれば充分です。
Visual Studio 2010 を使って .NET でクライアントを作成する場合は、%programfiles%\Microsoft WCF Data Services\5.0\bin\.NETFramework\Microsoft.Data.Services.Client.dll を参照追加し、下記の通り、WCF Data Services 5.0 に付属している DataSvcUtil.exe を使用します。(この際、必ず、/version:3.0 のオプションは忘れないでください。何も指定しないと、既定では、2.0 で作成されます。)
“%programfiles%\Microsoft WCF Data Services\5.0\bin\.NETFramework\DataSvcUtil.exe” /version:3.0 /out:C:\Demo\ConsoleApplication1\ConsoleApplication1\Reference.cs /uri:http://localhost:81/MvcApplication4/Service1.svc
クライアントのプログラム コードは下記の通りです。(特に、いつもと変わりありません。)
. . .static void Main(string[] args){Uri svcUri = new Uri("http://localhost:81/MvcApplication4/Service1.svc",UriKind.Absolute);PersonContext context = new PersonContext(svcUri);var q = from c in context.Personswhere c.Age > 20select c;foreach (var i in q)Console.WriteLine(i.Name);Console.ReadLine();}. . .
Vocabularies (annotation)
OData v3 に沿って、メタデータ ($metadata で取得できる情報) に、使用するエンティティの付帯情報 (例えば、表示方法や、値の範囲、など) を記述できます。
実際の設定方法は、Astoria Team の以下のブログで丁寧に説明されていますので、是非参考にしてください。(ここでは、手順の説明を省略します。)
http://blogs.msdn.com/b/astoriateam/archive/2011/10/13/vocabularies-in-wcf-data-services.aspx
以前、SharePoint 2010 の REST サービス (Web Api) では、厳密には、Entity 名がリスト名 (ListName) と一致していないと説明しましたが (このため、REST を使って、リスト名を取得することができません)、こうした問題も、この annotation を使うと解決できます。(もちろん、SharePoint 2010 の場合は OData 2.0 ベースなので、現時点では使えませんが . . .)
Action
賛否両論あろうかと思いますが、OData に CRUD 以外の操作 (例えば、チェックアウト、など) を提供可能にするための 新しい仕様 です。
CRUD 操作は、そのまま HTTP の Verb (POST / GET / PUT / DELETE など) に対応していますが、Action は URI で表現します。また、POST メソッドを使って、その操作の付帯情報も渡すことができます。
こちらも、WCF Data Services Team (Astoria Team) のブログで Action の構築方法などを詳しく紹介していますので、下記を参考にしてください。(長くなるので、ここでは割愛します。)
http://blogs.msdn.com/b/astoriateam/archive/2012/04/10/actions-in-wcf-data-services-part-1-service-author-code.aspx
http://blogs.msdn.com/b/astoriateam/archive/2012/04/11/actions-in-wcf-data-services-part2-how-idataserviceactionprovider-works.aspx
http://blogs.msdn.com/b/astoriateam/archive/2012/04/12/actions-in-wcf-data-services-part-3-a-sample-provider-for-the-entity-framework.aspx
Inheritance と Collection (Bag)
継承関係のサポートも充実しています。
例えば、上記で作成したサービスで、下記 (太字) の通り、Person から継承された Student クラスを追加してみましょう。
public class Person{. . .}public class Student : Person{public int Grade { get; set; }}public class PersonContext : DbContext{. . .public DbSet<Person> Persons { get; set; }public DbSet<Student> Students { get; set; }}public class PersonDBCreate : DropCreateDatabaseAlways<PersonContext>{protected override void Seed(PersonContext context){base.Seed(context);// create initial data ...var p1 = new Person{SocialId = 1,Name = "Tsuyoshi Matsuzaki",Age = 43};context.Persons.Add(p1);var s1 = new Student{SocialId = 2,Name = "Taro Demo",Age = 7,Grade = 2};context.Students.Add(s1);var s2 = new Student{SocialId = 3,Name = "Jiro Demo",Age = 9,Grade = 4};context.Students.Add(s2);var s3 = new Student{SocialId = 4,Name = "Saburo Demo",Age = 7,Grade = 2};context.Students.Add(s3);var s4 = new Student{SocialId = 5,Name = "Shiro Demo",Age = 7,Grade = 2};context.Students.Add(s4);context.SaveChanges();}}. . .
まず、クライアント側で、Student のみを取得したい場合 (上記の p1 以外を取得したい場合)、下記の通り記述できます。
[DataService クライアントの場合]
static void Main(string[] args){Uri svcUri = new Uri("http://kkdeveva11:81/MvcApplication4/Service1.svc",UriKind.Absolute);PersonContext context = new PersonContext(svcUri);var q = from c in context.Personswhere c is Studentselect c;foreach (var i in q)Console.WriteLine(i.Name);Console.ReadLine();}
[HTTP の場合]
GET http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons()?$filter=isof(‘MvcApplication4.Student’) HTTP/1.1
また、継承関係におけるクラス間の cast も可能です。下記では、Grade が 2 より上の Student のみを抽出しています。
[DataService クライアントの場合]
static void Main(string[] args){Uri svcUri = new Uri("http://kkdeveva11:81/MvcApplication4/Service1.svc",UriKind.Absolute);PersonContext context = new PersonContext(svcUri);var q = from c in context.Persons.OfType<Student>()where c.Grade > 2select c;foreach (var i in q)Console.WriteLine(i.Name);Console.ReadLine();}
[HTTP の場合]
GET http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons/MvcApplication4.Student()?$filter=Grade%20gt%202 HTTP/1.1
また、OData v3 では、Relation による 1 対多の関係以外に、Collection そのものを集合として handly に扱うための Bag という型 (Type) のプロパティ (Multi-valued property) が定義できます。(Bag を使うと、string の集合など primitive な Collection も扱えます。)
OData (Open Data Protocol) : Adding support for Bags
http://www.odata.org/blog/2010/9/27/adding-support-for-bags
新しい WCF Data Services では、この Bag (Multi-value property) もサポートしています。
ちなみに、Entity Framework を使用した場合、Collection は、Bag (Multi-valued property) ではなく、Relation (Navigation property) として設定されるので注意してください。($metadata を確認すると分かります。)
この Bag (Multi-valued property) は、現時点では、まだ、Entity Framework ではサポートされていないようですね。このため、現状では、サービス側でカスタムの Provider を作成した際などに、Bag の property が使用できます。(ライブラリーに、拡張のための専用のクラスが提供されています。)
補足 : WCF Data Services における multi-value property の使い方は非常に簡単です。下記の Astoria Team のブログに記載されていますので参考にしてください。(リレーションの扱い と同様、select に new しておく必要があるようです。)
http://blogs.msdn.com/b/astoriateam/archive/2010/11/11/introduction-to-multi-valued-properties.aspx
All / Any
OData では、All / Any の操作 が使用できます。例えば、以下のような場合です。
- 担当する すべて (All) の Student の Grade が 2 である Teacher を抽出 (検索)
- 担当する Student のうちのいずれか (Any) の Grade が 2 である Teacher を抽出 (検索)
この All / Any は、Relation (navigation property) と Bag (multi-valued property) の双方で使えます。
例えば、上記で作成したサービスに、今度は、下記 (太字) の通り Teacher クラスを追加してみましょう。(前述の通り、下記の Teacher と Student の関係は Relation として設定されます。)
. . .public class Person{. . .}public class Student : Person{public int Grade { get; set; }}public class Teacher : Person{public string ClassName { get; set; }public ICollection<Student> Students { get; set; }}public class PersonContext : DbContext{. . .public DbSet<Person> Persons { get; set; }public DbSet<Student> Students { get; set; }public DbSet<Teacher> Teachers { get; set; }}public class PersonDBCreate: DropCreateDatabaseAlways<PersonContext>{protected override void Seed(PersonContext context){base.Seed(context);// create initial data ...var p1 = new Person{SocialId = 1,Name = "Tsuyoshi Matsuzaki",Age = 43};context.Persons.Add(p1);var s1 = new Student{SocialId = 2,Name = "Taro Demo",Age = 7,Grade = 2};context.Students.Add(s1);var s2 = new Student{SocialId = 3,Name = "Jiro Demo",Age = 9,Grade = 4};context.Students.Add(s2);var s3 = new Student{SocialId = 4,Name = "Saburo Demo",Age = 7,Grade = 2};context.Students.Add(s3);var s4 = new Student{SocialId = 5,Name = "Shiro Demo",Age = 7,Grade = 2};context.Students.Add(s4);var t1 = new Teacher{SocialId = 6,Name = "Shinsuke Matsuzaki",Age = 70,ClassName = "C1",Students = new[] { s1, s2 }};context.Teachers.Add(t1);var t2 = new Teacher{SocialId = 7,Name = "Gosuke Matsuzaki",Age = 75,ClassName = "C2",Students = new[] { s3, s4 }};context.Teachers.Add(t2);context.SaveChanges();}}
この場合、担当する すべて (All) の Student の Grade が 2 である Teacher を検索する場合は、下記の通りです。今回の例では、結果として「Gosuke Matsuzaki」のみが出力されます。
[DataServices クライアントの場合]
static void Main(string[] args){Uri svcUri = new Uri("http://kkdeveva11:81/MvcApplication4/Service1.svc",UriKind.Absolute);PersonContext context = new PersonContext(svcUri);var q = from c in context.Persons.OfType<Teacher>()where c.Students.All(s => s.Grade == 2)select c;foreach (var i in q){Console.WriteLine(i.Name);}Console.ReadLine();}
[HTTP の場合]
GET http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons/MvcApplication4.Teacher()?$filter=Students/all(s:s/Grade%20eq%202) HTTP/1.1
担当する Student のいずれか (Any) の Grade が 2 である Teacher を検索する場合は、下記の通りです。今回の場合、結果として「Gosuke Matsuzaki」、「Shinsuke Matsuzaki」の 2 名が出力されます。
[DataServices クライアントの場合]
static void Main(string[] args){Uri svcUri = new Uri("http://kkdeveva11:81/MvcApplication4/Service1.svc",UriKind.Absolute);PersonContext context = new PersonContext(svcUri);var q = from c in context.Persons.OfType<Teacher>()where c.Students.Any(s => s.Grade == 2)select c;foreach (var i in q){Console.WriteLine(i.Name);}Console.ReadLine();}
[HTTP の場合]
GET http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons/MvcApplication4.Teacher()?$filter=Students/any(s:s/Grade%20eq%202) HTTP/1.1
Spatial データ
最新の SQL Server でもサポートされている Spatial Data が、OData / WCF Data Services を使った RESTful 呼び出しで使用できます。Spatial Data 用の操作 (例 : distance、lengthなど) も提供されており Query も可能です。
OData におけるSpatial Data の構成については、下記を参照してください。
http://www.odata.org/blog/2011/5/3/geospatial-data-support-in-odata
Prefer ヘッダー
WCF Data Services の既定の動作では、insert や update の際に結果 (更新後のデータ) を返しますが、例えば、この余計な Payload を抑えたい場合などに、HTTP の Prefer Header を使って制御できます。
Open Data Protocol (OData) Specification : Prefer
http://msdn.microsoft.com/en-us/library/hh537533(v=prot.10).aspx
例えば、結果の Body を返さないようにするには、クライアント側で、以下の通り記述します。
[DataServices クライアントの場合]
static void Main(string[] args){Uri svcUri = new Uri("http://kkdeveva11:81/MvcApplication4/Service1.svc",UriKind.Absolute);PersonContext context = new PersonContext(svcUri);Student s = new Student{SocialId = 8,Name = "Ichiro Demo",Age = 8,Grade = 3};context.AddAndUpdateResponsePreference = System.Data.Services.Client.DataServiceResponsePreference.NoContent;context.AddObject("Persons", s);context.SaveChanges();Console.ReadLine();}
[HTTP の場合]
POST http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons HTTP/1.1User-Agent: FiddlerContent-Type: application/json;odata=verboseAccept: application/jsonPrefer: return-no-contentHost: kkdeveva11:81Content-Length: 183{ "__metadata": { "uri": "http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons", "type": "MvcApplication4.Student" }, "SocialId":8, "Name":"Ichiro Demo", "Age":8, "Grade":3}
HTTP/1.1 204 No Content
Cache-Control: no-cache
Location: http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons(8)/MvcApplication4.Student
Server: Microsoft-IIS/7.5
X-Content-Type-Options: nosniff
Preference-Applied: return-no-content
DataServiceVersion: 3.0;
DataServiceId: http://kkdeveva11:81/MvcApplication4/Service1.svc/Persons(8)/MvcApplication4.Student
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 19 Apr 2012 13:56:22 GMT
補足 : OData v3 で json の呼出しをおこなう場合は、上記の通り Content-Type を指定する必要があるので注意してください。(application/json のみだと、Status 415 のエラーが返ってきます。)
Content-Type: application/json;odata=verbose
ちなみに、Astoria Team ブログに依ると、odata v3 の json light (content-type : application/json; odata=light) は、まだ入っていないようです。(json light では、metadata を簡略化することで、OData の情報交換における json の payload をさらに抑えることができます。)
PATCH メソッド
データ全体の変更 (入れ替え) ではなく、データの一部を変更 (他の属性は最新のデータのまま維持) したい場合、従来は MERGE のみを使用していましたが、PATCH Method も使用可能になります。(標準化の進捗などの観点から、ODataでは、これまで MERGE のみを採用していました。)
WCF Data Services クライアントから使用する際には、下記の通りオプションを指定します。
. . .context.SaveChanges( System.Data.Services.Client.SaveChangesOptions.PatchOnUpdate);. . .
この他に、%programfiles%\Microsoft WCF Data Services\5.0\bin\.NETFramework\Microsoft.Data.OData.dll、%programfiles%\Microsoft WCF Data Services\5.0\bin\.NETFramework\Microsoft.Data.Edm.dll のライブラリーが提供され、OData フォーマットの serialize / desirialize、validation 処理など、低レイヤーの処理を実装できます。(NuGet でも取得できます。また、Silverlight 用のライブラリーも提供されています。)
データ定義さえちゃんとしておけば、非常に多くの付加価値が付いた Web Api が手に入るというのが WCF Data Services の最大の魅力です。
是非、使いたおしてみてください !
関連ナンバー
- .NET 4 の WCF (including WCF Data Services, Workflow Services)
- .NET 4 の WF
- Windows Server AppFabric 1.0 (WCF / WF の拡張機能)
- .NET 4 Platform Update 1 の WF 新機能
- WCF 4.5 新機能
- WCF Data Services 5.0 新機能
- WF 4.5 新機能
Categories: Uncategorized
5 replies»