WCF: Custom encoder to read MTOM response
Today I will talk about one of the weird issues related to encoder.
Issue
Client application should send an MTOM encoded request message to 3rd party web service. The web service will respond it with a TEXT encoded response message. How can .net client support this ambiguity in encoder level?
Assessment
- When client is configured with <textMessageEncoding messageVersion="Soap11" /> in customBinding,
- Fiddler reads the response as 200 (means success).
- .net client could not read the response as it was MTOM encoded (at the same time client is designed for text encoding alone)
- As per fiddler, request is TEXT encoded and response is MTOM encoded.
- In ideal cases (from service perspective), both request and response should have same encoding type.
- Interestingly, the above flow works fine from SOAP UI client (a java application). However, .net client application is not smart enough to deal with this ambiguity.
- The fiddler request/ response packets are identical for SOAP UI and .net textMessageEncoded client.
Raw request header
Content-Type: text/xml; charset=utf-8
SOAPAction: "schemas.xmlsoap.org/soap/http"
Host: my-dev-service.server.personal.com:4412
Content-Length: 4630
Expect: 100-continue
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Raw response header
HTTP/1.1 200 OK
X-Backside-Transport: OK OK
Connection: Keep-Alive
Content-Type: multipart/related; boundary="uuid:bacd6bbc-a2ad-4d71-b56d-41e0131510e4+id=22"; start-info="application/soap+xml"; type="application/xop+xml"; start="<tempuri.org/0>"
Server: Microsoft-IIS/7.5
MIME-Version: 1.0
X-Powered-By: ASP.NET
Date: Thu, 14 Jan 2016 00:06:03 GMT
X-Client-IP: xxx.xx.x.xxx
X-Global-Transaction-ID: 7534509
Content-Length: 16627
--uuid:bacd6bbc-a2ad-4d71-b56d-41e0131510e4+id=22
Content-ID: <tempuri.org/0>
Content-Transfer-Encoding: binary
Content-Type: application/xop+xml; type="application/soap+xml"
So, the custom encoder should be able to read the response buffer and read the MTOM content properly.
When application reads the MTOM content, it reads the root content type header by the following section of source code:
Encoding ReadRootContentTypeHeader(ContentTypeHeader header, Encoding[] expectedEncodings, string expectedType)
{
if (header == null)
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new XmlException(SR.GetString(SR.MtomRootContentTypeNotFound)));
if (String.Compare(MtomGlobals.XopMediaType, header.MediaType, StringComparison.OrdinalIgnoreCase) != 0
|| String.Compare(MtomGlobals.XopMediaSubtype, header.MediaSubtype, StringComparison.OrdinalIgnoreCase) != 0)
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new XmlException(SR.GetString(SR.MtomRootNotApplicationXopXml, MtomGlobals.XopMediaType, MtomGlobals.XopMediaSubtype)));
string charset;
if (!header.Parameters.TryGetValue(MtomGlobals.CharsetParam, out charset) || charset == null || charset.Length == 0)
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new XmlException(SR.GetString(SR.MtomRootRequiredParamNotSpecified, MtomGlobals.CharsetParam)));
......
}
The API ReadRootContentTypeHeader looks for ‘charset’ parameter in the root content type header. If it will not find charset element in the response root content type header, it will throw an XmlException with message Root MIME part must contain non-zero length value for 'charset' parameter in Content-Type header.
Switching back to the raw response header Content-Type: application/xop+xml; type="application/soap+xml", it can be said that it lacks ‘charset’ parameter in header.
The job of the custom encoder would be to append an additional content of “; charset=utf-8” to Content-Type. Finally, the row will appear as the following:
Content-Type: application/xop+xml; charset=utf-8; type="application/soap+xml"
Reference
- https://blogs.msdn.microsoft.com/distributedservices/2010/01/06/manipulate-a-wcf-request-response-using-a-custom-encoder/
- https://blogs.msdn.microsoft.com/carlosfigueira/2011/02/15/using-mtom-in-a-wcf-custom-encoder/
Solution
Please follow the following steps in order to create a custom encoder.
- Create a Class Library project with name “CustomEncoderProject”
- Add reference (System.ServiceModel and System.configuration)
- Copy the following section of code into a .cs file (it will have definition for certain classes)
public class MessageVersionConverter: TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (typeof(string) == sourceType)
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (typeof(InstanceDescriptor) == destinationType)
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
string messageVersion = (string)value;
MessageVersion retval = null;
switch (messageVersion)
{
case ConfigurationStrings.Soap11WSAddressing10:
retval = MessageVersion.Soap11WSAddressing10;
break;
case ConfigurationStrings.Soap12WSAddressing10:
retval = MessageVersion.Soap12WSAddressing10;
break;
case ConfigurationStrings.Soap11WSAddressingAugust2004:
retval = MessageVersion.Soap11WSAddressingAugust2004;
break;
case ConfigurationStrings.Soap12WSAddressingAugust2004:
retval = MessageVersion.Soap12WSAddressingAugust2004;
break;
case ConfigurationStrings.Soap11:
retval = MessageVersion.Soap11;
break;
case ConfigurationStrings.Soap12:
retval = MessageVersion.Soap12;
break;
case ConfigurationStrings.None:
retval = MessageVersion.None;
break;
case ConfigurationStrings.Default:
retval = MessageVersion.Default;
break;
default:
throw new ArgumentOutOfRangeException("messageVersion");
}
return retval;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (typeof(string) == destinationType && value is MessageVersion)
{
string retval = null;
MessageVersion messageVersion = (MessageVersion)value;
if (messageVersion == MessageVersion.Default)
{
retval = ConfigurationStrings.Default;
}
else if (messageVersion == MessageVersion.Soap11WSAddressing10)
{
retval = ConfigurationStrings.Soap11WSAddressing10;
}
else if (messageVersion == MessageVersion.Soap12WSAddressing10)
{
retval = ConfigurationStrings.Soap12WSAddressing10;
}
else if (messageVersion == MessageVersion.Soap11WSAddressingAugust2004)
{
retval = ConfigurationStrings.Soap11WSAddressingAugust2004;
}
else if (messageVersion == MessageVersion.Soap12WSAddressingAugust2004)
{
retval = ConfigurationStrings.Soap12WSAddressingAugust2004;
}
else if (messageVersion == MessageVersion.Soap11)
{
retval = ConfigurationStrings.Soap11;
}
else if (messageVersion == MessageVersion.Soap12)
{
retval = ConfigurationStrings.Soap12;
}
else if (messageVersion == MessageVersion.None)
{
retval = ConfigurationStrings.None;
}
else
{
throw new ArgumentOutOfRangeException("messageVersion");
}
return retval;
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
public class ConfigurationStrings
{
internal const string MessageVersion = "messageVersion";
internal const string MediaType = "mediaType";
internal const string Encoding = "encoding";
internal const string ReaderQuotas = "readerQuotas";
internal const string None = "None";
internal const string Default = "Default";
internal const string Soap11 = "Soap11";
internal const string Soap11WSAddressing10 = "Soap11WSAddressing10";
internal const string Soap11WSAddressingAugust2004 = "Soap11WSAddressingAugust2004";
internal const string Soap12 = "Soap12";
internal const string Soap12WSAddressing10 = "Soap12WSAddressing10";
internal const string Soap12WSAddressingAugust2004 = "Soap12WSAddressingAugust2004";
internal const string DefaultMessageVersion = Soap11;
internal const string DefaultMediaType = "text/xml";
internal const string DefaultEncoding = "utf-8";
}
public class MyNewEncodingBindingExtensionElement : BindingElementExtensionElement
{
[ConfigurationProperty(ConfigurationStrings.MessageVersion,
DefaultValue = ConfigurationStrings.DefaultMessageVersion)]
[TypeConverter(typeof(MessageVersionConverter))]
public MessageVersion MessageVersion
{
get { return (MessageVersion)base[ConfigurationStrings.MessageVersion]; }
set { base[ConfigurationStrings.MessageVersion] = value; }
}
public override void ApplyConfiguration(BindingElement bindingElement)
{
base.ApplyConfiguration(bindingElement);
MyNewEncodingBindingElement binding = (MyNewEncodingBindingElement)bindingElement;
binding.MessageVersion = this.MessageVersion;
}
public override Type BindingElementType
{
get { return typeof(MyNewEncodingBindingElement); }
}
protected override BindingElement CreateBindingElement()
{
var binding = new MyNewEncodingBindingElement();
ApplyConfiguration(binding);
return binding;
}
}
public class MyNewEncodingBindingElement : MessageEncodingBindingElement
{
private MessageVersion msgVersion;
private string encoding;
public MyNewEncodingBindingElement()
{
}
MyNewEncodingBindingElement(MyNewEncodingBindingElement binding)
: this(binding.Encoding, binding.MessageVersion)
{
}
public MyNewEncodingBindingElement(string encoding, MessageVersion msgVersion)
{
if (encoding == null)
throw new ArgumentNullException("encoding");
if (msgVersion == null)
throw new ArgumentNullException("msgVersion");
this.msgVersion = msgVersion;
this.encoding = encoding;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new MyNewEncoderFactory(this.msgVersion);
}
public override MessageVersion MessageVersion
{
get
{
return this.msgVersion;
}
set
{
if (value == null)
throw new ArgumentNullException("value");
this.msgVersion = value;
}
}
public string Encoding
{
get
{
return this.encoding;
}
set
{
if (value == null)
throw new ArgumentNullException("value");
this.encoding = value;
}
}
public override BindingElement Clone()
{
return this;
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelListener<TChannel>();
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
return context.CanBuildInnerChannelFactory<TChannel>();
}
public override bool CanBuildChannelListener<TChannel>(BindingContext context)
{
return context.CanBuildInnerChannelListener<TChannel>();
}
}
public class MyNewEncoderFactory : MessageEncoderFactory
{
MessageVersion messageVersion;
MyNewEncoder encoder;
public MyNewEncoderFactory(MessageVersion messageVersion)
{
this.messageVersion = messageVersion;
this.encoder = new MyNewEncoder(messageVersion);
}
public override MessageEncoder Encoder
{
get { return this.encoder; }
}
public override MessageVersion MessageVersion
{
get { return this.encoder.MessageVersion; }
}
}
public class MyNewEncoder : MessageEncoder
{
MessageEncoder textEncoder;
MessageEncoder mtomEncoder;
public MyNewEncoder(MessageVersion messageVersion)
{
this.textEncoder = new TextMessageEncodingBindingElement(messageVersion, Encoding.UTF8).CreateMessageEncoderFactory().Encoder;
this.mtomEncoder = new MtomMessageEncodingBindingElement(messageVersion, Encoding.UTF8).CreateMessageEncoderFactory().Encoder;
}
public override string ContentType
{
get { return this.textEncoder.ContentType; }
}
public override string MediaType
{
get { return this.textEncoder.MediaType; }
}
public override MessageVersion MessageVersion
{
get { return this.textEncoder.MessageVersion; }
}
public override bool IsContentTypeSupported(string contentType)
{
return this.mtomEncoder.IsContentTypeSupported(contentType);
}
public override T GetProperty<T>()
{
T result = this.textEncoder.GetProperty<T>();
if (result == null)
{
result = this.mtomEncoder.GetProperty<T>();
}
if (result == null)
{
result = base.GetProperty<T>();
}
return result;
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
//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 occurrence 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");
string 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 buffer and pass it to the underlying
* MTOM Encoder.
*/
int size = newArray.Length;
byte[] msg = bufferManager.TakeBuffer(size);
Array.Copy(newArray, msg, size);
ArraySegment<byte> newResult = new ArraySegment<byte>(msg);
return this.mtomEncoder.ReadMessage(newResult, bufferManager, contentType);
}
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
{
return this.mtomEncoder.ReadMessage(stream, maxSizeOfHeaders, contentType);
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
return this.textEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
}
public override void WriteMessage(Message message, Stream stream)
{
this.textEncoder.WriteMessage(message, stream);
}
}
- Go to your client application and add reference to project “CustomEncoderProject”
- App.config should have the following section of configuration to register custom encoder
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IService1" />
</basicHttpBinding>
<customBinding>
<binding name="Mycb">
<mixedMessageEncoding messageVersion="Soap11"/>
<!--mtomMessageEncoding messageVersion="Soap11" writeEncoding="utf-8"></mtomMessageEncoding-->
<security allowSerializedSigningTokenOnReply="true" enableUnsecuredResponse="true"
authenticationMode="MutualCertificate" requireDerivedKeys="false"
securityHeaderLayout="Lax" includeTimestamp="false" messageProtectionOrder="SignBeforeEncrypt"
messageSecurityVersion="WSSecurity10WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10"
requireSecurityContextCancellation="true">
<issuedTokenParameters keyType="AsymmetricKey" />
<localClientSettings cacheCookies="true" detectReplays="false" />
<localServiceSettings detectReplays="false" />
<secureConversationBootstrap/>
</security>
<httpsTransport requireClientCertificate="true" />
</binding>
</customBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="clientSecured">
...
</behavior>
</endpointBehaviors>
</behaviors>
<client>
...
</client>
<extensions>
<bindingElementExtensions>
<add name="mixedMessageEncoding" type="CustomEncoderProject.MyNewEncodingBindingExtensionElement, CustomEncoderProject"/>
</bindingElementExtensions>
</extensions>
</system.serviceModel>
I hope this helps!
Comments
- Anonymous
May 25, 2016
Hello, I was having the same problem in my application.I used his example and during the test appeared this error: http://meneguci.com/imagem.pngThank you!