10

I'm currently developing a WCF RESTful service. Within the validation of the POST data, I am throwing exceptions if the request XML does not conform to our business rules.

The goal is to send an e-mail to the appropriate staff if a request comes in that considered invalid. But, along with the incoming request headers, method and URI, I'd like to also send the XML that was posted.

I have not been able to find a way to access this data. Is WCF actually destroying the request body/data before I have a chance to access it or am I missing something?

Your help is appreciated as I'm confused as to why I can't access the request data.

John Saunders
  • 160,644
  • 26
  • 247
  • 397
RossG
  • 103
  • 1
  • 1
  • 5

5 Answers5

9

This unfortunately isn't supported- we had a similar need, and did it by calling internal members with reflection. We just use it in an error handler (so we can dump the raw request), but it works OK. I wouldn't recommend it for a system you don't own and operate though (eg, don't ship this code to a customer), since it can change at any time with a service pack or whatever.

public static string GetRequestBody()
{
    OperationContext oc = OperationContext.Current;

    if (oc == null)
        throw new Exception("No ambient OperationContext.");

    MessageEncoder encoder = oc.IncomingMessageProperties.Encoder;
    string contentType = encoder.ContentType;
    Match match = re.Match(contentType);

    if (!match.Success)
        throw new Exception("Failed to extract character set from request content type: " + contentType);

    string characterSet = match.Groups[1].Value;

    object bufferedMessage = operationContextType.InvokeMember("request",
        BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField,
        null, oc, null);

    //TypeUtility.AssertType(bufferedMessageType, bufferedMessage);

    object messageData = bufferedMessageType.InvokeMember("MessageData",
        BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetProperty,
        null, bufferedMessage, null);

    //TypeUtility.AssertType(jsonBufferedMessageDataType, messageData);

    object buffer = jsonBufferedMessageDataType.InvokeMember("Buffer",
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty,
        null, messageData, null);

    ArraySegment<byte> arrayBuffer = (ArraySegment<byte>)buffer;

    Encoding encoding = Encoding.GetEncoding(characterSet);

    string requestMessage = encoding.GetString(arrayBuffer.Array, arrayBuffer.Offset, arrayBuffer.Count);

    return requestMessage;
}
shytikov
  • 9,155
  • 8
  • 56
  • 103
nitzmahone
  • 13,720
  • 2
  • 36
  • 39
  • 1
    Holy cow! That's even worse than my solution :-) – Darrel Miller Dec 05 '09 at 21:03
  • Agreed- the benefit is that we don't have to smuggle a second copy from the transport layer with a message inspector on every request. This way we can get at the original buffer directly from the service code, and only when there's a problem. Hence my original caution. :) I wish they'd just expose it off the WebOperationContext, but having taken it apart, I see why they don't (especially when you consider streamed requests of arbitrary size). – nitzmahone Dec 05 '09 at 22:07
  • Thanks for replying. I now understand why you are taking this approach. It is interesting that in order to understand why WCF works the way it does you have to dig into the implementation. It sort of defeats the purpose of trying to abstract away the complexity! – Darrel Miller Dec 07 '09 at 03:05
  • Nitzmahone, Thank you for providing this example. Accessing the Request body is something that I'm only wanting to do when handling errors. I appreciate the answer!!! – RossG Dec 09 '09 at 00:02
  • Agree that they need to add a .GetBodyAsString() method or something. All I needed was the raw JSON data and all I could get was an XML representation of the JSON - the JSON library I use needs the raw JSON, not what they deem the data should look like! – user289100 May 18 '11 at 17:26
9

So, if you declare your contract something like:

[WebInvoke(Method = "POST", UriTemplate = "create", ResponseFormat=WebMessageFormat.Json)]
 int CreateItem(Stream streamOfData);

(you can use XML instead) The streamOfData should be the body of an HTTP POST. You can deserialize it using something like:

 StreamReader reader = new StreamReader(streamId);
 String res = reader.ReadToEnd();
 NameValueCollection coll = HttpUtility.ParseQueryString(res);

It's working like that for us, at least. You may want to use a different approach to get the string into an XMLDocument or something. This works for our JSON posts. Might not be the most elegant solution, but it is working.

I hope this helps.

Glenn

Ruprict
  • 398
  • 4
  • 14
  • 1
    Glenn, Thanks for your response. I currently have an operation contract that is immediately deserializing the posted xml into an object. Even though I have access to the new object for processing, I would still like the raw request to be available. Just a simple string representation of the body. Thanks! – RossG Dec 06 '09 at 15:22
2

Try this,

OperationContext.Current.RequestContext.RequestMessage
Roman Marusyk
  • 23,328
  • 24
  • 73
  • 116
SajithK
  • 1,014
  • 12
  • 23
2

Here's how you do it without reflection:

using (var reader = OperationContext.Current.RequestContext.RequestMessage.GetReaderAtBodyContents ()) {
    if (reader.Read ())
        return new string (Encoding.ASCII.GetChars (reader.ReadContentAsBase64 ()));
                return result;
    }
}

If the reader is a HttpStreamXmlDictionaryReader (as it was in my case), the class's implementation of the method ReadContentAsBase64(byte[] buffer, int index, int count) simply passes these parameters to the Stream.Read method.

Once I have the byte[] I convert the bytes to a string via ASCII encoding. For a proper implementation, you could use the content type & encoding from the message's headers to do per HTTP spec.

Roman Marusyk
  • 23,328
  • 24
  • 73
  • 116
George Tsiokos
  • 1,890
  • 21
  • 31
  • Your solution does not work if the Message.State is already set to Read - you get an InvalidOperationException "This message cannot support the operation because it has been read." – Dai Feb 24 '12 at 03:08
  • Here's how you take care of that: http://stackoverflow.com/questions/2184806/read-wcf-message-body-twice-message-cannot-be-read – George Tsiokos Feb 24 '12 at 22:51
0

You could arrest the HttpApplication.Request.InputStream in a custom HttpModule of the WCF Service, read the stream and again set its position to 0 in the custom HttpModule's event handler. Then store it in session and access it further in the actual OperationContract.

For example:

public class CustomModule : IHttpModule
{
    public void Dispose()
    {

    }

    public void Init(HttpApplication context)
    {
        context.AcquireRequestState +=context_AcquireRequestState;
    }

    void context_AcquireRequestState(object sender, EventArgs e)
    {
        HttpApplication application = sender as HttpApplication;
        Stream str = application.Request.InputStream;
        StreamReader sr = new StreamReader(str);
        string req = sr.ReadToEnd();
        str.Position = 0;
        application.Session["CurrentRequest"] = req;
    }
 }
Roman Marusyk
  • 23,328
  • 24
  • 73
  • 116
Yatin
  • 76
  • 6