Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detecting invalid XML response with web service client/ClientBase

Tags:

c#

.net

xml

We are currently consuming a web service (IBM Message Broker). As the service is still under development, in many cases it returns invalid XML (yes, this will be fixed, I am promised).

The problem comes in when calling this service from .NET, using a client generated by svcutil using ClientBase<T>. It seems the XmlSerializer used is not faulting on invalid XML elements.

Here is an example of what fails to report a fault, and just return a partially initialized element:

using System;
using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Xml.Serialization;

[Serializable]
public class Program
{
  [XmlElement(Order = 0)]
  public string One { get;set; }

  [XmlElement(Order = 1)]
  public string Two { get;set; }

  static void Main(string[] args)
  {
    var ser = new XmlSerializer(typeof(Program));
    ser.UnknownElement += (o, e) => { 
      Console.WriteLine("Unknown element: {0}", e.Element.Name); 
    };

    using (var input = new StringReader(
@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<Program>
  <Two>Two</Two>
  <One>One</One>
</Program>"))
    {
      var p = (Program)ser.Deserialize(input);
      Debug.Assert(p.One != null);
    }
  }
}

When attaching to the UnknownElement event, it correctly reports the invalid XML (element order does not match), but when using ClientBase<T>, these (and some other cases) are simply ignored (as if not using the fault events of XmlSerializer).

My question is how can I make ClientBase<T> detect invalid XML? Is there a way to hook into the fault events of the XmlSerializer used by ClientBase<T>?

Currently we have to manually check responses using SoapUI if something does not make sense.

Thanks

like image 884
leppie Avatar asked Jul 22 '14 08:07

leppie


2 Answers

So, out-of-the-box, WCF doesn't believe in XML validation. It treats the XML as a message format, reading the information out which appears correct and ignoring the rest. This has the advantage of being very liberal in what the service will accept.

The trouble comes when things like the ordering of elements start to matter. It could be argued that ordering of the structures shouldn't be important, that you can indicate ordering with information in the data itself (dates, times or index properties, for example). In your trivial case, the ordering doesn't actually matter, since you can read and comprehend the information regardless of the order it's presented in. I am sure your actual case is much more valid, so I won't labour this point further.

In order to validate the XML structure, you need access to the message in the WCF pipeline. The easiest way in is to use an IClientMessageInspector impementation which validates the message and attach it to your client using a behaviour.

Assuming you want to do this with XML schema validation against an XSD, you would create an inspector like this:

class XsdValidationInspector : IClientMessageInspector
{
    private readonly XmlSchemaSet _schemas;

    public XsdValidationInspector(XmlSchemaSet schemas)
    {
        this._schemas = schemas;
    }

    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
        // Buffer the message so we can read multiple times.
        var buffer = reply.CreateBufferedCopy();

        // Validate the message content.
        var message = buffer.CreateMessage();

        using (var bodyReader
            = message.GetReaderAtBodyContents().ReadSubTree())
        {
            var settings = new XmlReaderSettings
            {
                Schemas = this._schemas,
                ValidationType = ValidationType.Schema,
            };

            var events = new List<ValidationEventArgs>();
            settings.ValidationEventHandler += (sender, e) => events.Add(e);

            using (var validatingReader
                = XmlReader.Create(bodyReader, settings))
            {
                // Read to the end of the body.
                while(validatingReader.Read()) {  }
            }

            if (events.Any())
            {
                // TODO: Examine events and decide whether to throw exception.
            }
        }

        // Assign a copy to be passed to the next component.
        reply = buffer.CreateMessage();
    }

    public object BeforeSendRequest(
        ref Message request,
        IClientChannel channel) {}
}

The accompanying validation behaviour isn't especially complicated:

class XsdValiationBehavior : IEndpointBehavior
{
    private readonly XmlSchemaSet _schemas;

    public XsdValidationBehavior(XmlSchemaSet schemas)
    {
        this._schemas = schemas;
    }

    public void AddBindingParameters(
        ServiceEndpoint endpoint,
        BindingParameterCollection bindingParameters) {}

    public void ApplyClientBehavior(
        ServiceEndpoint endpoint,
        ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(
            new XsdValidationInspector(this._schemas));
    }

