本页内容
 Web 方法序列化 
 从 Web 方法中提取 XmlSerializer 
 XmlElement 和消息验证 
 更多控制...更困难的代码 

在 Tim Ewald 的 House of Web Services 专栏文章 Accessing Raw SOAP Messages in ASP.NET Web Services(发表于 MSDN Magazine 第三期)中,Tim 介绍了一种直接使用 SOAP Extension 来操作 SOAP 消息流的有趣方法。这可能是以基于流的方式访问原始 SOAP 消息的有用方法,并使您能够完全控制消息分析过程。但是,Tim 还提到了一种更方便的方法,使用该方法可获得一些相同的好处,但他同时警告,您将访问的数据已经由内部 ASMX 处理程序分析过一次。这种更方便的方法是:在 Web 方法中使用 XmlElement 参数。在本期的“为您服务”专栏中,我探讨了一些不同的方法,在这些方法中,对 Web 服务使用 XmlElement 参数可能是一种有用的访问原始 XML 数据的机制,同时,我还探讨了如何通过此机制获得对 Web 服务的高级控制。

Web 方法序列化
在我们详细讨论如何使用 XmlElement 参数操作 Web 方法之前,让我们首先看一下 Web 方法如何使用 .NET 框架中的 XmlSerializer 来调用方法和生成响应。图 1 象征性地说明了在收到 SOAP 消息的 XML 正文时所发生的事情。 

按此在新窗口打开图片
图 1. XmlSerializer 在标准 ASP.NET Web 方法调用中的作用


XML 被传递给 XmlSerializer,以便将其反序列化为托管代码中的类的实例,而这些实例将映射到 Web 方法的参数。同样,Web 方法的输出参数和返回值被序列化为 XML,以便创建 SOAP 响应消息体。

如果我们创建一个将两个整数加起来并返回结果的 Web 方法,则托管代码可能如下所示:

[WebMethod]
public int Add (int x, int y)
{
    return x + y;
}

发送给该 Web 方法的 SOAP 消息将如下所示:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <Add 
        xmlns="http://msdn.microsoft.com/AYS/XmlElementService">
      <x>3</x> 
      <y>2</y> 
    </Add>
  </soap:Body>
</soap:Envelope>

因此,XmlSerializer 用于将红色的 XML 转换为上述 Add 方法的参数。因为 integer 是类型,这一点是很明显的,但现在我们将看到一个复杂一些的示例。

假设我们有一个 Web 方法,它采用除数据类型以外的其他内容作为参数。请考虑以下 C# 代码:

public class MyClass
{
    public string child1;
    public string child2;
}

[WebMethod]
public void SubmitClass(MyClass input)
{
    // Do something with complex input parameters
    return;
}

该方法只采用一个参数,但该参数并不映射到简单的 XML 类型。实际上,它映射到一个复杂类型,该类型由以下 SOAP 信封中的输入元素表示:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <SubmitClass
      xmlns="http://msdn.microsoft.com/AYS/XEService">
      <input> 
        <child1>foo</child1> 
        <child2>bar</child2> 
      </input> 
    </SubmitClass>
  </soap:Body>
</soap:Envelope>

XmlSerializer 采用 input 元素作为参数,并将其反序列化为 MyClass 类型(该类型在调用 Web 方法之前,在该方法的代码中定义)的实例。

您应该记住的 .NET 框架中另外一个 Web 服务支持的要点是,开发 Web 服务使用者代码很容易。这一点能够成立的原因是:将会自动为您的 Web 方法创建 WSDL,以便详细描述 Web 服务接口的细节。如果您查看 ASP.NET 为该方法生成的 WSDL,您将注意到 input 元素已在 types 节中按如下方式定义:

<s:complexType name="MyClass">
  <s:sequence>
    <s:element minOccurs="0" maxOccurs="1" 
               name="child1" type="s:string" />
    <s:element minOccurs="0" maxOccurs="1" 
               name="child2" type="s:string" />
  </s:sequence>
</s:complexType>

Visual Studio?.NET 中的 Add Web Reference 支持将在客户端上定义一个与前面的 MyClass 类定义相符合的类,以使您的调用代码很像是在调用 Web 方法,就如同它是函数调用一样。下面列出了该方法调用。

