Manipulate a WCF request / response using a custom encoder
Message Encoders are an interesting component within WCF channel stack. It’s primary job is to transform Message instances to and from the wire. While it has an independent existence within a WCF binding stack in the form of a Message Encoding binding element, in essence it is closely integrated with the underlying transport layer. This is unlike to any other binding elements, example a security binding element. Check out this MSDN article on message encoders for more details.
Today I will concentrate my discussion on custom encoders. I will present a very simple example to demonstrate the power of a custom encoder to resolve a seemingly complex scenario. For those who are yet to work with custom encoders, you can find more details here. There is also a MSDN sample which demonstrates a custom text encoder.
Consider a scenario where you are trying to consume a non - .NET web service from a WCF client application over MTOM. The client is configured with the following binding:
<customBinding>
<binding name="CustomTransferUsingMTOM">
<mtomMessageEncoding maxBufferSize="65536"
messageVersion="Soap12WSAddressing10" />
<httpTransport authenticationScheme="Anonymous"
maxReceivedMessageSize="2147483647"
maxBufferSize="2147483647"
transferMode="Buffered" />
</binding>
</customBinding>
Ideally things should work if both the participating components have identical implementation of the same standards. More often than not, things don’t follow the ideal path. In this scenario, a MTOM encoded response from the web service fails with the following exception:
<ExceptionType>System.Xml.XmlException, System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>Root MIME part must contain non-zero length value for 'charset' parameter in Content-Type header.</Message>
<StackTrace>
at System.Xml.XmlMtomReader.ReadRootContentTypeHeader(ContentTypeHeader header, Encoding[] expectedEncodings, String expectedType)
at System.Xml.XmlMtomReader.Initialize(Stream stream, String contentType, XmlDictionaryReaderQuotas quotas, Int32 maxBufferSize)
at System.Xml.XmlMtomReader.SetInput(Stream stream, Encoding[] encodings, String contentType, XmlDictionaryReaderQuotas quotas, Int32 maxBufferSize, OnXmlDictionaryReaderClose onClose)
at System.Xml.XmlDictionaryReader.CreateMtomReader(Stream stream, Encoding[] encodings, String contentType, XmlDictionaryReaderQuotas quotas, Int32 maxBufferSize, OnXmlDictionaryReaderClose onClose)
at System.ServiceModel.Channels.MtomMessageEncoder.TakeStreamedReader(Stream stream, String contentType)
</StackTrace>
Let’s have a look at the incoming response to get a better understanding of things:
HTTP/1.0 200 OK
Content-Type: multipart/related;
boundary="----=_Part_1_31588834.1260459496767";
start="<88a8d13e5330b97c125391cf9ba>"; start-info="application/soap+xml";
type="application/xop+xml"; charset=UTF-8
MIME-Version: 1.0
Content-Length: 309045
------=_Part_1_31588834.1260459496767
content-type: application/xop+xml; type="application/soap+xml"
content-transfer-encoding: binary
content-id: <88a8d13e5330b97c125391cf9ba>
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="
…………………………
</SOAP-ENV:Envelope>
------=_Part_1_31588834.1260459496767
content-type: application/octet-stream
content-transfer-encoding: binary
content-id: <19e0ed854e021b83125391cf9bb>
Base64Encoded data goes here
From the highlighted error message above, one can understand that the issue is with missing ‘charset’ parameter in Content-Type header. Check the root MIME header portion (highlighted above). Even though a charset parameter is present as part of HTTP header, it is indeed missing in the root MIME header. WCF does not like that. There are 2 ways to go about here: either we modify the service to generate a working response or we customize the WCF client so that it accepts this response. We will discuss about the later option. That’s where a custom encoder comes into picture.
With the help of a custom encoder we can modify the response to accommodate a charset parameter in the correct place. This is not possible either with a message inspector or a custom channel as either works on a Message instance. Note the error is thrown before a Message instance is created by MTOM encoder. Primary customization will go inside ReadMessage method:
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
Algorithm we need to follow out here:
- Convert ArraySegment<byte> buffer into a string
- Manipulate generated string to accommodate the charset parameter
- Recreate ArraySegment<byte> and pass it back to the mtomencoder by calling MtomMessageEncoder.ReadMessage.
Things become interesting if you have a binary data imbedded in the response, for example an image. In that case the first step becomes critical. Ideally we will use the following code to generate a string:
byte[] incomingResponse = buffer.Array;
string str = System.Text.Encoding.UTF8.GetString(incomingResponse);
This will work for a text attachment, not for an image. This will not generate any runtime errors though. WCF client will be able to process the response (which means that the issue with ‘charset’ parameter is resolved). However the downloaded binary image, present in the response, will be corrupt. That is because of the encoding format used in the above line. We cannot convert a base64encoded image into string via UTF-8 encoding. If we want to retain the encoding, we need to use Convert.ToBase64String. In that case, we will not be able to perform any string manipulation operations.
I decided to overcome this catch22 situation by converting the first ‘n’ bytes of an incoming response into string, instead of the entire response. This makes sense in this particular scenario since the size of the response till root MIME header will more or less be constant. Hence I wrote the following code piece which does the job of inserting a ‘charset’ parameter into a ‘Content-Type’ header:
//Convert the received buffer into a string
byte[] incomingResponse = buffer.Array;
//read the first 500 bytes of the response
string strFirst500 = System.Text.Encoding.UTF8.GetString(incomingResponse, 0, 500);
/*
* Check the last occurance of 'application/xop+xml' in the response. We check for the last
* occurrence since the first one is present in the Content-Type HTTP Header. Once found,
* append charset header to this string
*/
int appIndex = strFirst500.LastIndexOf("application/xop+xml");
modifiedResponse = strFirst500.Insert(appIndex + 19, "charset=utf-8");
//convert the modified string back into a byte array
byte[] ma = System.Text.Encoding.UTF8.GetBytes(modifiedResponse);
//integrate the modified byte array back to the original byte array
int increasedLength = ma.Length - 500;
byte[] newArray = new byte[incomingResponse.Length + increasedLength];
for (int count = 0; count < newArray.Length; count++)
{
if (count < ma.Length)
{
newArray[count] = ma[count];
}
else
{
newArray[count] = incomingResponse[count - increasedLength];
}
}
/*
* In this part generate a new ArraySegment<byte> buffer and pass it to the underlying MTOM
* Encoder.
*/
int size = newArray.Length;
byte[] msg = manager.TakeBuffer(size);
Array.Copy(newArray, msg, size);
ArraySegment<byte> newResult = new ArraySegment<byte>(msg);
With that we have resolved an interop issue using a custom encoder. Likewise we can extrapolate the usage of a custom encoder to other scenarios where we need to operate on a request / response prior to it being acted upon by an encoder.