WCFでカスタムシリアライザを使う

※今回の記事は、「池上彰の学べるニュース」風にお届けします。

池上「WCF では、オペレーションコントラクトの定義で記述した型しかやりとりできません。派生クラスは渡せません。WCF 既定の DataContractSerializer が、派生クラスをシリアライズできないためです。」

土田「WCF では派生クラスを、どうやっても渡せないんですか?」

池上「いえ、そんなことはありません。どうしても派生クラスを渡したいという人のために、方法が一応用意されています。」

池上「ServiceKnownTypeAttribute や KnownTypeAttribute を使って、このクラスの派生クラスはこれとこれですよ、とあらかじめ明記しておけばいいんです。これで派生クラスを渡すことができます。ただし、この方法だと派生クラスがたくさんあるときに大変面倒ですけどね。」

ひとり「DataContractSerialzier がシリアライズできないなら、代わりに自分でシリアライズしたらダメなんですか?」

池上「いい質問ですね〜。それも一つの方法です。カスタムシリアライザを作って、それを使うようにWCFを構成すれば、派生クラスも渡せるようになります。」

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml;

namespace SerializeSample
{
    // データコントラクトをバイナリにシリアライズする
    public class BinaryObjectSerializer : XmlObjectSerializer
    {
        // バイナリをメッセージに埋め込むとき使う要素名
        private const string BINARY_ELEMENT = "bin";

        public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
        {
            writer.WriteStartElement(BINARY_ELEMENT);
        }

        public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
        {
            // バイナリにシリアライズして、メッセージのボディに書き込む
            using (var stream = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(stream, graph);
                var buffer = stream.GetBuffer();
                writer.WriteBase64(buffer, 0, buffer.Length);
            }
        }

        public override void WriteEndObject(XmlDictionaryWriter writer)
        {
            writer.WriteEndElement();
        }

        public override bool IsStartObject(XmlDictionaryReader reader)
        {
            return reader.NodeType == XmlNodeType.Element && reader.Name == BINARY_ELEMENT;
        }

        public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
        {
            // メッセージのボディからデータを読み込んでデシリアライズ
            reader.ReadStartElement(BINARY_ELEMENT);
            try
            {
                using (var stream = new MemoryStream(reader.ReadContentAsBase64()))
                {
                    var formatter = new BinaryFormatter();
                    return formatter.Deserialize(stream);
                }
            }
            finally
            {
                reader.ReadEndElement();
            }
        }
    }

    // カスタムシライライザに差し替えるためのビヘイビア
    internal sealed class BinaryOperationBehavior : DataContractSerializerOperationBehavior
    {
        public BinaryOperationBehavior(OperationDescription operation)
            : base(operation)
        {
        }

        public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
        {
            return new BinaryObjectSerializer();
        }
    }

    // この属性を付けたオペレーションだけ、バイナリにシリアライズする
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class BinaryBehaviorAttribute : Attribute, IOperationBehavior
    {
        public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
        {
            IOperationBehavior innerBehavior = new BinaryOperationBehavior(operationDescription);
            innerBehavior.ApplyClientBehavior(operationDescription, clientOperation);
        }

        public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            IOperationBehavior innerBehavior = new BinaryOperationBehavior(operationDescription);
            innerBehavior.ApplyDispatchBehavior(operationDescription, dispatchOperation);
        }

        public void Validate(OperationDescription operationDescription)
        {
        }
    }

    [Serializable]
    public class Person
    {
        public string Name { get; set; }
    }

    [Serializable]
    public class Customer : Person
    {
        public string Code { get; set; }
    }

    [ServiceContract]
    public interface ITestService
    {
        // Echo メソッドはカスタムシリアライザを使う
        [BinaryBehavior]
        [OperationContract]
        Person Echo(Person data);
    }

    public class TestService : ITestService
    {
        public Person Echo(Person data)
        {
            // メッセージの内容をコンソールに出力
            Console.WriteLine(OperationContext.Current.RequestContext.RequestMessage);
            Console.WriteLine(data.Name);
            Console.WriteLine(((Customer)data).Code);
            return data;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string address = "net.pipe://localhost/Test";
            var host = new ServiceHost(typeof(TestService));
            host.AddServiceEndpoint(typeof(ITestService),
                new NetNamedPipeBinding(),
                address);
            host.Open();

            ITestService client = ChannelFactory<ITestService>.CreateChannel(
                new NetNamedPipeBinding(),
                new EndpointAddress(address));

            using (var scope = new OperationContextScope((IContextChannel)client))
            {
                var customer = new Customer()
                {
                    Code = "0001",
                    Name = "Foo"
                };
                var result = client.Echo(customer);
            }

            Console.ReadLine();
            ((IClientChannel)client).Close();
            host.Close();
        }
    }
}

池上「これは、バイナリにシリアライズするカスタムシリアライザのサンプルです。これを実行したのが次の画面になります。」

f:id:griefworker:20100802201953j:image

池上「見て下さい。WCF サービスに派生クラスを渡してもエラーは発生していませんね。で囲まれている部分が、シリアライズした結果です。」

ひとり「先生、なんかデータ量が多くなっていませんか?」

池上「いいところに気付きましたね〜。WCF ではシリアライズしたデータをメッセージのボディに埋め込む必要があります。埋め込むために、バイナリデータを Base64 エンコードで文字列に変換するので、結果的にデータ量が増えてしまうんです。」

土田「これでもしWCFのデータ転送量の制限に引っ掛かってしまったら本末転倒ですよね。」

池上「その通りです。カスタムシリアライザを作るなら、ここをしっかり作り込む必要があります。このサンプルみたいに、手抜きは禁物というわけです。」