localhost.XEService proxy = new localhost.XEService();
localhost.MyClass myInstance = new localhost.MyClass();
myInstance.child1 = "foo";
myInstance.child2 = "bar";
proxy.SubmitClass(myInstance);

XmlSerializer 还用在客户端上,以便将类实例序列化为与 WSDL 中定义的架构相符合的 XML。

返回页首
从 Web 方法中提取 XmlSerializer
在编写和使用 Web 服务时,XmlSerializer 的功能非常有用并且很强大。它使网络上的 XML 与托管代码中的类之间的关系变得透明。这一点通常会很有用,同时这也是 ASP.NET Web 方法成为编写 Web 服务最有效方法之一的一个重要原因。

但如果您不喜欢 XmlSerializer 的工作方式该怎么办?如果您希望对序列化和反序列化过程进行更多的控制该怎么办?或许您不喜欢这样一个事实:XmlSerializer 不会针对消息的架构对收到的 XML 进行验证。或许您希望能够选择对消息的哪些部分进行反序列化。或许您甚至根本不了解传入消息的架构是什么样子。是否有一种简便的方法能够避免在默认情况下使用 XmlSerializer,并且自行控制消息反序列化过程?存在几种解决该问题的方法:我们将集中讨论 XmlElement 参数的使用。

XmlElement 的功能
在前面的两个示例中,我们看到了参数和复杂类如何序列化为 Web 方法的参数。但如果参数本身就是 XML,会发生什么事情? 

请考虑以下 Web 方法:

[WebMethod]
public void SubmitXmlElement(XmlElement input)
{
    // Do something with the XML input
    return;
}

在该示例中,Web 方法采用 System.Xml.XmlElement 对象作为输入。下面的 SOAP 信封可用于向该 Web 服务发送消息。

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <SubmitXmlElement
        xmlns="http://msdn.microsoft.com/AYS/XEService">
      <input>
        <MyClass xmlns="">
          <child1>foo</child1>
          <child2>bar</child2>
        </MyClass>
      </input>
    </SubmitXmlElement>
  </soap:Body>
</soap:Envelope>

注意,我所发送的内容非常类似于我们发送到前面的 Web 方法的内容。然而,主要区别在于:由于 Web 方法的定义,XmlSerializer 将把参数反序列化为 XmlElement 对象而不是 MyClass 对象。事实是 XmlSerializer 将整个 SOAP 信封反序列化。当它确定 Web 方法的参数的类型是 XmlElement 时,反序列化过程将变得不再重要,而信息集的相应部分将以 XmlElement 的形式提供。

这种方法的不利之处在于:为该方法定义的自动 WSDL 现在将不会包含有关 input 参数结构的信息。毕竟,Web 方法并不定义 input 参数的结构。这既有好的一面,也有坏的一面。

当您在 Visual Studio .NET 中对该类型的方法使用 Add Web Reference 时,代理类上的 input 参数将被定义为 XmlNode(它是 XmlElement 的基类)。因此,我们可以向该方法发送我们喜欢的任何 XML。这一点很有意义,因为如果您查看 WSDL,就会看到 input 元素被定义为一个复杂类型,该类型的唯一元素的类型为 xsd:any。肯定在一些业务场合中,将原始的、没有架构的 XML 发送给 Web 服务确实是一件好事情,但在许多场合下这样做并不恰当。许多时候,我们的 Web 服务希望数据遵循一种或多种已知的格式。

因此,如果您了解参数的格式,为什么还要使用 XmlElement 呢?因为您现在可以自行控制反序列化过程,甚至可以不执行反序列化。这有可能改善性能,并且将有机会进行以下操作:执行 XML 变换、验证 XML 或执行基于内容的反序列化。

该问题的另外一个方面是如何定义自己的 WSDL,以便其公布它可以处理多个已知的参数类型。在此我们将不详细讨论该问题,但请参阅 Versioning Options,以获取有关以下内容的提示:编写能够公开多个参数类型的 Web 方法,及其对生成的 WSDL 产生何种影响。

返回页首
XmlElement 和消息验证
XmlSerializer 不会验证它所反序列化的 XML。事实表明,这通常不会是一个大问题。例如,如果某人发送了以下 XML:

<MyClass>
  <param2>foo</param2>
  <param1>bar</param1>
</MyClass>

