通过web服务客户机/ClientBase检测无效的XML响应

本文关键字:无效 XML 响应 检测 ClientBase web 服务 客户机 通过 | 更新日期: 2023-09-27 18:14:10

我们当前正在使用web服务(IBM Message Broker)。由于该服务仍在开发中,在许多情况下它会返回无效的XML(是的,这将得到修复,我保证)。

当使用svcutil使用ClientBase<T>生成的客户端从. net调用此服务时,问题就出现了。看来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?是否有一种方法可以钩入ClientBase<T>使用的XmlSerializer的故障事件?

目前,我们必须使用SoapUI手动检查响应,如果有些东西没有意义。

谢谢

通过web服务客户机/ClientBase检测无效的XML响应

所以,开箱即用,WCF不相信XML验证。它将XML视为一种消息格式,读取看起来正确的信息,而忽略其余的信息。这样做的好处是,服务可以接受的内容非常自由。

当元素的顺序开始起作用时,问题就来了。可能有人认为,结构的顺序不应该很重要,您可以使用数据本身的信息(例如日期、时间或索引属性)来指示顺序。在您的示例中,顺序实际上并不重要,因为无论信息呈现的顺序如何,您都可以阅读和理解信息。我相信你的实际的情况更有效,所以我就不再详述这一点了。

为了验证XML结构,需要访问WCF管道中的消息。最简单的方法是使用IClientMessageInspector实现,它验证消息并使用行为将其附加到您的客户端。

假设您希望对XSD进行XML模式验证,您将创建一个检查器,如下所示:

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

我建议与悲剧eg相同的实现。创建一个添加到服务端点的客户端消息检查器,该检查器对传入的所有消息进行模式验证。

使用本地服务模式进行动态验证

下面是动态加载最初从用于生成服务引用的服务获取的模式的示例。这样,您就可以随时更新服务,而不必更改此代码以使用模式验证xml。

这将使用Service引用来加载解决方案上的现有模式(您可以使用文件资源管理器在项目中的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'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);
        }
    }
}

这样你就不需要每次都重新生成模式,因为它总是选择最新的模式。

您可以配置svcutil来执行与DataContractSerializer的序列化:

/serializer:DataContractSerializer

生成使用数据契约序列化器进行序列化和反序列化的数据类型。

简写形式:/ser:DataContractSerializer

DataContractSerializer如果遇到元素顺序错误(有时对元素顺序严格得令人痛苦)或其他问题,将抛出异常。

现在,我不是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)我从来不喜欢通过XmlSerializer进行Xml验证。

XmlSerializer是用来序列化/反序列化对象的,就是这样。部分构造的对象是一场噩梦。我强烈建议(也是我自己在使用XmlSerializer时所遵循的),是根据模式实际验证XML,然后进行反序列化。

@CodeCaster的建议很好。