WCF Data Servicesの新機能 – カスタムプロバイダ1

WCF Data Servicesの新機能、今回は6)カスタムプロバイダをご紹介します。(2/24に実施されたTechDaysセッションのフォローアップも兼ねています)

このシリーズの目次は以下になります。
1) WCF Data Servicesの新機能 – 射影
2) WCF Data Servicesの新機能 – カウント
3) WCF Data Servicesの新機能 – Server Driven Paging(SDP)
4) WCF Data Servicesの新機能 – Feed Customization
5) WCF Data Servicesの新機能 – データバインド
6) WCF Data Servicesの新機能 – カスタムプロバイダ1
7) WCF Data Servicesの新機能 – カスタムプロバイダ2
8) WCF Data Servicesの新機能 – リクエストパイプライン
9)Open Data Protocolの実装 – Share Point Server 2010のデータを操作する

また以下のトピックに関しては1)の記事を参照してください。
■名称の変更
■新バージョン
■更新モジュール(開発環境)

■カスタムプロバイダ
WCF Data Servicesでは、以下の3種類のデータプロバイダの使用が可能です。

1)Entity Framework provider
リレーショナルデータをマップしたデータモデルの定義(EDM)を使用して、ADO.NET Entity Framework ベースのデータサービスを提供します。主にSQL Serverになりますが、もちろんEntity Frameworkに対応していれば、サードパーティ製のデータプロバイダも使用することが可能です。

2)Reflection provider
こちらは、リフレクションを使用します、既存のデータクラスを基に定義されたデータモデルの使用が可能になります。データのクエリにはIQueryableインターフェイスを用います。また、更新を可能にするにはIUpdatableインターフェイスの実装が必要です。

3)Custom Data Service Providers
これが新たに加わった手法です。動的にデータモデルの定義を可能にします。またデータプロバイダに関しての詳細な制御も可能になります。

■どのような時に使用するべきか?
色々な観点はあるのですが、
・データサービス(モデル)が今後も開発対象であり発展する可能性がある
・データモデルに関して細かく制御したい(ログ取得、公開しないメンバー、リネームなど)
・公開したいデータモデルに対して、必ずしも厳密な型定義ができない
・BlobストリームやServerDrivenPagingなどの新機能の実装を行いたい
などなどです。

■使用インターフェイス
以下を使用して機能を実装します。
- IDataServiceMetadataProvider
- IDataServiceQueryProvider
- IDataServiceUpdateProvider
- IDataServiceStreamProvider
- IDataServicePagingProvider

■カスタムプロバイダの作成(メタデータの提供)
これから、実際にカスタムプロバイダを作成してみたいと思います。お題目として、ちょっと迷ったのですが、普通に自分で考えたProductクラスとかを公開しても面白くないので、Word2007形式のdocxをWCF Data Servicesでデータの取得を可能にするプロバイダを作成することにします。ただ、記事が長くなるので、まず今回はmetadataの取得ができるところまでを目指しましょう。

カスタムプロバイダの作成には、上記のインターフェイスを用いるわけですが、もちろんデータサービスを作成する根本的なインターフェイスを用いる必要があります。IServiceProvider、DataService<T> の2つです。この2つのインターフェイスを継承したクラスを作成します。

IServiceProvider が持つGetServiceメソッドでは、クライアントから要求が有った場合に適切なインターフェイスをもつオブジェクトを返却しなくてはなりません。この適切なインターフェイスが上記の使用インターフェイスになるわけです。イメージとしては以下のような感じですね。IDataServiceMetadataProviderはサービスのメタデータを返し、IDataServiceQueryProviderは、データの取得要求にこたえます。最低この2つが必要です。

public object GetService(Type serviceType) {     if (serviceType == typeof(IDataServiceMetadataProvider))         return metadata;     else if (serviceType == typeof(IDataServiceQueryProvider))         return query;     else         return null; }

上記を踏まえてメインとなるクラスを記述したのが以下です。(事前にクラスライブラリのプロジェクトを新規作成して、参照の追加で、System.Data.Servicesを追加してください。)