根据前面为类定义的架构,它将不会通过验证。这里的问题在于,类型被定义为元素序列,这意味着顺序是重要的。当然,我们没有在代码中编写任何表明我们确实关心参数顺序的内容,因此如果发送给我们的 XML 没有经过严密的有效性检查,代码仍然可能没有问题。XML 验证过程的开销可能比较大,这很可能是默认情况下不对传入消息执行验证的原因。

然而,在某些场合下,对消息进行严格验证可能非常重要。例如,如果要对传入的 XML 执行 XPath 查询,对 XML 结构进行假设可能导致非常意外的结果。

因此,在将传入的 XML 反序列化为类实例之前,可以使用 XmlElement 对其进行验证。请考虑下面的 Web 方法:

[WebMethod]
public void SubmitValidatedTypedXml([XmlAnyElement]XmlElement input)
{
    XmlSchemaCollection schemaCollection = new XmlSchemaCollection();
    schemaCollection.Add("http://localhost/XEService/MyNewClass.xsd"
        "http://localhost/XEService/MyNewClass.xsd");
    // XmlValidatingReader only accepts XmlTextReader as input
    // which is why we pass in input.OuterXml instead of an instance
    // of XmlNodeReader.
    XmlValidatingReader validator
        = new XmlValidatingReader(input.OuterXml, 
                                  XmlNodeType.Element, 
                                  null);
    validator.Schemas.Add(schemaCollection);
    XmlSerializer serializer 
        = new XmlSerializer(typeof(MyNewClassType));
    MyNewClassType newInstance 
        = (MyNewClassType)serializer.Deserialize(validator);
    // Do something with MyNewClassType object
}

该 Web 方法做了几件事情。其目标是获取传递给 Web 方法的 XmlElement,并手动将其作为名为 MyNewClassType 的类的实例进行反序列化。要完成该过程的这一部分,请创建 XmlSerializer 类的实例,指示作为构造函数的参数所涉及的类型,然后调用 Deserialize 方法。

但在这种情况下,我们还做了另外一件事情。Deserialize 方法将 XmlReader 对象作为输入。我们可能已经创建了 XmlNodeReader 类(该类继承 XmlReader)的实例,并将其传递给 Deserialize 方法。然而,在此情况下,我们将 XmlValidatingReader 类的实例传递给了 Deserialize 方法。您通过 .NET 框架验证 XML 的方法是使用 XmlValidatingReader。我们通过向 XmlValidatingReader 传递 input 参数的 XML 片段,创建了它的实例。为了针对架构来验证 XML,验证程序需要加载该架构,以便了解针对什么来进行验证。通过向架构集合中添加已知的架构,可以实现这一目标。

XML 的最终验证发生于对 XmlSerializer 对象调用 Deserialize 方法时。这是因为 XmlValidatingReader 也派生自 XmlReader,对所包含的 XML 的验证是随着通过 XmlReader 接口读取各种节点而发生的。在 Deserialize 方法遍历所有节点以创建 MyNewClassType 类的实例时,将读取这些节点。

MyNewClassType 类与前面创建的 MyClass 类非常类似,不同之处在于:我使用 Visual Studio 对创建 XSD 文件的支持来定义 XML 架构,从而创建了该类,然后使用 XSD.exe 实用工具并通过以下命令行创建了一个托管类:

Xsd /c MyNewClass.xsd

通过这种方式,我将 XSD 架构传递给 XmlValidatingReader,同时传递的还有将 XML 反序列化所得到的类代码。

在该 Web 方法中,还有另外一个在上一版本中未曾包括的重要之处。在该方法中,我们用 XmlAnyElement 参数修饰了 input 参数。这表明该参数将从 xsd:any 元素进行反序列化得到。因为未向该属性传递任何参数,这意味着 SOAP 消息中该参数的整个 XML 元素将位于由该 XmlElement 参数表示的信息集中。这与前面的 Web 方法稍有不同。让我们看一下区别在哪里。

XmlAnyElement 属性
要了解 XmlAnyElement 属性的工作方式,让我们看一下以下两个 Web 方法:

// Simple Web method using XmlElement parameter
[WebMethod]
public void SubmitXml(XmlElement input)
{return;}

