user1545748
user1545748

Reputation: 51

Getting WCF message body before deserialization

I am implementing WCF service that exposes a method whose [OperationContract] is [XmlSerializerFormat]. I sometimes get request whose body is not valid XML. In such cases I want to log the original body, so I can know why it didn't constitute valid XML. However, I can't get it from the Message object, see my attempts (by implementing IDispatchMessageInspector interface):

    public object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        request.ToString();  // "... Error reading body: System.Xml.XmlException: The data at the root level is invalid. Line 1, position 1. ..."
        request.WriteBody(...);   // Serialization Exception, also in WriteMessage and other Write* methods
        request.GetReaderAtBodyContents(...);   // Same
        HttpRequestMessageProperty httpRequest = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];  // no body in httpRequest
    }

When looking in watch, request.messageData appears to contain the body - but that's a private member.

How can I get the message buffer without trying to deserialize it?

Upvotes: 5

Views: 8053

Answers (3)

Heinzi
Heinzi

Reputation: 172200

For the opposite direction (I am writing a WCF client and the server returns invalid XML), I was able to extract the raw reply message in IClientMessageInspector.AfterReceiveReply by

  • accessing the internal MessageData property of reply via Reflection, and then
  • accessing its Buffer property, which is an ArraySegment<byte>.

Something similar might be available for the request message on the server side; so it might be worth examining the request variable in the debugger.


I'm aware that this is not exactly what you are asking for (since you are on the server side, not on the client side), and I'm also aware that using reflection is error-prone and ugly. But since the correct solution is prohibitively complex (see baur's answer for details) and this "raw dump" is usually only required for debugging, I'll share my code anyways, in case it is helpful to someone in the future. It works for me on .NET Framework 4.8.

public void AfterReceiveReply(ref Message reply, object correlationState)
{
    object messageData = reply.GetType()
        .GetProperty("MessageData", 
                     BindingFlags.NonPublic | BindingFlags.Instance)
        .GetValue(reply, null);

    var buffer = (ArraySegment<byte>)messageData.GetType()
        .GetProperty("Buffer")
        .GetValue(messageData, null);

    byte[] rawData = 
        buffer.Array.Skip(buffer.Offset).Take(buffer.Count).ToArray();

    // ... do something with rawData
}

And here's the full code of the EndpointBehavior:

public class WcfLogger : IEndpointBehavior
{
    public byte[] RawLastResponseBytes { get; private set; }

    // We don't need these IEndpointBehavior methods
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
    public void Validate(ServiceEndpoint endpoint) { }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        clientRuntime.ClientMessageInspectors.Add(new MessageCaptureInspector(this));
    }

    internal class MessageCaptureInspector : IClientMessageInspector
    {
        private WcfLogger logger;

        public MessageCaptureInspector(WcfLogger logger)
        {
            this.logger = logger;
        }

        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            // Ugly reflection magic. We need this for the case where 
            // the reply is not valid XML, and, thus, reply.ToString() 
            // only contains an error message.
            object messageData = reply.GetType()
                .GetProperty("MessageData", 
                             BindingFlags.NonPublic | BindingFlags.Instance)
                .GetValue(reply, null);

            var buffer = (ArraySegment<byte>)messageData.GetType()
                .GetProperty("Buffer")
                .GetValue(messageData, null);

            logger.RawLastResponseBytes = 
                buffer.Array.Skip(buffer.Offset).Take(buffer.Count).ToArray();
        }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            return null;
        }
    }
}

Usage:

var logger = new WcfLogger();
myWcfClient.Endpoint.EndpointBehaviors.Add(logger);

try
{
    // ... call WCF method that returns invalid XML
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

File.SaveAllBytes(@"C:\temp\raw_response.bin", logger.RawLastResponseBytes);

// Use the exception message and examine raw_response.bin with 
// a hex editor to find the problem.

Upvotes: 0

RQDQ
RQDQ

Reputation: 15569

UPDATE

Some others that have run into this issue appear to have created a Customer Message Encoder.

A message encoding binding element serializes an outgoing Message and passes it to the transport, or receives the serialized form of a message from the transport and passes it to the protocol layer if present, or to the application, if not present.

Upvotes: -1

Adam
Adam

Reputation: 2115

Yes, you need custom MessageEncoder, unlike message inspectors (IDispatchMessageInspector / IClientMessageInspector) it sees original byte content including any malformed XML data.

However it's not trivial how to implement this approach. You have to wrap a standard textMessageEncoding as custom binding element and adjust config file to use that custom binding.

Also you can see as example how I did it in my project - wrapping textMessageEncoding, logging encoder, custom binding element and config.

Upvotes: 2

Related Questions