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


WCF Data Servicesの新機能、今回は6)カスタムプロバイダPart2をご紹介します。(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)の記事を参照してください。
■名称の変更
■新バージョン
■更新モジュール(開発環境)

■今回の主旨
前回からのカスタムプロバイダの作成の続きを行います。作成しているのはWord2007形式のdocxをWCF Data Servicesでデータの取得を可能にするプロバイダです。前回はmetadataの取得ができるところまで作成しました。今回はデータをクエリすることころを目指しましょう。

■カスタムプロバイダ (データの提供)
データを提供することを考えた場合、はじめに対応するべきは、やはりIDataServiceQueryProviderの箇所です。(Boldが追加&変更箇所)

データのIDataServiceQueryProvider のメソッドGetQueryRootForResourceSetとGetResourceTypeの実装が必要になります。特にGetQueryRootForResourceSetでは名前通りルートでのリクエストに対してデータを返す必要があります。currentDataSource はデータコンテキストなので、その中にデータを返却するメソッドを作成して、それを利用しています。また、メタデータ情報が必要なので、それもメンバーとして内部で保持できるように変更を行いました。

public class MyQueryProvider<T> : IDataServiceQueryProvider where T : MyDataProvider.DataContext.MyContext
{
    T currentDataSource;
    IDataServiceMetadataProvider serviceMetadata;

    public MyQueryProvider(IDataServiceMetadataProvider metadata)
    {
        serviceMetadata = metadata;
    }

    public object CurrentDataSource
    {
        get { return currentDataSource; }
        set { currentDataSource = value as T; }
    }

    public IQueryable GetQueryRootForResourceSet(ResourceSet resourceSet)
    {
        return currentDataSource.GetQueryable(resourceSet);
    }

    public ResourceType GetResourceType(object target)
    {
        Type type = target.GetType();
        return serviceMetadata.Types.Single(t => t.InstanceType == type);
 
}

    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();
    }
}

上記のMyQueryProvider内でメタデータの情報が必要であるので、MyDataServiceを変更します。コンストラクタでMyQueryProviderに対して、メタデータ情報を渡すようにしています。更に、初期データの作成が必要なためDataService<T>のCreateDataSource メソッドをオーバーライドして独自データを作成します。このプロバイダはWordデータを扱う予定でしたので、ここで書いたメソッドCreateResourceData の中で、実際のWordドキュメントのアクセスを行います。

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;
    }
    protected override T CreateDataSource()
    {
        T ct = new T();
        ct.CreateResourceData();
        return ct;
    }

}

と言うわけで、データコンテキストも変更します。
上記の変更を考慮するとデータコンテキストは以下の2点の修正が必要になります。
・GetQueryable メソッドの実装
・CreateResourceData メソッドの実装

これらは、where句で指定されたクラス(MyDataProvider.DataContext.MyContext,)の中になければ上記のコードが成立しませんので、まずはこの抽象クラスを修正します。(Bold部分)

public abstract class MyContext
{
    public abstract IQueryable GetQueryable(ResourceSet set);
    public abstract ResourceType CreateResourceType();
    public abstract void CreateResourceData();
}

当然、このクラスを継承したデータコンテキスト本体の実装が必要ですから、以下のように修正します。

public class WordTextContext : MyContext
{
    const string docxPath = @"c:\Demo.docx";

    private List<WordText> wordText = new List<WordText>();
    public List<WordText> WordText
    {
        get { return wordText; }
    }
    public override IQueryable GetQueryable(ResourceSet set)
    {
        if (set.Name == "WordText")
            return WordText.AsQueryable();

        throw new NotSupportedException(string.Format("{0} not found", set.Name));
    }

    public override void CreateResourceData()
    {
        WordprocessingDocument wordDoc = WordprocessingDocument.Open(docxPath, false);

        int index = 0; 
        wordText = (from para in wordDoc
                                                   .MainDocumentPart
                                                   .Document
                                                   .Body
                                                   .Elements<Paragraph>()
                             select new WordText
                             {
                                 Id = index++,
                                 InnerText = para.InnerText,
                                 InnerXML = para.InnerXml
                             }).ToList<WordText>();
    }

    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;
    }
}

上記のコードでWordファイルの扱いには、OpenXML SDK 2.0 を使用しています。(LINQ を使用しているところです)

http://www.microsoft.com/downloads/details.aspx?FamilyID=c6e744e5-36e9-45f5-8d8c-331df206e0d0&DisplayLang=en

こちらについては以前の記事などを参考にしてみてください。
Open XML Format SDK 2.0 (CTP)
Open XML Format SDK 2.0 (CTP) =続き=
Open XML Format SDK 2.0 (CTP) =続き3=

そのため、上記のコードには、下記のusingディレクティブを記述してあります。

using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;

そうそう、忘れてはいけないそもそものエンティティクラスは以下のように定義しました。
Idには通し番号、InnerTextには文章そのもの、InnerXMLは実際のXMLElementsを表すようにしたいと思います。

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

これで変更は終わりです。早速テストしてみましょう。c:\Demo.docx がクエリ対象のWordファイルなので適当に用意します。(今回は下記ですが、なんでも結構です。)

image

下記のようにルートのデータアクセスでは・・・
http://localhost:2015/WcfDataService1.svc/WordText

なにやら沢山でてきましたね・・・

image

普通にクエリーが可能です。例えば5行目のInnerTextだけ取得(射影)する場合なら・・・http://localhost:2015/WcfDataService1.svc/WordText(4)?$select=InnerText

image

こんな具合です。(ちゃんと5行目が表示されてますよね)

もちろん、クライアントとして.NET Frameworkを使用したプログラミングでも使用することが可能です。
試しにWPFのプロジェクトを新規で追加してサービス参照の追加を行ってみました。
(前回適当に入力したEntityContainerが、激しくかっこ悪いですね・・・)

image

コードは以下のようにしてみました。
(ここでもカッコ悪さを引きずってしまう・・・)

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    ServiceReference1.こだか dc =
     new ServiceReference1.こだか(
         new Uri("http://localhost:2015/WcfDataService1.svc"));

    var res = (from w in dc.WordText
               orderby w.Id
               select new
               {
                   w.Id,
                   w.InnerText
               }
               ).ToList();

    lstBox.ItemsSource = res;
}

実行結果です。Wordドキュメント⇒WCF Data Services ⇒WPFクライアントという順序でデータが渡ってきています。

image

と言った形で、カスタムデータプロバイダを作成することが可能です。今回は静的なクラスを用いましたが、動的に解決する手法も存在しますし、まだまだ奥が深い分野です。
作ってみて思いましたが、適当に名前を付けると後々まで引きずってかっこ悪いことが分かりました(笑)

こちらのプロジェクトは以下になります。

http://cid-f1df6ad9b682ecf0.skydrive.live.com/self.aspx/2010^_TechDays/MyDataProvider^_2.zip


Skip to main content