松崎 剛 Blog

This Blog's theme : エンタープライズ開発 (Server side)、Office サーバ開発

WCF Data Services 5.0 新機能 (OData 3.0 対応)

WCF Data Services 5.0 新機能 (OData 3.0 対応)

  • Comments 0

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>
    <serviceHostingEnvironment
        aspNetCompatibilityEnabled="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.Persons
            where c.Age > 20
            select 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.Persons
            where c is Student
            select 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 > 2
            select 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.1
User-Agent: Fiddler
Content-Type: application/json;odata=verbose
Accept: application/json
Prefer: return-no-content
Host: kkdeveva11:81
Content-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 の最大の魅力です。
是非、使いたおしてみてください !

 

関連ナンバー

 

Leave a Comment
  • Please add 6 and 7 and type the answer here:
  • Post