How to Intercept WCF Web Service Reponses and Manually Deserialize Them

Share

We were using a web service that wasn’t quite up to interoperability standards. When attempting to use the service from .Net, we saw a number of errors and warnings related to the how the response is deserialized. In particular, .Net seemed to have trouble with the arrays (ArrayOfString or ArrayOfStrings).

Some of the warning or exceptions I saw are listed here, the solution is below it, and some of the root causes are below that. I don’t think it’s important to know about the specific web service or WSDL I was using, except to say that it didn’t pass the WS-I Basic Profile.

These were the original symptoms:

  • Error in deserializing body of reply message for operation ‘myMethodName’. (System.ServiceModel.CommunicationException)
  • There is an error in XML document ([line], [position]). (This was the inner exception to the above, and is common when an XML document doesn’t deserialize.)
  • The specified type was not recognized: name=’ArrayOfStrings’, namespace='[namespace]’, at [location]. (The was the inner exception of the above line, and explains where deserialization failed.)
  • Exceptions were thrown specifically at System.Xml.Serialization.XmlSerializationReader.GetPrimitiveType()
  • Undefined complexType ‘http://schemas.xmlsoap.org/soap/encoding/:Array’ is used as a base for complex type

The solution was to intercept the XML within WCF and deserialize it manually. There’s not a lot of instruction or examples for this online, so I thought I’d provide some here. Read the comments in the code carefully, and remember to change the return values in the Reference.cs file.

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click( object sender, EventArgs e )
        {
            MyServiceReference.MyService service = new MyServiceReference.MyService();

            // In this case we have HTTP authentication
            service.ClientCredentials.UserName.UserName = "username";
            service.ClientCredentials.UserName.Password = "password";

            // Add a behavior to the operations we want to override
            service.Endpoint.Contract.Operations.Find( "getArrayOfStrings" ).Behaviors.Add( new FormatterBahavior() );
            service.Endpoint.Contract.Operations.Find( "getSomthingElse" ).Behaviors.Add( new FormatterBahavior() );

            // Call the web services
            var result1 = service.getArrayOfStrings();
            var result2 = service.getSomthingElse();
        }

        // This operation behavior changes the formatter for a specific set of operations in a web service. 
        public class FormatterBahavior : IOperationBehavior
        {
            #region IOperationBehavior Members
            public void AddBindingParameters( OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters )
            { }

            public void ApplyClientBehavior( OperationDescription operationDescription, ClientOperation clientOperation )
            {
                // The client should use a different, custom formatter depending upon which operation is called.
                switch( operationDescription.Name )
                {
                    case "getArrayOfStrings":
                        clientOperation.Formatter = new MyCustomFormatter1( clientOperation.Formatter );
                        break;
                    case "getSomthingElse":
                        clientOperation.Formatter = new MyCustomFormatter2( clientOperation.Formatter );
                        break;
                }
            }

            public void ApplyDispatchBehavior( OperationDescription operationDescription, DispatchOperation dispatchOperation )
            { }

            public void Validate( OperationDescription operationDescription )
            { }
            #endregion
        }

        // This customized formatter intercepts the deserialization process and handles it manually.
        public class MyCustomFormatter1 : IClientMessageFormatter
        {
            // Hold on to the original formatter so we can use it to return values for method calls we don't need.
            private IClientMessageFormatter _InnerFormatter;

            public MyCustomFormatter1( IClientMessageFormatter innerFormatter )
            {
                // Save the original formatter
                _InnerFormatter = innerFormatter;
            }

            #region IClientMessageFormatter Members
            public object DeserializeReply( System.ServiceModel.Channels.Message message, object[] parameters )
            {
                // Create a new response object.
                MyCustomResponseObject retVal = new MyCustomResponseObject();
                System.Xml.XPath.XPathDocument doc = new System.Xml.XPath.XPathDocument( message.GetReaderAtBodyContents() );
                var nav = doc.CreateNavigator();

                // Pulling out the data we need from the XML
                foreach( System.Xml.XPath.XPathNavigator item in nav.Select...() )
                {
                    // Populate retVal with the data we need from the XML
                }

                // IMPORTANT: Be sure to change the return type of the operation (and also its interface) in
                // the service's Reference.cs file to object or you will get a cast error.
                return retVal;
            }

            public System.ServiceModel.Channels.Message SerializeRequest( System.ServiceModel.Channels.MessageVersion messageVersion, object[] parameters )
            {
                // Use the inner formatter for this so we don't have to rebuild it.
                return _InnerFormatter.SerializeRequest( messageVersion, parameters );
            }

            #endregion
        }

        public class MyCustomFormatter2 : IClientMessageFormatter
        {
            // ...
        }
    }

    public class MyCustomResponseObject
    {
        public List<Report> ReportList = new List<Report>();
    }

Here are some specifics on where the web service failed to pass the WS-I Basic profile:

  • R2110 specifies that soapend:Array declarations must not extend or restrict the soapenc:Array type.
    • ArrayOfStrings restricts this type. ArrayOfStrings is an array type, and the other complex types in the service depend on ArrayOfStrings.
  • R2111 specifies that soapenc:Array types must not use wsdl:arrayType attribute in the type declaration.
    • It was used in the only attribute of the only restriction on that type.
  • R2706 specifies that a wsdl:binding in a description must use the value of “literal” for the “use” attribute in body (and other) elements because the profile prohibits the use of encodings.
    • The body elements of the binding use the value “encoded” for the “use” attributes.
  • R2801 specifies that a description must use XML Schema 1.0 recommendations as the basis for user-defined types and structures.
    • The prefix “wsdl” in the attribute of ArrayOfStrings is unbound.
Share

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *