Using MTOM in a WCF custom encoder
One of the out-of-the-box XML encoders for messages in WCF is the MTOM encoder. It provides an efficient way of transmitting large binary data in the body of SOAP envelopes (by *not* applying the base-64 encoding to such data, which increases its size by roughly 33%), while still being interoperable with other platforms (MTOM is a W3C standard). The MTOM encoder in WCF can even read “normal” XML-encoded messages, but it will always write MTOM-encoded messages.
One scenario which appears quite frequently is the need for a “smart” encoder, which will reply with the same encoding as its input – i.e., if a client sent a request using normal XML, the server should reply in normal XML; if the client sent a request using MTOM, the server should also reply using MTOM. It seems like a simple scenario – create a custom encoder which wraps both a Text and a Mtom encoder, use some inspector to correlate the request with the reply indicating via a message property whether request was Text or Mtom, and on output, simply use the same encoder used to decode the input to write out the response.
- public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
- {
- if (this.ShouldWriteMtom(message))
- {
- return this.mtomEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
- }
- else
- {
- return this.textEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
- }
- }
This, however, doesn’t work. When we look at the output, it actually looks like a valid MTOM/XOP document inside the HTTP body. And in a strict sense it is: the body of the response is indeed a valid XOP document – very similar to the one at https://www.w3.org/TR/2005/REC-xop10-20050125/#example.
- HTTP/1.1 200 OK
- Content-Length: 974
- Content-Type: multipart/related; type="application/xop+xml"
- Server: Microsoft-HTTPAPI/2.0
- Date: Wed, 16 Feb 2011 04:17:08 GMT
- MIME-Version: 1.0
- Content-Type: multipart/related; type="application/xop+xml";start="<https://tempuri.org/0>";boundary="uuid:85982505-3777-4476-8a57-477305bbfd65+id=1";start-info="application/soap+xml"
- --uuid:85982505-3777-4476-8a57-477305bbfd65+id=1
- Content-ID: <https://tempuri.org/0>
- Content-Transfer-Encoding: 8bit
- Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml"
- <s:Envelope xmlns:s="https://www.w3.org/2003/05/soap-envelope" xmlns:a="https://www.w3.org/2005/08/addressing">...</s:Envelope>
- --uuid:85982505-3777-4476-8a57-477305bbfd65+id=1--
However, this is not valid in the context of MTOM used within SOAP/HTTP. As described in the Serialization of a SOAP message section of the MTOM specification, the outer (HTTP) content-type must contain all the content-type of the MIME package. Also, the MIME-Version header must be “promoted” to an outer package header (HTTP) as well., so that the example above should be encoded as follows:
- HTTP/1.1 200 OK
- Content-Length: 974
- Content-Type: multipart/related; type="application/xop+xml";start="<https://tempuri.org/0>";boundary="uuid:85982505-3777-4476-8a57-477305bbfd65+id=1";start-info="application/soap+xml"
- Server: Microsoft-HTTPAPI/2.0
- MIME-Version: 1.0
- Date: Wed, 16 Feb 2011 04:17:08 GMT
- --uuid:85982505-3777-4476-8a57-477305bbfd65+id=1
- Content-ID: <https://tempuri.org/0>
- Content-Transfer-Encoding: 8bit
- Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml"
- <s:Envelope xmlns:s="https://www.w3.org/2003/05/soap-envelope" xmlns:a="https://www.w3.org/2005/08/addressing">...</s:Envelope>
- --uuid:85982505-3777-4476-8a57-477305bbfd65+id=1--
So the MTOM encoder doesn’t write the message in a way that is compatible with the HTTP binding. The out-of-the-box MTOM encoder in WCF works (i.e., it creates the correct body) because the HttpTransport uses an internal method in the MtomEncoder class (which is by itself internal) to write the appropriate body (see the MTOM Encoding section at https://msdn.microsoft.com/en-us/library/ms735115.aspx). If we use a custom encoder, the HTTP transport has no way to know how to call that method, so we get the (incorrect) mapping shown before.
So how can we enable this scenario? The solution needs to be broken down in two parts. First, we need to add the correct headers (MIME-Version and Content-Type) to the HTTP response. This cannot be done at the encoder level, since at that point the headers have already been written to the wire, and the transport only needs the body from the encoder. My sample uses an IDispatchMessageInspector to add the headers to the HTTP level. The second part is to change the way the message is written, both to prevent the MIME header from being output, and to use the same boundary value as the one specified in the Content-Type HTTP header.
The first part is shown below. Notice that it’s passing, in the message properties, all the information that the encoder needs to create the MTOM body.
- public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
- {
- object result;
- request.Properties.TryGetValue(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, out result);
- return result;
- }
- public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
- {
- bool isMtom = (correlationState is bool) && (bool)correlationState;
- reply.Properties.Add(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, isMtom);
- if (isMtom)
- {
- string boundary = "uuid:" + Guid.NewGuid().ToString();
- string startUri = "https://tempuri.org/0";
- string startInfo = "application/soap+xml";
- string contentType = "multipart/related; type=\"application/xop+xml\";start=\"<" +
- startUri +
- ">\";boundary=\"" +
- boundary +
- "\";start-info=\"" +
- startInfo + "\"";
- HttpResponseMessageProperty respProp;
- if (reply.Properties.ContainsKey(HttpResponseMessageProperty.Name))
- {
- respProp = reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;
- }
- else
- {
- respProp = new HttpResponseMessageProperty();
- reply.Properties[HttpResponseMessageProperty.Name] = respProp;
- }
- respProp.Headers[HttpResponseHeader.ContentType] = contentType;
- respProp.Headers["MIME-Version"] = "1.0";
- reply.Properties[TextOrMtomEncodingBindingElement.MtomBoundaryPropertyName] = boundary;
- reply.Properties[TextOrMtomEncodingBindingElement.MtomStartInfoPropertyName] = startInfo;
- reply.Properties[TextOrMtomEncodingBindingElement.MtomStartUriPropertyName] = startUri;
- }
- }
Next is the encoder part. Here I’m only showing the [Read/Write]Message implementation, as it contains the main change to a “normal” custom wrapping encoder (and also only the buffered version; the streamed version is similar). On ReadMessage, we always use the MTOM encoder to decode the message – since it can read both text and mtom-encoded ones. On ReadMessage we also set the flag which will be picked up by the inspector to identify whether the request is MTOM or not. On WriteMessage, we’re handling the writing of the message ourselves, creating a MTOM writer directly. One of the overloads of the XmlDictionaryWriter.CreateMtomWriter method does exactly what we need – it allows us to pass the start-info, boundary, start-uri parameters, and also a flag indicating whether the MIME headers should be written. With that writer, we simply ask for the message to write itself (Message.WriteMessage) and that’s essentially it (plus some buffer management required by the encoder contract).
- public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
- {
- Message result = this._mtomEncoder.ReadMessage(buffer, bufferManager, contentType);
- result.Properties.Add(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, IsMtomMessage(contentType));
- return result;
- }
- public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
- {
- if (this.ShouldWriteMtom(message))
- {
- using (MemoryStream ms = new MemoryStream())
- {
- XmlDictionaryWriter mtomWriter = CreateMtomWriter(ms, message);
- message.WriteMessage(mtomWriter);
- mtomWriter.Flush();
- byte[] buffer = bufferManager.TakeBuffer((int)ms.Position + messageOffset);
- Array.Copy(ms.GetBuffer(), 0, buffer, messageOffset, (int)ms.Position);
- return new ArraySegment<byte>(buffer, messageOffset, (int)ms.Position);
- }
- }
- else
- {
- return this._textEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
- }
- }
- private static bool IsMtomMessage(string contentType)
- {
- return contentType.IndexOf("type=\"application/xop+xml\"", StringComparison.OrdinalIgnoreCase) >= 0;
- }
- private bool ShouldWriteMtom(Message message)
- {
- object temp;
- return message.Properties.TryGetValue(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, out temp) && (bool)temp;
- }
- private XmlDictionaryWriter CreateMtomWriter(Stream stream, Message message)
- {
- string boundary = message.Properties[TextOrMtomEncodingBindingElement.MtomBoundaryPropertyName] as string;
- string startUri = message.Properties[TextOrMtomEncodingBindingElement.MtomStartUriPropertyName] as string;
- string startInfo = message.Properties[TextOrMtomEncodingBindingElement.MtomStartInfoPropertyName] as string;
- return XmlDictionaryWriter.CreateMtomWriter(stream, Encoding.UTF8, int.MaxValue, startInfo, boundary, startUri, false, false);
- }
This custom encoder should fulfill the requirement of using MTOM in a custom encoder. The full project with the inspector and the encoder can be found here.
Comments
- Anonymous
April 08, 2013
A belated "thank you very much" for this article, it helped me in being able to add a charset to the content-type header (~needed by WS). - Anonymous
May 12, 2013
Dear Carlos Figueira,I want to process response message by MtomMwssageEncodeingBindingElement in WinRT but WinRT doesn't support MtomMwssageEncodeingBindingElement.Can you help me ?How to create a custom encoder which knows how to parse MTOM messages?Thank you very much. - Anonymous
June 12, 2013
Dear Carlos,Thanks a lot for this wonderful blog. I'm currently stuck with a problem related to 'boundary' value within the MIME header. We have a wcf service with custom binding to support mtom. Here is a portion of sample MIME header that we are getting out of our service:content-type: multipart/related; type="application/xop+xml";start="<tempuri.org/.../soap+xml"We are using NIST message validator to validate the message and the validator is not liking the 'boundary' value for some reason and the validation fails. In the above sample boundary value, if I just take out '=' sign and make it look like boundary="uuid:f45e6739-e796-41c7-bc96-6dd977a185f6+id2", the validation passes. So can you please me in getting rid of this '=' sign from the boundary value?Thanks. - Anonymous
March 28, 2014
This method does not seem to work for Security Token calls ( RST/SCT ) because they do not trigger an IDispatchMessageInspector.Is there a way to change the header for RST/SCT ?