    public void ApplyDispatchBehavior(
        ServiceEndpoint endpoint,
        EndpointDispatcher endpointDispatcher) {}

    public void Validate(ServiceEndpoint endpoint){}
}

You can either create some configuration elements and apply the behaviour via config, or you can do so programatically by modifying the client's channel factory before you open the client connection. Here's the programmatic approach:

var schemaMarkup =  @"<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema'>
       <xsd:element name='Program'>
        <xsd:complexType>
         <xsd:sequence>
          <xsd:element name='One' minOccurs='1' maxOccurs='1'/>
          <xsd:element name='Two' minOccurs='1' maxOccurs='1'/>
         </xsd:sequence>
        </xsd:complexType>
       </xsd:element>
      </xsd:schema>";

var schema = new XmlSchema();
using (var stringReader = new StringReader(schemaMarkup));
{
    var events = new List<ValidationEventArgs>();
    schema.Read(stringReader, (sender, e) => events.Add(e));

    // TODO: Check events for any errors.
}

var validation = new XsdValidationBehavior(new XmlSchemaSet { schema });

client.ChannelFactory.Behaviours.Add(validation);
like image 152
Paul Turner Avatar answered Oct 04 '22 10:10

Paul Turner


I would suggest the same implementation as Tragedian eg. create a a client message inspector that is added to the service endpoint which preforms the schema validation of all messages coming in.

Dynamic validation with Local Service Schema

Below is an example for dynamically loading the schema fetched originally from the service that was used to generate the service reference. This way you can always update the service and never have to change this code to validate the xml with the schema.

This uses the Service reference you have to load the existing schema on your solution (you can see that schema information in the ServiceReference Folder inside your project using a file explorer.

using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml.Schema;

namespace ConsoleApplication1
{
    class Program
    {
        class XsdValidationInspector : IClientMessageInspector ... //omitted for clarity
        class XsdValiationBehavior : IEndpointBehavior ... //omitted for clarity

        static void Main(string[] args)
        {
            ContractDescription cd = ContractDescription.GetContract(typeof(ServiceReference1.IService1));

            WsdlExporter exporter = new WsdlExporter();

            exporter.ExportContract(cd);

            XmlSchemaSet set = exporter.GeneratedXmlSchemas;

            // Client implementation omitted for clarity sake.
            var client = <some client here>; //omitted for clarity

            var validation = new XsdValidationBehavior(new XmlSchemaSet { xmlSchema });

            client.ChannelFactory.Behaviours.Add(validation);
        }
    }
}

Dynamic Check of Service End Point Schema

But in line with your comment about not having to change the hard-coded schema and or objects I have added below a way for you to automatically get the schema dynamically from the service end point. I would suggest that you cache this.

You can even use this to identify if the service end point has changed eg. when you first get the reference for the service you save it to disk and generate your messages then the service can get the schema dynamically from the service end point every day and check for any modifications or differences and notify you or log any errors.

See below an example of how to do this.

using System;
using System.IO;
using System.Net;
using System.Web.Services.Description;
using System.Text;
using System.Xml.Schema;

namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            //Build the URL request string
            UriBuilder uriBuilder = new UriBuilder(@"http://myservice.local/xmlbooking.asmx");
            uriBuilder.Query = "WSDL";

            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uriBuilder.Uri);
            webRequest.ContentType = "text/xml;charset=\"utf-8\"";
            webRequest.Method = "GET";
            webRequest.Accept = "text/xml";

            //Submit a web request to get the web service's WSDL
            ServiceDescription serviceDescription;
            using (WebResponse response = webRequest.GetResponse())
            {
                using (Stream stream = response.GetResponseStream())
                {
                    serviceDescription = ServiceDescription.Read(stream);
                }
            }

            Types types = serviceDescription.Types;
            XmlSchema xmlSchema = types.Schemas[0];

            // Client implementation omitted for clarity sake.
            var client = some client here;

            var validation = new XsdValidationBehavior(new XmlSchemaSet { xmlSchema });

            client.ChannelFactory.Behaviours.Add(validation);

        }
    }
}

This way you don't need to regenerate the schema every time as it will always pick up the latest schema.

like image 26
dmportella Avatar answered Oct 04 '22 11:10

dmportella