DetaService<T> は、これまでEDMから得られるコンテキスト情報を型として渡していましたが、今回は自分で用意したクラスをデータコンテキストとして使用したいと思います。

また、コンストラクタでIDataServiceQueryProviderとIDataServiceMetadataProviderを継承したオブジェクトを実体化しています。(当然まだクラスの定義はありません)

加えてwhere以下は、内部で使用しているMyMetadataProvider(これから定義します)での指定に必要であるため、あえてつけて有ります。

public class MyDataService<T> : DataService<T>, IServiceProvider where T : MyDataProvider.DataContext.MyContext, new() {     public IDataServiceMetadataProvider metadata;     public IDataServiceQueryProvider query;

    public MyDataService()     {         metadata = new MyMetadataProvider<T>();         query = new MyQueryProvider<T>(metadata);     }

    public object GetService(Type serviceType)     {         if (serviceType == typeof(IDataServiceMetadataProvider))             return metadata;         else if (serviceType == typeof(IDataServiceQueryProvider))             return query;         else             return null;     }

}

上記のMyMetadataProviderを定義します。もちろんIDataServiceMetadataProvider を継承します。ここでは、ResourceSets とTypeプロバティの要求に対して、適切な値(Dictionary)を返す必要があります。実際この2種類の値を作成しているのがコンストラクタなのですが、Typeは実際のデータサービスのメタデータ情報(型情報など)になるので、データコンテキストの中で定義するべきかと思います。そのためMyContext抽象クラスを用意して、そこから定義されるメソッド(CreateResourceType)で行いたいと思います。といった理由から、インターフェイスの型に制限を設けました。(where以下です)

  加えてContainerName 、ContainerNamespace 辺りに適当な文字を入れています。後はお決まりだと思ってそのまま記述してください。

public class MyMetadataProvider<T> : IDataServiceMetadataProvider where T : MyDataProvider.DataContext.MyContext, new() {     private Dictionary<string, ResourceType> resourceTypes = new Dictionary<string, ResourceType>();     private Dictionary<string, ResourceSet> resourceSets = new Dictionary<string, ResourceSet>();

    public MyMetadataProvider()     {         //メタデータの作成         T ct = new T();         ResourceType RT = ct.CreateResourceType();         RT.SetReadOnly();         resourceTypes.Add(RT.FullName, RT);

        ResourceSet RS = new ResourceSet(RT.Name, RT);         RS.SetReadOnly();         resourceSets.Add(RS.Name, RS);     }

    public string ContainerName     {         get { return "こだか"; }     }

    public string ContainerNamespace     {         get { return "ネームすぺーす"; }     }

    public IEnumerable<ResourceType> Types     {         get { return this.resourceTypes.Values; }     }

    public IEnumerable<ResourceSet> ResourceSets     {         get { return this.resourceSets.Values; }     }

    public bool TryResolveResourceSet(string name, out ResourceSet resourceSet)     {         return resourceSets.TryGetValue(name, out resourceSet);     }

    public bool TryResolveResourceType(string name, out ResourceType resourceType)     {         return resourceTypes.TryGetValue(name, out resourceType);     }

    public bool TryResolveServiceOperation(string name, out ServiceOperation serviceOperation)     {         serviceOperation = null;         return false;     }

    public IEnumerable<ResourceType> GetDerivedTypes(ResourceType resourceType)     {         yield break;     }

    public ResourceAssociationSet GetResourceAssociationSet(ResourceSet resourceSet, ResourceType resourceType, ResourceProperty resourceProperty)     {         throw new NotImplementedException("No relationships.");     }

    public bool HasDerivedTypes(ResourceType resourceType)     {         return false;     }

    public IEnumerable<ServiceOperation> ServiceOperations     {         get { yield break; }     } }

次にMyQueryProviderを定義します。今回はデータの取得は行わないので、こちらもお決まりの記述になります。

public class MyQueryProvider<T> : IDataServiceQueryProvider where T : MyDataProvider.DataContext.MyContext {     T currentDataSource;     public object CurrentDataSource     {         get { return currentDataSource; }         set { currentDataSource = value as T; }     }

    public IQueryable GetQueryRootForResourceSet(ResourceSet resourceSet)     {       throw new NotImplementedException();     }

    public ResourceType GetResourceType(object target)     {         throw new NotImplementedException();     }

    public bool IsNullPropagationRequired     {         get { return true; }     }

    public object GetOpenPropertyValue(object target, string propertyName)     {         throw new NotImplementedException();     }

    public IEnumerable<KeyValuePair<string, object>> GetOpenPropertyValues(object target)     {         throw new NotImplementedException();     }

    public object GetPropertyValue(object target, ResourceProperty resourceProperty)     {         throw new NotImplementedException();     }

    public object InvokeServiceOperation(ServiceOperation serviceOperation, object[] parameters)     {         throw new NotImplementedException();     } }

そして、最後にデータコンテキストを用意します。
はじめに抽象クラスMyContextです。メタデータ用に関数を一つ(CreateResourceType)定義しています。これはMyMetadataProviderのコンストラクタで使用していました。

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Services.Providers;

namespace MyDataProvider.DataContext {     public abstract class MyContext     {         public abstract ResourceType CreateResourceType();     } }

今回公開するデータの基となるクラスです。今回は静的なクラスを使用します。
最終的にWordドキュメントをWCFDataServcesで公開したいので、そのためのメンバをもったクラス定義にしています。

public class WordText {       public int Id { get; set; }       public string InnerText { get; set; }       public string InnerXML { get; set; } }

以下が実際のデータコンテキスト本体です。ここでデータタイプの指定をしています。リフレクションプロバイダーの場合、属性をつけられたクラスそのものが公開されていたのですが、カスタムプロバイダの場合は以下のように細かい指定が可能です。(例えば公開したくないメンバがあっても問題ありません。)

  public class WordTextContext : MyContext   {       public override ResourceType CreateResourceType()       {           var productType = new ResourceType(typeof(WordText),                                                  ResourceTypeKind.EntityType,                                                  null,                                                  "Namespace",                                                  "WordText",                                                  false                                                   );           var Id = new ResourceProperty("Id",                                          ResourcePropertyKind.Key |                                          ResourcePropertyKind.Primitive,                                          ResourceType.GetPrimitiveResourceType(typeof(int))                                           );

          var InnerText = new ResourceProperty("InnerText",                                                  ResourcePropertyKind.Primitive,                                                  ResourceType.GetPrimitiveResourceType(typeof(string))                                                  );           var InnerXML = new ResourceProperty("InnerXML",                                                    ResourcePropertyKind.Primitive,                                                    ResourceType.GetPrimitiveResourceType(typeof(string))                                                 );

          productType.AddProperty(Id);           productType.AddProperty(InnerText);           productType.AddProperty(InnerXML);           return productType;       }   }

実際に使用してみましょう。WebApplicationを新規で作成して、これまで作成していてクラスライブラリを参照追加します。そして、WCF Data Servicesを追加して以下のコードを記述します。Boldの箇所が追加したプロバイダの指定ですね。

public class WcfDataService1 : MyDataProvider.MyDataService<MyDataProvider.DataContext.WordTextContext> {      public static void InitializeService(DataServiceConfiguration config)     {         config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);         config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;     } }

後は、実行してみます。当然ながら、まだデータクエリの部分は実装していないので、以下のようにメタデータの指定を行います。
https://localhost:2015/WcfDataService1.svc/$metadata

結果は以下のようになります。コード内で定義したさまざまな値が表示されているのが分かります。当然ながら、まだデータクエリの部分は実装していないのでエラーになります。

image

ここまで作成したものは以下です。
https://cid-f1df6ad9b682ecf0.skydrive.live.com/self.aspx/2010^_TechDays/MyDataProvider^_1.zip

次回はWordデータを取得するところまで作成したいと思います。