WCFでメッセージのサイズを取得する方法

WCF は送受信できるメッセージサイズの制限が厳しいので、実際のメッセージがどれくらいのサイズなのか、デバッグ時にチェックしたいと思った人は多いんじゃないでしょうか。


WCF には、TraceViewer やパフォーマンスカウンタが用意されていて、膨大な情報を取得できます。最初はこれらのツールに期待しましたが、私が調べた限りでは、肝心のメッセージサイズは取得できませんでした。Microsoft Network Monitor みたいなスニファを使って通信を監視すればサイズが分かるみたいですが、これまた取得できる情報が多すぎて、目的のデータを見つけ出すのが大変です。


そんなわけで、メッセージをトレースするビヘイビア作ってみました。WCF には MessageInspector という、メッセージを検査したり操作したいときに使える仕組みがあるので、これを利用してメッセージのトレースを出力します。オペレーション名も出力したのに取得できるプロパティやメソッドが無かったので、LINQ to XML 使ってむりやりメッセージのボディから取り出しています。

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml;
using System.Xml.Linq;

namespace InspectMessageSample
{
    // WCF のメッセージを操作できる仕組みを利用して、トレースを出力する。
    // このクラスはサービス側で利用される。
    internal class TraceMessageInspector : IDispatchMessageInspector
    {
        private readonly string _contractName;
        private readonly EndpointAddress _endpointAddress;

        public TraceMessageInspector(string contractName, EndpointAddress endpointAddress)
        {
            _contractName = contractName;
            _endpointAddress = endpointAddress;
        }

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            DumpMessageSize(ref request);
            return null;
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            DumpMessageSize(ref reply);
        }

        // メッセージのサイズを出力する
        private void DumpMessageSize(ref Message msg)
        {
            using (MessageBuffer buffer = msg.CreateBufferedCopy(int.MaxValue))
            using (MemoryStream stream = new MemoryStream())
            using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(stream))
            {
                Message copy = buffer.CreateMessage();
                copy.WriteMessage(writer);
                writer.Flush();

                // とりあえずコンソールに出力。
                // 実際に使うときは、ファイルに出力したほうがいいね。
                Console.WriteLine("時間          : {0}", DateTime.Now);
                Console.WriteLine("アドレス      : {0}", _endpointAddress);
                Console.WriteLine("コントラクト  : {0}", _contractName);
                Console.WriteLine("オペレーション: {0}", GetOperationName(copy));
                Console.WriteLine("サイズ        : {0}", stream.Position);

                // バッファーからコピーを作成して差し替える。
                // CreateBufferedCopy でバッファに格納してしまったので、
                // 差し替えないと WCF 側でメッセージを処理できない。
                msg = buffer.CreateMessage();
            }
        }

        // メッセージからオペレーション名を抽出する
        private static string GetOperationName(Message request)
        {
            XDocument document = XDocument.Parse(request.ToString());
            string name = "{http://www.w3.org/2003/05/soap-envelope}Body";
            XElement body = document.Descendants(name).FirstOrDefault();
            if (null != body)
            {
                // Body の最初の要素からオペレーション名を取得
                XElement op = body.Elements().FirstOrDefault();
                if (null != op)
                {
                    return op.Name.LocalName;
                }
            }
            return string.Empty;
        }
    }

    // カスタム MessageInspector を適用するためのビヘイビア
    [AttributeUsage(AttributeTargets.Class)]
    public class TraceMessageBehaviorAttribute : Attribute, IServiceBehavior
    {
        public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
            {
                foreach (var endpointDispatcher in channelDispatcher.Endpoints)
                {
                    endpointDispatcher.DispatchRuntime.MessageInspectors.Add(
                        new TraceMessageInspector(
                            endpointDispatcher.ContractName,
                            endpointDispatcher.EndpointAddress));
                }
            }
        }

        public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
        }
    }

    [ServiceContract]
    public interface IGreetingService
    {
        [OperationContract]
        string Greet(string name);
    }

    // 送受信するメッセージをトレースする
    [TraceMessageBehavior]
    public class GreetingService : IGreetingService
    {
        public string Greet(string name)
        {
            return string.Format("Hello, {0}.", name);
        }
    }

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

            IGreetingService client = ChannelFactory<IGreetingService>.CreateChannel(
                new NetNamedPipeBinding(),
                new EndpointAddress(address));
            string result = client.Greet("Negi");
            Console.WriteLine(result);

            Console.WriteLine("Enter:終了");
            Console.ReadLine();
            ((IChannel)client).Close();
            host.Close();
        }
    }
}

f:id:griefworker:20100809191358j:image