Обнаружение неверного ответа XML с клиентом веб-службы/ClientBase

В настоящее время мы используем веб-службу (IBM Message Broker). Поскольку служба все еще находится в разработке, во многих случаях она возвращает недопустимый XML (да, это будет исправлено, я обещаю).

Проблема возникает при вызове этой службы из .NET с использованием клиента, сгенерированного svcutil, с помощью ClientBase<T>. Похоже, что используемое XmlSerializer не является ошибкой в ​​недопустимых XML-элементах.

Вот пример того, что не сообщит о неисправности, и просто верните частично инициализированный элемент:

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);
    }
  }
}

При подключении к событию UnknownElement он корректно сообщает о недопустимом XML (порядок элементов не совпадает), но при использовании ClientBase<T> эти (и некоторые другие случаи) просто игнорируются (как если бы они не использовали ошибку события XmlSerializer).

Мой вопрос: как я могу сделать ClientBase<T> обнаружение недопустимого XML? Есть ли способ подключиться к событиям сбоя XmlSerializer, используемым ClientBase<T>?

В настоящее время мы должны вручную проверять ответы с использованием SoapUI, если что-то не имеет смысла.

Спасибо

Ответ 1

Таким образом, WCF не верит в валидацию XML. Он рассматривает XML как формат сообщения, считывая информацию, которая кажется правильной, и игнорируя остальные. Преимущество этого подхода в том, что он очень либерален в том, что принимает служба.

Проблема возникает, когда такие вещи, как порядок элементов начинают иметь значение. Можно утверждать, что упорядочение структур не должно быть важным, что вы можете указать порядок с информацией в самих данных (например, даты, время или свойства индекса). В вашем тривиальном случае порядок на самом деле не имеет значения, так как вы можете читать и понимать информацию независимо от того, в каком порядке она представлена. Я уверен, что ваш фактический вариант намного более действителен, поэтому я не буду вдаваться в подробности.

Для проверки структуры XML вам необходим доступ к сообщению в конвейере WCF. Самый простой способ - использовать имплементацию IClientMessageInspector, которая проверяет сообщение и прикрепляет его к вашему клиенту с помощью поведения.

Предполагая, что вы хотите сделать это с проверкой XML-схемы на XSD, вы должны создать инспектор, подобный следующему:

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) {}
}

Сопутствующее валидационное поведение не особенно сложно:

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){}
}

Вы можете либо создать некоторые элементы конфигурации и применить поведение через config, либо сделать это программно, изменив фабрику клиентских каналов перед открытием клиентского соединения. Здесь программный подход:

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

Ответ 2

Я бы предложил ту же реализацию, что и Tragedian, например. создайте клиентский инспектор сообщений, который добавляется к конечной точке службы, который выполняет предварительную проверку схемы всех входящих сообщений.

Динамическая проверка с помощью локальной схемы обслуживания

Ниже приведен пример для динамической загрузки схемы, изначально выбранной из службы, которая использовалась для создания ссылки на службу. Таким образом, вы всегда можете обновить сервис и никогда не менять код для проверки XML с помощью схемы.

При этом используется ссылка "Служба", необходимая для загрузки существующей схемы в ваше решение (эту информацию о схеме можно увидеть в папке ServiceReference внутри вашего проекта с помощью проводника файлов.

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);
        }
    }
}

Динамическая проверка схемы конечной точки службы

Но в соответствии с вашим комментарием о том, что нет необходимости изменять жестко запрограммированную схему и/или объекты, которые я добавил ниже, вы можете автоматически получить схему динамически из конечной точки службы. Я бы посоветовал вам кешировать это.

Вы даже можете использовать это, чтобы определить, изменилась ли конечная точка обслуживания, например. Когда вы впервые получаете ссылку на службу, вы сохраняете ее на диск и генерируете свои сообщения, тогда служба может динамически получать схему из конечной точки службы каждый день, проверять любые изменения или различия и уведомлять вас или регистрировать любые ошибки.

