问题 使用Web服务客户端/ ClientBase检测无效的XML响应


我们目前正在使用Web服务(IBM Message Broker)。由于服务仍在开发中,在许多情况下它会返回无效的XML(是的,这将被修复,我承诺)。

当使用由svcutil使用生成的客户端从.NET调用此服务时,会出现此问题 ClientBase<T>。好像是 XmlSerializer used不会导致无效的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手动检查响应。

谢谢


7359
2017-07-22 08:28


起源

你可以尝试一下 消息检查器自己验证? - CodeCaster
@CodeCaster:虽然它可以工作(需要看看我是否拥有我需要的所有东西),但对于应该开箱即用的东西来说,这似乎是很多工作...... - leppie
@CodeCaster:我必须做一些蠢事,但接口方法就行了 IEndpointBehavior 永远不会被调用...从文档中看起来应该是以下工作: svc.Endpoint.Behaviors.Add(new ValidatingEndpointBehavior()); - leppie
@CodeCaster:修正:不要参考 InnerChannel 在应用行为之前! - leppie
经过几个小时的实验,我仍然没有进一步......你没有工作吗? - leppie


答案:


因此,开箱即用的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){}
}

您可以创建一些配置元素并通过配置应用行为,也可以通过修改客户端的通道工具以编程方式执行此操作 在打开客户端连接之前。这是程序化方法:

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

5
2017-07-24 12:22



我尝试过这种方法,但引用外部模式并不理想,因为服务合同可能正在改变(对我来说更多的工作,试图做更少的工作; p)。如果有办法获得 XmlSchemaSet 从生成的代码结构并使其工作(不适合我),这将是理想的解决方案。 - leppie
如果您正在使用WSDL,它将包含对服务公开的消息的XML模式的引用。您可能遇到的问题是该服务可能没有您尝试测试的元素排序的强约束。 - Paul Turner
似乎我不能扩展/拆分赏金:(我感谢你的努力,但纯粹基于代表点,我会选择较低代表的那个。抱歉。会尝试投票给你一些答案来弥补它:) - leppie
如果检测到“投票垃圾邮件”,请告诉我,如果需要,我会重做。 - leppie
你不用担心,leppie;我不是来这里的互联网点,只是很高兴你有一些有用的东西。 - Paul Turner


我建议使用相同的实现 悲剧 例如。创建一个添加到服务端点的客户端消息检查器,该检查器预先形成所有消息的模式验证。

使用本地服务架构进行动态验证

下面是动态加载最初从用于生成服务引用的服务中获取的模式的示例。这样,您始终可以更新服务,而无需更改此代码以使用架构验证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'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);

        }
    }
}

这样您就不需要每次都重新生成模式,因为它总是会获取最新的模式。


4
2017-07-29 09:17



动态引用模式可能不正确,除非客户端也将动态更新。这导致您可以使用多个版本的模式,并且最好始终错误而不是偶尔纠正。 - Paul Turner
刚才注意到,他说他不想手动进行更改。这样它是动态的...... - dmportella
我喜欢这种方法和@ Tragedian的方法,但是没有办法从生成的代码结构中获取模式 xsd.exe 呢? (嗯,确定有,但我没时间去挖掘它) - leppie
我有一个例子,所以一旦我开始工作,我会发布它。 - dmportella
@leppie您好我已经更新了我的答案,这应该是您现在想要的。 - dmportella


你可以配置 svcutil 与...进行序列化 DataContractSerializer

/serializer:DataContractSerializer

生成使用数据协定序列化程序进行序列化和反序列化的数据类型。

简短形式:/ ser:DataContractSerializer

DataContractSerializer 如果遇到错误排序的元素,它将抛出一个异常(有时是 关于元素排序的严格要求)或其他问题。


3
2017-07-24 11:32



这种方法很棒,但前提是你的合同与 DataContractSerializer。它们不能包含文档数据的XML属性,只能包含元素。 - Paul Turner
谢谢!我会尝试并报告:) - leppie
不幸的是,这是一个不行......它吐出关于标题,正文和错误的警告,并且没有生成任何东西:(示例: Warning: The optional WSDL extension element 'fault' from namespace 'http://schemas.xmlsoap.org/wsdl/soap/' was not handled. 我用错了吗? - leppie
看起来SvcUtil + DataContractSerializer对它接受的内容非常苛刻  - 查看架构参考的链接。我不认为IBM Message Broken可以生成不同的WSDL。例如。 SOAP 1.1或SOAP 1.2,您可以尝试这两种方法吗? - ta.speot.is
我可以试试,但我怀疑是否有人真正知道如何正确使用它; p - leppie


现在,我不是百分之百确定这一点,但我相信派对正在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或者 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建议很好。


2
2017-07-24 11:16



我或许可以反思一下 ServiceEndpoint 并修改 XmlSerialzer 的 MessageInfo.BodySerializer。有点深,但可行:D - leppie