// Simple Web method...using the XmlAnyElement attribute
[WebMethod]
public void SubmitXmlAny([XmlAnyElement] XmlElement input)
{return;}

这两个方法之间的区别在于:对于第一个方法 SubmitXml,XmlSerializer 期望名为 input 的元素是 SOAP 体中 SubmitXml 元素的直接子元素。而第二个方法 SubmitXmlAny 则不关心 SubmitXmlAny 元素的子元素的名称是什么。它将所包含的任何 XML 插入到 input 参数中。ASP.NET 帮助中有关这两种方法的消息样式如下所示。首先,我们看一下不带 XmlAnyElement 属性的方法的消息。

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SubmitXml xmlns="http://msdn.microsoft.com/AYS/XEService">
      <input>xml</input>
    </SubmitXml>
  </soap:Body>
</soap:Envelope>

现在,我们看一下使用 XmlAnyElement 属性的方法的消息。

<!-- SOAP message for method using XmlAnyElement -- >
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SubmitXmlAny xmlns="http://msdn.microsoft.com/AYS/XEService">
      Xml 
    </SubmitXmlAny>
  </soap:Body>
</soap:Envelope>

用 XmlAnyElement 属性修饰的方法少一个包装元素。只有与该方法同名的元素包装了传递给 input 参数的内容。

如果您希望为您的 Web 方法控制 XmlSerializer 的反序列化过程,使用 XmlAnyElement 是一个不错的窍门,藉此可通过您的自定义逻辑处理消息体的更多内容。但是,我们能否获取传递给 XmlElement 实例的消息体的更多内容?事实上我们能够做到这一点,方法就是使用 SoapDocumentMethod 属性的 ParameterStyle 属性。

[WebMethod]
[SoapDocumentMethod(ParameterStyle = SoapParameterStyle.Bare)]
public void SubmitBody([XmlAnyElement]XmlElement input)
{
    return;
}

通过将 ParameterStyle 属性设置为 SoapParameterStyle.Bare 并使用 XmlAnyElement,现在可将消息样式更改为以下形式:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>xml</soap:Body>
</soap:Envelope>

现在,SOAP 体的全部内容都将被传递给我们的输入参数。

上述机制在某些方面可能非常有用,但我们应该记住 SOAP 体本身并不是 XML 文档。特别是,它并非必须具有单个根元素。实际上,WS-I 基本配置文件的确指明 Body 应该具有单个子元素;但是,这一点并不能得到保证。如果我们确实要访问传递给 Web 方法的原始 XML,我们需要考虑消息体可能具有多个子元素的情形,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <name>Fred</name>
    <name>Wilma</name>
    <name>Betty</name>
    <name>Barney</name>
  </soap:Body>
</soap:Envelope>

结果表明,SubmitBody 方法可以接受这种消息,只是没有办法访问所有数据。当消息被反序列化后,您只能通过 input 参数访问列表中的最后一个元素。

我们可以通过将单个 XmlElement 输入更改为 XmlElements 数组来处理这种情形。以下方法将能够读取发送给它的任何 SOAP 消息的整个消息体。

[WebMethod]
[SoapDocumentMethodAttribute(ParameterStyle = SoapParameterStyle.Bare)]
public void SubmitAnything([XmlAnyElement]XmlElement [] inputs)
{
    return;
}

返回页首
更多控制...更困难的代码
我们已经解决了如何从 ASP.NET Web 方法直接访问 XML 中的 SOAP 体的全部内容这一问题。这将使您有机会做很多事情,例如,在反序列化 XML 之前针对架构对其进行验证,避免首先执行反序列化,分析 XML 以确定希望如何对其进行反序列化,以及使用许多功能强大的 XML API 直接处理 XML。这还使您能够以自己的方式处理错误,而不是使用 XmlSerializer 可能在内部产生的错误。 

对于那些希望对发送给 Web 方法的 XML 进行低级访问的读者来说,通过将 XmlElement 属性与本文介绍的一些 Web 方法属性窍门结合使用,可以在 Web 服务中获得更多控制,但这的确需要编写更多的代码,并且需要熟悉 .NET 框架中提供的一些基本 XML 技术。不过,.NET 框架的绝妙之处就在于它使开发人员能够灵活地进行许多低级控制,同时又为创建和使用 Web 服务提供了简单而有效的开发环境。