介绍
目前在微软.NET中实现基于HTTP的Web服务有两种根本不同的途径。最底层的技术是编写一个插入.NET HTTP管道的自定义IHttpHandler类。这种途径要求你使用System.Web API来处理输入的HTTP消息,使用System.Xml API处理HTTP体中的SOAP封装。编写自定义处理程序要求你手工建立正确描述实现的WSDL文档。严格执行所有的这些操作要求你非常了解XML、XSD、SOAP和WSDL规范,但是这对于大多数开发者来说很困难。
实现Web服务的效率更高的途径是使用微软ASP.NET WebMethods框架组件。ASP.NET为.asmx终结点(称为WebServiceHandler)发布了一个特定的IHttpHandler类,它提供必要的XML、XSD、SOAP和WSDL功能的范本文件。因为WebMethods框架组件把你从下层XML技术的复杂性中解放了出来,你能够快速聚焦于手头的业务问题。
图1:灵活性和生产率之间的折衷
在实现技术之间作出选择形成了图1中所示的灵活性和生产率之间的折衷。编写自定义的IhttpHandler给了你无限大的灵活性,但是要花费你很长时间编写、测试和调试代码。WebMethods框架组件使建立自己的Web服务和快速运行变得很容易,但是很明显你要受到该框架组件界限的限制。但是,在WebMethods框架组件不能提供你完全需要的信息的情况下,可以通过添加自己的附加功能来扩展该框架组件。
通常,除非你已经掌握了XML、XSD、SOAP和WSDL并且原意承担直接处理它们的负担,最好不要专注于Web服务所需要的WebMethods框架组件。它提供了大多数Web服务端点(endpoints)所需要的基本服务,以及能把框架组件进行"弯曲"来适合你的精确的需要的一些有趣的可扩充能力。本文是基于这种假定讨论WebMethods如何工作的内部信息。如果你不太了解XML Schema和SOAP,可以参阅Understanding XML Schema和Understanding SOAP。
WebMethods框架组件
WebMethods框架组件循环地把SOAP消息映射到.NET类中的方法。这种功能的实现首先需要把你的方法注解为System.Web.Services名字空间中的[WebMethod]属性。例如,下面的.NET类包含四个方法,其中两个使用[WebMethod]属性注解了:
using System.Web.Services;
public class MathService
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
public double Subtract(double x, double y) {
return x - y;
}
public double Multiply(double x, double y) {
return x * y;
}
public double Divide(double x, double y) {
return x / y;
}
}
为了在WebMethods框架组件重使用这个类,你需要把这个类编译为一个部件(assembly)并把它复制到虚拟目录的bin目录中。在这个例子中,Add和Subtract方法可以被暴露作为Web服务操作,但是Multiply和Divide却不能(因为它们没有使用[WebMethod]标记)。
你可以通过一个.asmx端点(endpoint)把Add和Subtract暴露为Web服务操作。为了实现这个功能,建立一个名为Math.asmx的包含下面的简单声明的新文本文件,并把它放到包含该部件的虚拟目录中(注意:它自己进入虚拟目录而不是子目录bin中):
<%@ WebService class="MathService"%>
这个声明告诉.asmx处理程序使用哪个类检查WebMethods,并且该处理程序自动处理其它的信息。例如,假定虚拟目录叫作"math"并且它包含了Math.asmx,并且bin子目录包含了该部件,那么浏览http://localhost/math/math.asmx将导致.asmx处理程序生成图2所示的文档页面。
这与.asmx处理程序如何工作有较大的变化。.asmx文件通常只包含通过名字引用Web服务类(如上所示)的WebService声明。因此,在这种情况下,该部件必须已经被编译好、配置到了虚拟目录的bin目录中。.asmx处理程序也提供.asmx文件中源代码的just-in-time(实时)编译。例如,下面的文件(叫作Mathjit.asmx)包含了WebService声明和被引用类的源代码。
<@% WebService class="MathServiceJit" language="C#"%>
using System.Web.Services;
public class MathServiceJit
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
public double Subtract(double x, double y) {
return x - y;
}
public double Multiply(double x, double y) {
return x * y;
}
public double Divide(double x, double y) {
return x / y;
}
}
第一次通过HTTP访问这个文件时,.asmx处理程序编译源代码并把部件配置到正确的位置。注意WebService声明必须提供语言,这样.asmx处理程序才能在运行时选择正确的编译器。这种方法的一个明显的问题是直到你第一次访问该文件时才会发现编译错误。
图2:MathService文档
当你使用Visual Studio .NET建立一个新的Web服务项目时,它通常使用"双文件"技术,把源文件与引用它的.asmx文件分开。集成开发环境(IDE)隐藏了这些文件,但是你可以点击"解决方案浏览器"工具条上的Show All Files(显示所有文件),你会发现项目中的每个Web服务类都有两个文件。实际上,Visual Studio .NET并不支持.asmx文件的高亮度提醒或IntelliSense。有了Web项目后,Visual Studio .NET也处理建立虚拟目录并自动把部件编译到虚拟目录的bin目录中。
在深入分析.asmx处理程序如何工作前,我们先简短讨论一下来自IIS的消息如何分派到用于处理的.asmx处理程序中。当输入的HTTP消息到达80端口时,IIS使用自己的元数据库(metabase)的信息来找出使用哪一个ISAPI DLL来处理这个消息。.NET安装程序把.asmx扩展映射到Aspnet_isapi.dll,如图3所示。
图3:.asmx 的IIS应用程序映射
Aspnet_isapi.dll是.NET框架组件提供的一个标准的ISAPI扩充,它简单地把HTTP请求转发到单独的叫作Aspnet_wp.exe的工作进程中。Aspnet_wp.exe寄宿了通用语言运行时和.NET HTTP管道。一旦消息进入.NET HTTP管道,管道就查询配置文件,看应该使用哪一个IhttpHandler类处理给定的扩充。如果你查看Machine.config文件,你会发现它包含了.asm文件的一个httpHandler映射,如下所示:
<configuration>
<system.web>
<httpHandlers>
<add verb="*" path="*.asmx"
type="System.Web.Services.Protocols.WebServiceHandlerFactory,
System.Web.Services, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" validate="false"/>
...
因此当某个目标为.asmx文件的消息进入.NET HTTP管道时,该管道调用WebServiceHandlerFactory类来实例化用于处理这个请求的新的WebServiceHandler对象。WebServiceHandler对象打开物理的.asmx文件以决定包含WebMethods的类的名称。
一旦.asmx处理程序被.NET HTTP管道调用,它就开始处理XML、XSD、SOAP和WSDL进程。.asmx处理程序提供的功能可以分为三个部分:1)消息发送;2)把XML映射到对象;3)自动化WSDL和文档生成。下面讲详细讲解每一部分。
消息发送
当HTTP管道调用.asmx处理程序时,它通过查看.asmx文件中的WebService声明来找出使用哪个.NET类来检查。接着它查看输入的HTTP消息的信息以正确地决定调用被引用类地哪个方法。为了调用前面的例子中显示的Add操作,输入的HTTP消息必须有类似下面的信息:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://tempuri.org/Add"
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Add xmlns="http://tempuri.org/">
<x>33</x>
<y>66</y>
</Add>
</soap:Body>
</soap:Envelope>
在输入的HTTP消息中的确有两部分信息可以用于找出调用类中的哪个方法:SOAPAction头部或者请求的元素名称(例如soap:Body元素内的元素名称)。在这种情况下,任何一个都表明了发送者希望调用的方法的名称。
默认情况下.asmx处理程序使用SOAPAction头部的值来执行消息发送。因此,.asmx 处理程序查看消息中的SOAPAction头部,接着使用.NET反射(reflection)检查被引用类中的方法。它只考虑使用[WebMethod]标志标记的方法,但是它通过查看每个方法的SOAPAction值来正确地决定调用哪个方法。因为我们没有在自己的类的方法中明确指定SOAPAction的值,.asmx处理程序就假定SOAPAction的值是Web服务的名字空间后面跟上方法名称的组合。因为我们也没有指定名字空间,处理程序就把http://tempuri.org作为默认值。因此Add方法的默认的SOAPAction值是http://tempuri.org/Add。
你可以使用[WebService]标志作为类的注解来自定义Web服务的名字空间,使用下面所说明的[SoapDocumentMethod]标志作为WebMethods的注解来指定SOAPAction的值:
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="http://example.org/math")]
public class MathService
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
[SoapDocumentMethod(Action="urn:math:subtract")]
public double Subtract(double x, double y) {
return x - y;
}
...
}
现在.asmx处理程序认为Add 方法的SOAPAction的值为http://example.org/math/Add(使用默认的启发式),Subtract方法的为urn:math:subtract(因为我们明确地定义了这个值)。例如,下面的HTTP请求消息调用Subtract操作:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "urn:math:subtract"
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Subtract xmlns="http://example.org/math">
<x>33</x>
<y>66</y>
</Subtract>
</soap:Body>
</soap:Envelope>
如果.asmx处理程序无法查找到匹配输入的HTTP消息的SOAPAction,它简单地抛出一个异常(后面将解释异常如何处理)。如果你不愿意依赖SOAPAction头部来进行方法调度,你可以指示.asmx处理程序通过使用[SoapDocumentService]标志的RoutingStyle属性作为类的注解来使用请求元素地名称。如果你这样做了,你大概也会通过把SOAPAction的值设置为空字符串来表明WebMethods不需要SOAPAction值:
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="http://example.org/math")]
[SoapDocumentService(
RoutingStyle=SoapServiceRoutingStyle.RequestElement)]
public class MathService
{
[WebMethod]
[SoapDocumentMethod(Action="")]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
[SoapDocumentMethod(Action="")]
public double Subtract(double x, double y) {
return x - y;
}
...
}
在这种情况下,处理程序不会查看SOAPAction值--它使用请求元素的名称作为代替。例如,它认为Add方法的请求元素的名称为Add(来自http://example.org/math名字空间),下面演示了这种HTTP请求消息:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: ""
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Add xmlns="http://example.org/math">
<x>33</x>
<y>66</y>
</Add>
</soap:Body>
</soap:Envelope>
因此,.asmx处理程序接收到一个输入的HTTP请求时,第一件重要的事情是找出怎样把这个消息发送到对应的WebMethod。但是在能够实际调用该方法前,它需要把输入的XML映射到.NET对象。
把XML映射到对象
一旦WebMehod处理程序找出了将调用哪个方法,接着它就需要把XML消息转换为可以提供给方法调用的.NET对象。在消息发送后,处理程序通过反射检查该类找出如何处理输入的XML消息以达到这个目的。System.Xml.Serialization名字空间中的XmlSerializer类执行XML和对象之间的自动映射。
XmlSerializer使任何公共的.NET类型映射到XML Schema类型成为可能,并且有了类似的映射,它可以在.NET对象和XML实例文档之间自动映射(图4所示)。目前XmlSerializer被限制为XML Schema所支持的模型,因此不能处理目前现代的对象模型(例如复杂的非树型对象图表、两重指针等等)的所有复杂性。然而,XmlSerializer可以处理开发者趋向使用的大多数复杂的类型。
对于上面的例子中所示的Add方法,XmlSerializer会把x和y元素映射到.NET双精度型值,那么调用Add时它们就可以使用了。Add方法给调用者返回一个双精度型值,接着需要串行化该值返回为SOAP响应中的一个XML元素。
图4:把XML映射到对象
XmlSerializer也能自动处理复杂的类型(除了上面描述的限制)。例如,下面的WebMethod计算两个Point(点)结构体之间的距离。
using System;
using System.Web.Services;
public class Point {
public double x;
public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
[WebMethod]
public double Distance(Point orig, Point dest) {
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
这种操作的SOAP请求消息将包含一个Distance元素,该元素包含两个子元素,一个叫作orig,另一个叫dest,它们每个都包含两个子元素x和y,如下所示:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Distance xmlns="urn:geometry">
<orig>
<x>0</x>
<y>0</y>
</orig>
<dest>
<x>3</x>
<y>4</y>
</dest>
</Distance>
</soap:Body>
</soap:Envelope>
这种情况下的SOAP响应消息将包含一个DistanceResponse元素,该元素包含一个双精度类型的DistanceResult元素:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<DistanceResponse
xmlns="urn:geometry">
<DistanceResult>5</DistanceResult>
</DistanceResponse>
</soap:Body>
</soap:Envelope>
默认的XML映射使用方法的名称作为请求元素的名称,参数的名称作为它的子元素的名称。每个参数的结构依赖于类型的结构。公共字段和属性的名称简单地映射到子元素(这种情况下是Point中的x和y)。默认情况下响应元素的名称是请求元素的名称后面加上"Response"。响应元素也包含一个子元素,名称为请求元素的名称加上"Result"。
你可以使用一系列内建的映射标志从标准的XML映射中解放出来。例如,你可用使用[XmlType]标志自定义类型的名称和名字空间。你可以使用[XmlElement]和[XmlAttribute]标志来控制参数或类成员如何分别映射到元素或属性。你可以使用[SoapDocumentMethod]标志来控制方法自身如何映射到请求/响应消息中的元素名称。例如,你可以看一看下面版本的Distance示例的多种标志:
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;
public class Point {
[XmlAttribute]
public double x;
[XmlAttribute]
public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
[WebMethod]
[SoapDocumentMethod(RequestElementName="CalcDistance",
ResponseElementName="CalculatedDistance")]
[return: XmlElement("result")]
public double Distance(
[XmlElement("o")]Point orig, [XmlElement("d")]Point dest) {
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
这个版本的Distance要求输入的SOAP消息格式如下:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<CalcDistance xmlns="urn:geometry">
<o x="0" y="0" />
<d x="3" y="4" />
</CalcDistance>
</soap:Body>
</soap:Envelope>
它生成的SOAP响应消息的格式如下:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<CalculatedDistance xmlns="urn:geometry">
<result>5</result>
</CalculatedDistance>
</soap:Body>
</soap:Envelope>
该.asmx处理程序使用SOAP document/literal样式来实现和描述上面所示的默认映射。这意味着该WSDL定义将包含描述SOAP消息中所使用的请求和响应元素的字面上的XML schema定义(例如,没有使用SOAP编码规则)。
该.asmx处理程序也可以使用SOAP rpc/encoded样式。这意味着SOAP主体(Body)包含一个RPC调用的XML表现,并且参数都使用SOAP编码规则串行化了(例如,不需要XML Schema)。为了达到这个目标,使用[SoapRpcService]和[SoapRpcMethod]代替[SoapDocumentService]和[SoapDocumentMethod]标志。如果你要了解这些样式之间的差别,请参阅Understanding SOAP。
从上面的信息中你可以发现,完全地自定义如何把给定的方法映射到SOAP消息是可能的。XmlSerializer提供了强大的串行化引擎以及许多我们在此文中没有讨论的特性。如果你要了解XmlSerializer如何工作的详细信息,请查阅Moving to .NET and Web Services。
作为处理参数的并行化的补充,.asmx处理程序也能够并行化/串行化SOAP头部。SOAP头部的处理方法与参数不同,因为典型情况下它们被认为是范围之外的信息,没有直接关联到某个特定的方法。由于这个原因,典型的头部处理是由监听层完成的,使WebMethod根本不用进行头部处理。
但是,如果你希望在WebMethod中处理头部信息,你必须提供一个演示自SoapHeader(它描述了头部的XML Schema类型)的.NET类。接着你定义该类型的一个成员变量作为头部实例的位置标志符。最后,你给每个需要访问该头部的WebMethod作注解,指定你需要处理的字段的名称。
例如,看一看下面的包含用于身份验证目的的UsernameToken头部的SOAP请求:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Header>
<x:UsernameToken xmlns:x="http://example.org/security">
<username>Mary</username>
<password>yraM</password>
</x:UsernameToken>
</soap:Header>
<soap:Body>
<CalcDistance xmlns="urn:geometry">
...
为了使.asmx处理程序能够并行化该头部,首先你必须定义一个描述隐含的XML Schema类型的.NET类(注意:如果你已经有了该头部的XML Schema,那么可以使用xsd.exe /c生成这个类)。在这种情况下,相应的类如下所示:
[XmlType(Namespace="http://example.org/security")]
[XmlRoot(Namespace="http://example.org/security")]
public class UsernameToken : SoapHeader {
public string username;
public string password;
}
接着,你必须在WebMethod类中简单地定义一个成员变量来保持该头部类的一个实例,并使用[SoapHeader]标志来注解该WebMethod,如下所示:
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="urn:geometry")]
public class Geometry {
public UsernameToken Token;
[WebMethod]
[SoapHeader("Token")]
public double Distance(Point orig, Point dest) {
if (!Token.username.Equals(Reverse(Token.password)))
throw new Exception("access denied");
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +Math.Pow(orig.y-dest.y, 2));
}
}
接着,在WebMethod中你可以访问该头部提供的Token字段并提取信息。你也可以使用相同的技术把该头部送回给客户端--你只需要在[SoapHeader]标志声明中简单地指定头部的方向。需要了解在WebMethod框架组件中处理SOAP头部的更多信息,请查阅Digging into SOAP Headers with the .NET Framework。
.asmx处理程序也提供了.NET异常的自动的串行化。任何被.asmx捕捉到的没有处理的异常都自动地串行化进入响应中的SOAP Fault元素。例如,在前面的例子中,如果用户名与与密码不匹配,代码将抛出一个.NET异常。接着.asmx处理程序捕捉到这个异常并串行化到下面所示的SOAP响应中:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>
soap:Server
</faultcode>
<faultstring>
Server was unable to process request. --> access denied
</faultstring>
<detail />
</soap:Fault>
</soap:Body>
</soap:Envelope>
如果你需要更多地SOAP Fault元素控制权,可以明确地抛出一个SoapException对象,指定所有的SOAP Fault元素细节,例如faultcode、faulstring、faultactor和detail元素。你可以参阅Using SOAP Faults获得更多信息。
如上所示,指出WebMethod如何工作必须了解下层串行化引擎和它的多种选项。串行化引擎的优点是它隐藏了所有的通常在编写自定义处理程序中需要的下层XML API代码。但是,尽管大多数开发者发现这是有正面意义的,有一些开发者认为它是一个缺陷,因为它们仍然希望在WebMethod实现中手动包装SOAP消息。要了解如何实现这种混合的途径的详细信息,请参阅Accessing Raw SOAP Messages in ASP.NET Web Services。
自动生成WSDL
一旦你已经编写并配置了一个WebMethod,客户端为了成功地与它通讯,需要了解正确地SOAP消息是什么样子的。提供Web服务描述的标准的途径是WSDL(或嵌入式XSD定义)。为了帮助适应这种情况,.asmx处理程序自动生成人类可以阅读的文档页面和准确反映WebMethod接口的WSDL定义。如果你给WebMethods应用了一系列的映射标志,它们都会反映在生成的文档中。
如果你浏览.asmx文件,你会看到类似图2所示的可以阅读的文档页面。该文档页面是由一个叫作DefaultWsdlHelpGenerator.aspx的.aspx页面(位置在C:\windows\Microsoft.NET\Framework\ v1.0.3705\config)生成的。如果打开这个文件,你可以发现这仅仅是一个使用.NET反射生成文档的标准的ASP.NET页面。这个特性使你的文档一直与代码同步。你可以简单地修改这个文件来自定义生成的文档。
你可以通过在Web.config文件中指定一个不同的文档文件来绕过在虚拟目录基础上的文档生成:
<configuration>
<system.web>
<webServices>
<wsdlHelpGenerator href="MyDocumentation.aspx"/>
</webServices>
...
如果客户端提出的.asmx端点的GET请求在请求字符串中有"?wsdl",那么.asmx处理程序会生成WSDL定义来代替可以阅读的文档。客户端可以使用这个WSDL定义来生成自动了解如何与Web服务通讯的代理类(例如在.NET中使用Wsdl.exe)。
为了自定义WSDL生成进程,你可以编写一个SoapExtensionReflector类并在Web.config文件中向WebMethods框架组件注册。接着,当.asmx处理程序生成WSDL定义时,它将调用你的反射类,给了你自定义最终提供给客户端的定义的机会。如果你要了解更多的编写SoapExtensionReflector类的方法,请查看SoapExtensionReflectors in ASP.NET Web Services。
你可以使用两种不同的技术来绕过WSDL生成进程。第一种是你可以在虚拟目录种为客户端的访问提供静态的WSDL文档并在Web.config文件中删除文档生成部分,如下所示:
<configuration>
<system.web>
<webServices>
<protocols>
<remove name="Documentation"/>
</protocols>
...
另一种稍微自动化的技术是使用[WebServicesBinding]标志来指定实现WebMethod类的虚拟目录中的静态WSDL文档的位置。你也必须使用[SoapDocumentMethod]标志指定帮定到每个WebMethod实现的WSDL的名称。有了这些后,自动化的WSDL生成进程将导入你的静态WSDL文件并在它周围包装一个新的服务描述。如果你要了解这种技术的更多信息,请查阅Place XML Message Design Ahead of Schema Planning to Improve Web Service Interoperability。
目前WSDL极难手动编写,因为没有很多的可用的WSDL编辑器。因此,自动的文档/WSDL生成是WebMethods框架组件中有价值的一部分,很多开发者将很长时间依赖它。
结论
ASP.NET的WebMethods框架组件提供了一条高效率建立Web服务的途径。WebMethods把传统.NET方法为支持HTTP、XML、XML Schema、SOAP和WSDL暴露为Web服务操作成为可能。WebMethods(.asmx)处理程序自动找出怎样把输入的SOAP消息分派给适当的方法,这时它自动把输入的XML元素串行化成相应的.NET对象。为了简化与客户端的集成,.asmx处理程序也提供了生成人类可读(HTML)和计算机可读(WSDL)文档的自动支持。
尽管WebMethods框架组件与自定义IHttpHandlers相比稍微有点限制,但是它也提供了强大的可扩展性模型,就是我们所知道的SOAP扩展框架组件。SOAP扩展允许你根据需要引入附加的功能。例如,微软为.NET发布了Web Services Enhancements 1.0(WSE),它仅仅提供了一个SoapExtension类,该类为WebMethods框架组件引入了对几种GXA规格的支持。如果你需要了解编写SOAP扩展的更多信息,请参阅Fun with SOAP Extensions。
原作者:Aaron Skonnard
原标题:How ASP.NET Web Services Work