忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

WCFチュートリアル カスタム型を使う

WCFのコントラクトではそのままでは、引数や戻り値にカスタムなクラスを使うことが出来ません。クライアントからサーバーへ独自に定義したクラスを(そのままでは)送れませんし、サーバーからクライアントへ、独自に定義したクラスを(そのままでは)返せないのです。

× ダメなコントラクトの例(マネしないで!) ×
using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    //Error!
    //(throws InvalidDataContractException)
    public class Rectangle
    {
        public double Width;
        public double Height;
    }
}


もちろん全くムリということではありません。だいたいInt32だってクラス(構造体)の一つなんですからね。ある型をWCFでの引数や戻り値に使うためには、いくつかの条件のうちの一つを満たしている必要があります。そのうちの最も基本的なのは、System.Serializable属性が付けられていることでしょう。この属性は、Int32、Single(float)、String、DateTimeなど、.netの多くの型につけられています(ですからこれらの型はそのままWCFのコントラクトに使用することが出来ます)

SerializableAttributeをつける

カスタムクラスをWCFで使えるようにする方法のひとつがこれです。つまり、作ったカスタムクラスにSerializableAttributeを付けるのです。SerializableAttributeをつけられた型は、リフレクションによりprivateだろうとpublicだろうとすべてのフィールドがシリアライズされ、ネットの向こう側に送られます(もちろん同じパソコンに送られる場合もありますけどね)。さっきの例を正しくするとこうなります。

コントラクト:
using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    [System.Serializable]
    public class Rectangle
    {
        public double Width;
        public double Height;
    }
}

せっかくなのでサーバーとクライアントも書いておきます。サーバーのプロジェクトもクライアントのプロジェクトも、このコントラクトのプロジェクトを参照しています。

サーバー:
using System;
using System.ServiceModel;

using Contracts;

namespace Server
{
    class MyCalculator : ICalculator
    {
        public double GetArea(Rectangle rect)
        {
            return rect.Width * rect.Height;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost serviceHost = new ServiceHost(typeof(MyCalculator));
            serviceHost.AddServiceEndpoint(
                typeof(ICalculator),
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                );
            serviceHost.Open();

            Console.WriteLine("サービスがオープンしました。終了するにはEnterを押してください。");
            Console.ReadLine();

            serviceHost.Close();
        }
    }
}




クライアント:
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

using Contracts;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            ICalculator remoteService = new ChannelFactory(
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                ).CreateChannel();

            Console.WriteLine(
                "2 * 3 = "
                + remoteService.GetArea(new Rectangle{Width = 2, Height = 3})
                );

            ((IChannel)remoteService).Close();
        }
    }
}

実行するとクライアント側で"2 * 3 = 6"というふうに表示されるはずです。Rectangleオブジェクトがバイト列にシリアライズされ、サーバープログラムに送られているわけですね。

(Serializable属性を使う場合に限らず、このようにしてサービスとクライアントの間を飛び交うオブジェクトはちょうどC言語の構造体のようなものになります。つまり、メソッドを持ちませんし、値がそのままコピーされるのです。メソッドを持たないというのはどういうことかというと、データだけだということです。これは「データとその操作を一緒にまとめる」というオブジェクト指向に反しているように見えますが、WCFのサービスコントラクトが使う型はメソッドを持っていても仕方が無いですからね。また、サーバークライアント間を移動するときにデータが反対側でシリアライズされるという方法を取るので、片方のオブジェクトの値を変更してももう片方のオブジェクトには影響を与えません。参照型ではなくて値型みたいなもんなんですね。)


DataContractAttributeとDataMemberAttributeをつける

しかしこのSerializableAttributeを使う方法はWCFとしてはあまり勧められないのだそうです。WCFのうたうサービス志向にのっとっていないからでしょう。コントラクトはなるべく明示的であるべきということでしょうか・・・。それにまあ、SerializableAttributeはWCFより以前からあった属性ですしね。

WCF特有の方法には、DataContractAttributeDataMenberAttributeを使う方法があります。DataContractは型に付け、DataMemberはそのフィールドまたはプロパティに付けます。この2つは両方ともSystem.Runtime.Serialization名前空間内にあり、使うにはSystem.Runtime.Serialization.dllの参照を加える必要があります(意外ですね・・・・・・)。

さっきのSerializable属性を使った方法をこの方法で書き換えるとこうなります:

using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    [System.Runtime.Serialization.DataContract]
    public class Rectangle
    {
        [System.Runtime.Serialization.DataMember]
        public double Width;

        [System.Runtime.Serialization.DataMember]
        public double Height;
    }
}


さっきの方法と違うのはコントラクトだけです。サーバーとクライアントはそのまま。















拍手[0回]

PR