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.

  1. public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
  2. {
  3.     if (this.ShouldWriteMtom(message))
  4.     {
  5.         return this.mtomEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
  6.     }
  7.     else
  8.     {
  9.         return this.textEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
  10.     }
  11. }

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.

  1. HTTP/1.1 200 OK
  2. Content-Length: 974
  3. Content-Type: multipart/related; type="application/xop+xml"
  4. Server: Microsoft-HTTPAPI/2.0
  5. Date: Wed, 16 Feb 2011 04:17:08 GMT
  6.  
  7. MIME-Version: 1.0
  8. 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"
  9.  
  10. --uuid:85982505-3777-4476-8a57-477305bbfd65+id=1
  11. Content-ID: <https://tempuri.org/0>
  12. Content-Transfer-Encoding: 8bit
  13. Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml"
  14.  
  15. <s:Envelope xmlns:s="https://www.w3.org/2003/05/soap-envelope" xmlns:a="https://www.w3.org/2005/08/addressing">...</s:Envelope>
  16. --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:

  1. HTTP/1.1 200 OK
  2. Content-Length: 974
  3. 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"
  4. Server: Microsoft-HTTPAPI/2.0
  5. MIME-Version: 1.0
  6. Date: Wed, 16 Feb 2011 04:17:08 GMT
  7.  
  8.  
  9. --uuid:85982505-3777-4476-8a57-477305bbfd65+id=1
  10. Content-ID: <https://tempuri.org/0>
  11. Content-Transfer-Encoding: 8bit
  12. Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml"
  13.  
  14. <s:Envelope xmlns:s="https://www.w3.org/2003/05/soap-envelope" xmlns:a="https://www.w3.org/2005/08/addressing">...</s:Envelope>
  15. --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.

  1. public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
  2. {
  3.     object result;
  4.     request.Properties.TryGetValue(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, out result);
  5.     return result;
  6. }
  7.  
  8. public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
  9. {
  10.     bool isMtom = (correlationState is bool) && (bool)correlationState;
  11.     reply.Properties.Add(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, isMtom);
  12.     if (isMtom)
  13.     {
  14.         string boundary = "uuid:" + Guid.NewGuid().ToString();
  15.         string startUri = "https://tempuri.org/0";
  16.         string startInfo = "application/soap+xml";
  17.         string contentType = "multipart/related; type=\"application/xop+xml\";start=\"<" +
  18.             startUri +
  19.             ">\";boundary=\"" +
  20.             boundary +
  21.             "\";start-info=\"" +
  22.             startInfo + "\"";
  23.  
  24.         HttpResponseMessageProperty respProp;
  25.         if (reply.Properties.ContainsKey(HttpResponseMessageProperty.Name))
  26.         {
  27.             respProp = reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;
  28.         }
  29.         else
  30.         {
  31.             respProp = new HttpResponseMessageProperty();
  32.             reply.Properties[HttpResponseMessageProperty.Name] = respProp;
  33.         }
  34.  
  35.         respProp.Headers[HttpResponseHeader.ContentType] = contentType;
  36.         respProp.Headers["MIME-Version"] = "1.0";
  37.  
  38.         reply.Properties[TextOrMtomEncodingBindingElement.MtomBoundaryPropertyName] = boundary;
  39.         reply.Properties[TextOrMtomEncodingBindingElement.MtomStartInfoPropertyName] = startInfo;
  40.         reply.Properties[TextOrMtomEncodingBindingElement.MtomStartUriPropertyName] = startUri;
  41.     }
  42. }

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).

  1. public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
  2. {
  3.     Message result = this._mtomEncoder.ReadMessage(buffer, bufferManager, contentType);
  4.     result.Properties.Add(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, IsMtomMessage(contentType));
  5.     return result;
  6. }
  7. public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
  8. {
  9.     if (this.ShouldWriteMtom(message))
  10.     {
  11.         using (MemoryStream ms = new MemoryStream())
  12.         {
  13.             XmlDictionaryWriter mtomWriter = CreateMtomWriter(ms, message);
  14.             message.WriteMessage(mtomWriter);
  15.             mtomWriter.Flush();
  16.             byte[] buffer = bufferManager.TakeBuffer((int)ms.Position + messageOffset);
  17.             Array.Copy(ms.GetBuffer(), 0, buffer, messageOffset, (int)ms.Position);
  18.             return new ArraySegment<byte>(buffer, messageOffset, (int)ms.Position);
  19.         }
  20.     }
  21.     else
  22.     {
  23.         return this._textEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
  24.     }
  25. }
  26. private static bool IsMtomMessage(string contentType)
  27. {
  28.     return contentType.IndexOf("type=\"application/xop+xml\"", StringComparison.OrdinalIgnoreCase) >= 0;
  29. }
  30. private bool ShouldWriteMtom(Message message)
  31. {
  32.     object temp;
  33.     return message.Properties.TryGetValue(TextOrMtomEncodingBindingElement.IsIncomingMessageMtomPropertyName, out temp) && (bool)temp;
  34. }
  35. private XmlDictionaryWriter CreateMtomWriter(Stream stream, Message message)
  36. {
  37.     string boundary = message.Properties[TextOrMtomEncodingBindingElement.MtomBoundaryPropertyName] as string;
  38.     string startUri = message.Properties[TextOrMtomEncodingBindingElement.MtomStartUriPropertyName] as string;
  39.     string startInfo = message.Properties[TextOrMtomEncodingBindingElement.MtomStartInfoPropertyName] as string;
  40.     return XmlDictionaryWriter.CreateMtomWriter(stream, Encoding.UTF8, int.MaxValue, startInfo, boundary, startUri, false, false);
  41. }

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.