Ниже приведен пример того, как это сделать.

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

        }
    }
}

Таким образом, вам не нужно каждый раз перегенерировать схему, так как она всегда подбирает последнюю схему.

Ответ 3

Вы можете настроить svcutil для выполнения сериализации с помощью DataContractSerializer:

/serializer:DataContractSerializer

Создает типы данных, которые используют Сериализатор Контрактов Данных для сериализации и десериализации.

Короткая форма:/ser: DataContractSerializer

DataContractSerializer выдаст исключение, если встретит ошибочно упорядоченный элемент (иногда больно строгий порядок элементов) или другие проблемы.

Ответ 4

Теперь я не уверен в этом на 100%, но я верю, что эта вечеринка происходит в файле XmlSerializerOperationFormatter.cs(System.ServiceModel),

а именно в DeserializeBody:

private object DeserializeBody(XmlDictionaryReader reader, MessageVersion version, XmlSerializer serializer, MessagePartDescription returnPart, MessagePartDescriptionCollection bodyParts, object[] parameters, bool isRequest)
{
  try
  {
    if (reader == null)
      throw DiagnosticUtility.ExceptionUtility.ThrowHelperError((Exception) new ArgumentNullException("reader"));
    if (parameters == null)
      throw DiagnosticUtility.ExceptionUtility.ThrowHelperError((Exception) new ArgumentNullException("parameters"));
    object obj = (object) null;
    if (serializer == null || reader.NodeType == XmlNodeType.EndElement)
      return (object) null;
    object[] objArray = (object[]) serializer.Deserialize((XmlReader) reader, this.isEncoded ? XmlSerializerOperationFormatter.GetEncoding(version.Envelope) : (string) null);
    int num = 0;
    if (OperationFormatter.IsValidReturnValue(returnPart))
      obj = objArray[num++];
    for (int index = 0; index < bodyParts.Count; ++index)
      parameters[((Collection<MessagePartDescription>) bodyParts)[index].Index] = objArray[num++];
    return obj;
  }
  catch (InvalidOperationException ex)
  {
    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError((Exception) new CommunicationException(System.ServiceModel.SR.GetString(isRequest ? "SFxErrorDeserializingRequestBody" : "SFxErrorDeserializingReplyBody", new object[1]
    {
      (object) this.OperationName
    }), (Exception) ex));
  }

как вы можете видеть, никто не подключается к XmlSerializer.UnknownElement. Хотя, опять же, мы не можем сказать этого, потому что XmlSerializer передается через параметр. Короче; он исходит из свойства replyMessageInfo.BodySerialize r или requestMessageInfo.BodySerializer, который является частью XmlSerializerOperationFormatter.cs, они исходят из конструктора XmlSerializerOperationFormatter.

Несколько шагов дальше, и.. хорошо 20983832972389 шаг дальше, поскольку исходный код - безумие. В принципе, это приводит к тому, что я не вижу ничего, что применимо к XmlSerializer, что вроде бы указывает на то, что вы только что сказали.

Возможные решения:

1) Используйте XmlSerializerOperationBehavior в качестве базы и напишите свой собственный "пользовательский сериализатор". Это отличный пример, как написать собственный сериализатор: http://code.google.com/p/protobuf-net/source/browse/trunk/protobuf-net/ServiceModel/

Возможно, вы сможете повторно использовать некоторые части в XmlSerializerOperationBehavior. Может быть, добавить какие-то сообщения об ошибках.

2) Я никогда не был поклонником проверки Xml через XmlSerializer.

XmlSerializer предназначен для сериализации/десериализации объектов, что он. Частично построенный объект - это кошмар. То, что я настоятельно рекомендую (и то, что я слежу за собой при использовании XmlSerializer), - это фактически проверить XML на схему и THEN deserialize.

Все в стороне, предложение @CodeCaster приятно.