UML软件工程组织

通过添加 XML 架构验证来扩展 ASP.NET WebMethod Framework
转自:www.microsoft.com 作者:Aaron Skonnard和Dan Sullivan

摘要

WebMethod 通过封装大量的功能使得 XML Web 服务的开发更加容易,但是仍然有许多基础的 XML 处理需要由用户来负责。例如,WebMethod 不会根据隐含架构对消息进行验证。因为没有对这些消息进行验证,所以返回的响应可能会导致预料不到的结果。为了解决这个问题,作者通过自定义的 SoapExtension 类来添加 XML 架构验证以扩展 WebMethod 框架。

ASP.NET 提供了一种基础结构,用于将以 C# 或 Visual Basic .NET 编写的普通类的方法映射到支持 XML 1.0、XML 架构、SOAP 和 Web 服务描述语言 (WSDL) 的 Web 服务操作 (WebMethod)。这为开发人员提供了一种熟悉的编程模型来构建 Web 服务,而无需编写任何代码来生成或处理网络上使用的 XML 消息。

尽管 WebMethod 可以极大地提高工作效率,但您仍需注意基础 XML 处理过程中的某些方面。WebMethod 不会根据隐含架构对其处理的消息进行完全验证。因此,对于给定的 WebMethod 调用,在本应产生异常的情况下可能会产生误确认(对不正确或意外结果的成功响应)。此外,WebMethod 不提供对基于业务规则声明附加消息级约束的内置支持。由于缺少验证支持,所以必须在每个 WebMethod 中内置错误处理,这样就使得错误处理更加复杂。

在本文中,我们将为您展示如何使用内置在 ASP.NET 中的 WebMethod 扩展性挂接来弥补该问题。完整示例可以从本文开头处的链接下载。

本页内容
WebMethod 功能 WebMethod 功能
WebMethod 遗漏了什么 WebMethod 遗漏了什么
WebMethod 扩展 WebMethod 扩展
SoapExtension 配置 SoapExtension 配置
SoapExtension 处理 SoapExtension 处理
ValidationExtension 实现 ValidationExtension 实现
架构缓存 架构缓存
更完善的架构缓存 更完善的架构缓存
自定义架构定义 自定义架构定义
业务规则 业务规则
小结 小结

WebMethod 功能

ASP.NET 允许通过 System.Web.Services.WebMethod 属性将传统方法映射到 Web 服务操作。例如,下面的类将 CalcArea 方法公开为 Web 服务:

using System.Web.Services;

[WebService(Namespace="http://example.org/geometry/")]
public class Geometry
{
    [WebMethod]
    public double CalcArea(double length, double width)
    {
        return length * width;
    }
}

要调用该 WebMethod,您必须提供引用 Geometry 类的 .asmx 文件,如下所示:

<!-- geometry.asmx -->
<%@ WebService class="Geometry" language="C#" %>

尽管更常见的是将源代码放在独立的文件中,但也可以在 .asmx 文件本身中包含类定义。在这种情况下,类将在 .asmx 文件第一次被请求时进行实时编译。

当对 geometry.asmx 的请求传入时,WebServiceHandler 类(.asmx HTTP 处理程序)会将 SOAP 请求转换为传统的方法调用。对于 CalcArea 来说,它预期的 SOAP 消息如下:

<soap:Envelope 
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <CalcArea xmlns="http://example.org/geometry/">
      <length>2.3</length>
      <width>1.2</width>
    </CalcArea>
  </soap:Body>
</soap:Envelope>

该请求将产生如下 SOAP 响应:

<soap:Envelope 
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <CalcAreaResponse xmlns="http://example.org/geometry/">
      <CalcAreaResult>2.76</CalcAreaResult>
    </CalcAreaResponse>
  </soap:Body>
</soap:Envelope>

简言之,WebMethod 基础结构负责将传入的 SOAP 请求发送到适当的类和方法(基于请求元素名称或 SOAPAction 头,它们可通过 SoapDocumentService.RoutingStyle 进行配置),同时它还负责将 XML 消息序列化/反序列化为公共语言运行库 (CLR) 对象,包括将 .NET 样式的异常转换为 SOAP Fault 元素。

另外,该基础结构还使用 XML 架构类型和 WSDL 构造 (http://localhost/geometry/geometry.asmx?wsdl),来自动生成描述该 WebMethod 的 WSDL 定义。System.Web.Services 命名空间包含许多附加属性,这些附加属性可用于控制发送以及 XML 架构和 WSDL 映射的详细信息。

WebMethod 遗漏了什么

如果不使用 WebMethod,则您必须编写 HTTP 处理程序来处理 SOAP 请求、提取宽度和长度的文本表示、将文本转换为数值类型、计算面积,然后构造 SOAP 响应。尽管 Microsoft .NET Framework 为编写此类处理过程提供了丰富的支持,但如果使用 WebMethod,您需要做的所有事情就是计算面积。

虽然在工作效率方面有了极大的提高(这是大多数开发人员需要利用的),但您仍需处理(或至少是思考)基本消息处理的某些方面。通常,您需要对 WebMethod 将接受的消息执行比默认行为所提供的更多的控制。请考虑下面这个为 CalcArea WebMethod 生成的 XML 架构复杂类型定义:

<s:element name="CalcArea">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="1" maxOccurs="1" 
                 name="length" type="s:double" />
      <s:element minOccurs="1" maxOccurs="1" 
                 name="width" type="s:double" />
    </s:sequence>
  </s:complexType>
</s:element>

该复杂类型定义声明 CalcArea 元素必须包含两个必需元素的序列,其中 length 在 width 的前面,这两个元素都必须是双精度类型的。

现在,请考虑下面这个意外的 SOAP 请求:

<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <CalcArea xmlns="http://example.org/geometry/">
        <Length>2.3</Length>
        <Width>1.2</Width>
    </CalcArea>
  </soap:Body>
</soap:Envelope>

乍一看,这个消息正确无误,但实际上存在着细微的问题,即 length 和 width 字段的大小写不正确。然而,WebMethod 并未产生 SOAP 错误,而是产生了下面的成功响应:

<soap:Envelope 
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <CalcAreaResponse xmlns="http://example.org/geometry/">
      <CalcAreaResult>0</CalcAreaResult>
    </CalcAreaResponse>
  </soap:Body>
</soap:Envelope>

实际上,下面的请求(没有 length 和 width 元素)也产生了一个值为 0 的成功结果:

<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <CalcArea xmlns="http://example.org/geometry/"/>
  </soap:Body>
</soap:Envelope>

或者考虑以下请求,其中客户端无疑会对 WebMethod 的工作方式产生错误的认识:

<soap:Envelope 
   xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <CalcArea xmlns="http://example.org/geometry/">
        <Length>2.3</Length>
        <Width>1.2</Width>
        <Length>5.0</Length>
        <Width>3.4</Width>
        <Length>9.2</Length>
        <Width>2.8</Width>
    </CalcArea>
  </soap:Body>
</soap:Envelope>

在本例中,WebMethod 将返回一个值为 2.76 的成功响应,这与第一个示例中的情况完全一样。

这样的情况以及许多其他情况都会导致 WebMethod 产生误确认。对于某些应用程序来说,这可能无所谓,但是对于其他应用程序来说,这可能是灾难性的,因而完全不能接受。对于这些应用程序,不管您使用什么实现技术,都需要进行某种形式的验证。

这种行为的原因与 XmlSerializer 有关,XmlSerializer 是负责对象反序列化的基础管道 (plumbing)。从设计角度来看,XmlSerializer 是非常宽容的。它将忽略意外的 XML 节点,并对所预期、但没有出现的 XML 节点使用默认的 CLR 值。它不会执行 XML 架构验证,因此在反序列化过程中,详细信息(如结构、顺序、出现约束或简单类型限制)都不是强制的。2002 年 11 月的 The XML Files 部分中更加详细地描述了这一问题。

可以强制 XmlSerializer 在反序列化的过程中执行验证,方法是为反序列化调用提供一个 XmlValidatingReader 对象,如下所示:

XmlReader xr = new XmlTextReader(fileName);
XmlValidatingReader vr = new XmlValidatingReader(xr);
vr.Schemas.Add(schemaNamespace, schemaLocation);
object validatedObject = ser.Deserialize(vr);

然而,在 WebMethod 基础结构中存在两个问题。第一,干预基础 XmlSerializer 是不可能的;第二,架构文件并不作为项目的一部分而存在(该架构派生于修饰方法的 CLR 属性,且仅在用户通过 ?wsdl 查询字符串发出 GET 请求时生成)。

WebMethod 扩展

理想情况下,只需“轻拨开关”就可以启用 WebMethod 验证,但遗憾的是,事实并非如此。在某些情况下,甚至无法从 WebMethod 实现中检测到类似缺少信息这样的错误条件。因此,您必须利用所提供的扩展性点来添加验证支持。

有两种通用的 WebMethod 扩展机制:HTTP 模块和 SoapExtension 类。HTTP 模块只是一个从 IHttpModule 派生的类,并且配置为在特定虚拟根目录(使用 web.config)或整个计算机(使用 machine.config)上运行。HTTP 模块可用来扩展任何类型的 HTTP 应用程序,而不仅限于 WebMethod。HTTP 模块可以对通过管线传输的 HTTP 消息进行预处理和后处理(请参见图 1)。

MIissues0307XMLSchemaValidationfig01

图 1 HTTP 模块


HTTP 模块可以强制用户完全工作在消息级别。 因此,这是一种相当低级的方法,它需要您手动实现 SOAP 细节(比如生成 SOAP 错误)。 由于 HTTP 模块配置为针对整个虚拟根目录运行,所以逐类或逐方法启用功能或提供额外配置信息是非常困难的。

另一种扩展 WebMethod的技术完全由 .asmx 处理程序来管理,这种技术通常称为 SoapExtension 框架。 要使用这种方法,您需要从 SoapExtension 派生一个新类,并重写其部分成员。 然后,使用配置文件或自定义属性来配置 SoapExtension 的运行。 在每个 WebMethod 调用之前和之后都将调用 SoapExtension,如图 2 所示。

MIissues0307XMLSchemaValidationfig02

图 2 SoapExtensions

使用挂接程序可以十分灵活地对 WebMethod 进行扩展。 我们不打算介绍所有可能的使用方法,而只讨论如何检查和验证与调用前的某个特定方法相关联的消息。 该挂接程序将根据相应的 XML 架构对传入的消息进行验证。 基于验证结果,系统将允许消息继续进入 WebMethod,或者将相应的 SOAP 错误返回到客户端以描述错误状况。 图 3 展示了基本流程。

MIissues0307XMLSchemaValidationfig03

图 3 在调用前验证消息

SoapExtension 是用来扩展 WebMethod 的首选框架,因为它可以使您拥有更多的配置控制,同时允许您针对每个方法自定义扩展行为。

SoapExtension 配置

您可以将 SoapExtensions 配置为运行在整个虚拟根目录上(类似于 HTTP 模块),或者针对每个方法进行配置。要为整个虚拟根目录配置 SoapExtension,您只需将一个条目添加到 web.config 中。下面是一个示例 web.config 文件,它注册了一个名为 ValidationExtension 的自定义 SoapExtension:

<configuration>
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="ValidationExtension, 
                   DevelopMentor.Web.Services" 
             priority="1" group="0" />
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>
 
Figure 4 SoapExtension per Method

[AttributeUsage(AttributeTargets.Method,
AllowMultiple=false)]
public class ValidationAttribute : SoapExtensionAttribute
{
int priority = 0;

// used by soap extension to get the type
// of object to be created
public override System.Type ExtensionType
{
get { return typeof(ValidationExtension); }
}
public override int Priority
{
get { return priority; }
set { priority = value; }
}
}

图4

为了针对每个方法配置 SoapExtension,您需要编写一个派生自 SoapExtensionAttribute 的自定义属性(请参见图 4)。ExtensionType 属性将返回要使用的 SoapExtension 的类型,在本例中为 ValidationExtension。需要注意的是,该属性只能应用于方法,且只能应用一次。这样,编译器将捕获该属性可能被误用的大部分情况。将其限定为某个方法的单个实例将更容易实现该扩展。

然后,您就可以使用该属性来修饰 WebMethod,以告知基础结构在运行时调用您的 SoapExtension。请注意,如果属性的名称以“Attribute”结尾,则可以在使用时将其去掉:

[Validation]
[WebMethod]
public double CalcArea(double length, double width)
{
    return length * width;
}

您也可以设计自己的属性,以通过公共字段或属性获得附加配置信息 — 您不久将看到这样的例子。总的来说,这种配置技术可与 WebMethod 很好地结合在一起。

SoapExtension 处理

ValidationExtension 类必须派生自 SoapExtension 基类并重写所需的成员,如图 5 所示。图 6 提供了有用的摘要,以描述基础结构对各个不同 SoapExtension 方法的调用情况。然而,决定如何实现这些方法需要完全理解执行过程。

Figure 5 ValidationExtension Class

public class ValidationExtension : SoapExtension
{
public override object GetInitializer(
System.Type serviceType)
{
// TODO
}
public override object GetInitializer(
LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute)
{
// TODO
}
public override void Initialize(object initializer)
{
// TODO
}
public override void ProcessMessage(SoapMessage message)
{
// TODO
}
}

图5

每次调用一个 WebMethod 时,基础结构都会创建 SoapExtension 类的一个新实例。每次调用都会创建一个新的 SoapExtension 实例的事实带来了状态管理方面的问题。您不能将状态存储在成员变量中,因为这些变量会在调用之间消失。考虑到显而易见的性能因素,您也不希望在构造函数中执行代价很高的初始化。因此,SoapExtension 框架提供了 GetInitializer 和 Initialize 方法,以执行一次性的初始化。


图6

在第一次调用虚拟根目录中的任何 WebMethod 时,基础结构将检查 SoapExtensions 是否已经在适用的配置文件(web.config 或 machine.config)中进行了配置。如果是,则基础结构将创建每个已配置的 SoapExtension 类的实例,并调用第一个 GetInitializer 方法,如图 6所示。您有机会执行任何必要的初始化,将信息包装在一个对象中,并将此对象返回给基础结构。基础结构将存储返回的对象,然后将其传递给 Initialize,在构造 SoapExtension 类之后,每个 WebMethod 调用随后都会导致调用 Initialize 方法。

在第一个 WebMethod 调用的过程中,基础结构还将检查每个 WebMethod 是否具有从 SoapExtensionAttribute 派生的属性。如果有,则基础结构将生成每个属性的实例(使用所有已提供的信息),并通过 ExtensionType 属性来请求从 SoapExtension 派生的类(该类应该在每次调用时调用)。对于这些类中的每一个,基础结构都会创建一个实例并调用第二个 GetInitializer 方法(如图 6所示),同时传入已提供的属性。您有机会执行特定于 WebMethod 的初始化。基础结构将为该特定的 WebMethod 存储从 GetInitializer 返回的对象,并在以后的每次调用中将其传递给 Initialize 方法。完整的初始化过程如图 7 所示。

MIissues0307XMLSchemaValidationfig07

图 7 初始化过程

初始化分为两个部分;一部分只在第一次调用时进行,另一部分在每次调用方法时都进行。第一部分初始化在调用 GetInitializer 时进行,并返回一个 initializer 对象,运行库会缓存该对象以供将来使用。第二部分初始化在调用 Initialize 时进行,也可以在调用某个方法时进行,即使该方法是第一次调用。可以通过 ProcessMessage 函数为 Initialize 提供 Initializer 对象。这样,创建 Initializer 对象所需的时间就可以平摊到对某一方法的所有调用中。然后调用 ProcessMessage 四次,每个消息处理状态(BeforeDeserialize、AfterSerialize、BeforeSerialize 和 AfterSerialize)一次。

在 Initialize 中,通常只需要将所提供的对象存储在成员变量中,以供将来在 ProcessMessage 中使用,真正的预处理和后处理都在 ProcessMessage 中进行。WebMethod 的处理过程分为四个阶段:BeforeDeserialize、AfterDeserialize、BeforeSerialize 和 AfterSerialize(请参见图 2)。在 ProcessMessage 的这些阶段的任一个中,您都可以编写执行代码。WebMethod 实现是在 AfterDeserialize 和 BeforeSerialize 之间调用的。

ValidationExtension 实现

要实现验证扩展,需要在 System.Web.Services 对消息进行任何操作之前执行 XML 架构验证。我们将把此逻辑挂接到 BeforeDeserialize 阶段,如图 8所示。

--------------------------------------------------------------------------------
Figure 8 BeforeDeserialize

public override void ProcessMessage(SoapMessage message)
{
if (SoapMessageStage.BeforeDeserialize == message.Stage)
{
try
{
// perform XML Schema validation here
// configure XmlValidatingReader
XmlTextReader tr = new XmlTextReader(message.Stream);
XmlValidatingReader vr = new XmlValidatingReader(tr);
... // load schema files in XmlValidatingReader
while (vr.Read()) ; // read through stream
}
finally
{
// reset stream position
message.Stream.Position = 0;
}
}
}

图8

该实现将原始消息流加载到 XmlTextReader 中,然后使用 XmlValidatingReader 来包装它。在将架构文件加载到 XmlValidatingReader 的架构缓存(Schemas 属性)之后,它就只需读取整个流来执行验证。如果 XmlValidatingReader 在消息中遇到验证错误,它将引发一个包含错误描述信息的异常。该异常将被转换为 SOAP Fault 响应消息,并返回到客户端,而无需调用 WebMethod。在从 ProcessMessage 返回之前,还需要重置流的位置;否则,基础结构将无法知道如何正确地反序列化该消息。关于 ProcessMessage 的唯一棘手部分就是将架构加载到 XmlValidatingReader。

架构缓存

要对传入的请求执行验证,XmlValidatingReader 需要访问描述该消息的架构定义。问题在于,我们的代码到哪里查找架构定义呢?有好几种可能的答案。最简单的解决方案可能就是加载虚拟根目录中某个已知位置的所有架构文件,比如一个子 xsd 目录。然后,这种扩展方案的用户可以简单地将所需的架构文件放入该目录,它们将被自动选取。

因为加载所有的架构文件将带来一定的性能开销,所以这是在何处使用 GetInitializer 的一个很好的例子。在 GetInitializer 中,需要创建 XmlSchemaCollection 的一个实例,并在其中填充所有已编译的架构。不管调用的是哪个 GetInitalizer 版本,我们都需要做相同的事情,因此可以创建一个 helper 方法(即 GetInitializerHelper),以便将两种 GetInitializer 版本都委托给它(请参见图 9)。

--------------------------------------------------------------------------------
Figure 9 GetInitializerHelper

public object GetInitializerHelper()
{
// XML Schema cache for schema validation
XmlSchemaCollection sc = new XmlSchemaCollection();
// load schemas from vroot cache
HttpContext ctx = HttpContext.Current;
if (Directory.Exists(ctx.Server.MapPath(relativeDir)))
{
string[] schemaFiles = Directory.GetFiles(
ctx.Server.MapPath("xsd"), "*.xsd");
foreach (string schemaFile in schemaFiles)
{
XmlTextReader r = new XmlTextReader(filePath);
XmlSchema schema = XmlSchema.Read(r, null);
sc.Add(schema);
}
}
return sc;
}

图9

请记住,在将来每次调用 WebMethod 的过程中,从 GetInitializer 返回的对象都将传递到 Initialize 中。我们的 Initialize 实现将 XmlSchemaCollection 对象缓存到成员变量中,以供将来在 ProcessMessage 中使用:

public override void Initialize(object initializer)
{
    _context = (XmlSchemaCollection)initializer;
}

接下来,回到前面的 ProcessMessage 实现,我们可以在处理流之前将已存储的 XmlSchemaCollection 对象与 XmlValidatingReader 关联起来: // ProcessMessage method

XmlTextReader tr = new XmlTextReader(message.Stream);
XmlValidatingReader vr = new XmlValidatingReader(tr);
vr.Schemas.Add(_context); // load schema cache
while (vr.Read()) ; // read through stream

这样,一切都应该已经就绪,可以开始使用 ValidationExtension 了。

要对先前所描述的示例进行实验,您需要遵循下列步骤:

1.

在项目中添加一个对 ValidationExtension 程序集 (DevelopMentor.Web.Services) 的引用。

2.

使用 [Validation] 标注您的 WebMethod。

3.

在虚拟根目录中创建一个 xsd 目录。

4.

从 http://schemas.xmlsoap.org/soap/envelope 上下载 SOAP 1.1 架构,并将其保存到 xsd 目录中。

5.

定位到 .asmx 终结点,按 Service Description (.asmx?wsdl),然后将 WSDL 文件保存到 xsd 目录中。

6.

从已保存的 WSDL 文件中提取架构定义,并将其保存到一个新的 .xsd 文件中(确保您获取了所有命名空间声明)。

现在,使用无效请求调用 CalcArea 将产生一个 SOAP Fault。例如,包含大写字母 (Length/Width) 的无效请求将产生如图 10 所示的响应。请注意,SOAP Fault 中包含有关在验证过程中出现的错误的准确信息。由于有了架构定义,前面所述的所有误确认情况都将被检测出来。

Figure 10 SOAP Fault

<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. —&gt; Element
'http://example.org/geometry/:CalcArea' has invalid child element
'http://example.org/geometry/:Length'. Expected
'http://example.org/geometry/:length'. An error occurred at
(4, 3).</faultstring>
<detail />
</soap:Fault>
</soap:Body>
</soap:Envelope>

图10

更完善的架构缓存

无论是使用 [Validation] 标注还是在 web.config 中添加条目,都可以轻松地使 ValidationExtension 得以执行。其关键之处在于设置架构缓存。为了帮助简化这个过程,我们决定在扩展中添加一些功能。

首先,从程序集源文件中自动加载 SOAP 1.1 架构。在 GetInitalizerHelper 中有一个对 helper 函数的调用,该 helper 函数负责这项工作,并将 SOAP 1.1 架构添加到 XmlSchemaCollection 对象中:

LoadSchemaFromResourceFile(sc, "ValidationResources", 
   "DevelopMentor.Web.Services", "SOAP1.1");

它处理了 SOAP 架构,但是如何加载用户自己的架构还是一个问题。由于在使用前从 WSDL 文件中提取架构定义比较麻烦,所以可以在自动加载 .xsd 文件的代码中进行如下扩展,以便直接从完整的 WSDL 文件中加载架构:

string[] wsdlFiles = Directory.GetFiles(
  ctx.Server.MapPath("xsd"), "*.wsdl");
foreach (string wsdlFile in wsdlFiles)
{
    ServiceDescription sd=ServiceDescription.Read(wsdlFile);
    foreach(XmlSchema embeddedXsd in sd.Types.Schemas)
       sc.Add(embeddedXsd);                
}

当然也可以将 WSDL 文件放入 xsd 目录中,这样就可以了。同时,为了在存储 XML 架构文件的位置方面提供更大的灵活性,我们定义了几个可以与 ValidationExtension 一起使用的附加属性:它们是 ValidationSchemaAttribute 和 ValidationSchemaCacheAttribute,这两个属性都用于类定义。通过使用 ValidationSchemaAttribute,您可以指定希望加载到缓存中的架构文件位置(相对于虚拟根目录):

[WebService(Namespace="http://example.org/geometry/")]
[ValidationSchema("schemas/geo/geo.xsd")]
[ValidationSchema("schemas/global/math.xsd")]
[ValidationSchema("schemas/global/simple.xsd")]
public class Geometry {
       
}

通过使用 ValidationSchemaCache,您可以指定将包含大量 .xsd 或 .wsdl 文件的附加目录(xsd 除外),所有这些文件都将由 GetInitializer 加载到缓存中。下面是一个活动中的 ValidationSchemaCache 示例:

[WebService(Namespace="http://example.org/geometry/")]
[ValidationSchemaCache("schemas")]
[ValidationSchemaCache("schemas/global")]
public class Geometry {
    
}

属性类的定义比较简单 - 它们派生自 System.Attribute,并分别为存储文件/目录的位置提供了一个公共属性。然后,GetInitializerHelper 将检查这些类 (serviceType) 是否已使用,如果是,它将检索信息并从指定位置进行加载。下面的代码来自 GetInitializerHelper:

// load schemas from user-defined file locations
   atts = serviceType.GetCustomAttributes(
   typeof(ValidationSchemaAttribute), true);
 foreach(ValidationSchemaAttribute vsa in schemaLocations)
   LoadSchemaFromFile(sc, ctx.Server.MapPath(vsa.Location));

// load schemas from user-defined directories
   object[] atts = serviceType.GetCustomAttributes(
   typeof(ValidationSchemaCacheAttribute), true);
foreach (ValidationSchemaCacheAttribute loc in atts)
    LoadSchemasFromDirectory(sc, loc.RelativeDirectory);

这使得用户对其系统中存放架构和 WSDL 文件的位置有了更多的控制。

作为最后一点简化工作,我们希望可以使让开发人员避免同时管理架构文件。.asmx 处理程序支持生成描述终结点的 WSDL 文档。我们可以利用此功能来生成包含可用于验证的 XML 架构定义的 WSDL 文档。这就可以为给定的 WebMethod 启动验证功能,而无需开发人员手动管理架构文件。下面的方法展示了这一过程:

internal void LoadReflectedSchemas(
    XmlSchemaCollection sc, Type type, string url)
{
    ServiceDescriptionReflector r=
          new ServiceDescriptionReflector();
        r.Reflect(type, url);
        foreach (XmlSchema xsd in r.Schemas)
            sc.Add(xsd);
        foreach (ServiceDescription sd in r.ServiceDescriptions)
            LoadSchemasFromServiceDescriptions(sc, sd);
}

提供希望为其生成 WSDL 的类类型,然后循环访问所生成的架构集合。您可以通过向 GetInitializer 方法传递 serviceType 或使用 methodInfo.DeclaringType 来进行调用。请注意,用户定义的架构始终优先于这种方式。

如果所有这些代码已就绪,您就可以跳过前面列出的最后四步;现在,扩展将可用了。您只需将程序集添加到项目中,并用 [Validation] 进行标注就完成了。

自定义架构定义

启用 XML 架构验证将极大地简化 WebMethod 错误处理。现在,就可以限定消息的结构以及文本值的语法和值空间了。如果不希望考虑架构文件的管理,则您基本上必须接受基础结构所带来的一切。但是如果您想获得对架构的控制(使用前面刚刚描述的技术),则可以使用更高级的架构功能。例如,请考虑以下 WebMethod:

public void AddEmployee(string name, string id, DateTime 
  bday, double salary) { ... }

为该 WebMethod 生成的架构将自动使用 XML 架构的内置数据类型来限定值。然而,字符串和双精度值并不能精确地分别描述 id 和 salary 值的预期格式。请考虑下面的 XSD 简单类型定义:

<xs:simpleType name="SSN">
    <xs:restriction base="xs:string">
        <xs:pattern value="\d{3}-\d{2}-\d{4}" />
    </xs:restriction>
</xs:simpleType>
<xs:simpleType name="NewHireSalary">
    <xs:restriction base="xs:double">
        <xs:minExclusive value="0" />
        <xs:maxExclusive value="5000" />
    </xs:restriction>
</xs:simpleType>

这些定义精确地描述了操作预期的数据格式。SSN 类型利用一个正则表达式来限定所允许的字符串格式,同时 NewHireSalary 显式地定义了所允许的值空间。要在验证过程中利用这些类型,只需修改架构文件来使用它们即可,如图 11 所示。现在,如果客户端提供不符合社会保障号格式的 id 值,或者不属于 0 到 5000 范围内的 salary 值,则 ValidationExtension 将会捕获该错误,并返回一个描述性的 SOAP Fault。

Figure 11 New Types

<s:element name="AddEmployee">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1"
name="name" type="s:string" />
<s:element minOccurs="0" maxOccurs="1"
name="id" type="tns:SSN" />
<s:element minOccurs="1" maxOccurs="1"
name="bday" type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1"
name="salary" type="tns:NewHireSalary" />
</s:sequence>
</s:complexType>
</s:element>

图11

业务规则

正如刚刚展示的,使用 ValidationExtension,您可以充分利用 XML 架构语言的优点。因为该语言具有很好的扩展性,所以您可以对复杂的结构细节、简单类型值空间、唯一性、替代机制等方面进行控制。当然,XML 架构也有一些局限性。其中最明显的局限性就是,在给定节点的出现或值影响其他节点约束的情况下,您无法强制进行并发约束。

对于现实世界中的大多数业务规则而言,并发约束是很典型的。业务规则通常可以对超出其类型的数据指定任意约束。那么对数据来说,可能会存在什么样的任意约束呢?请考虑一下用来处理 length 和 width 元素的 CalcArea 操作。在现实世界中,长度通常定义为大于宽度。

尽管可以任意选择,但在生产过程中使用统一的词汇是很重要的,比如要确保塔盘纵向放在传送带上,这样箱子才能适合传送带和通道的大小。假设现在正是这种情况。这样的规则无法用 XML 架构来实现,但可以在 CalcArea 方法的实现中进行相应处理。通过添加一小段过程代码,您可以在计算面积之前对输入参数进行检查。但由于业务规则通常来自企业规范,所以若将其包含在类定义中会起到更清晰的说明作用。

XPath 是用于定义这些附加约束的理想语言。它正是为计算 XML 文档的内容而特别设计的;它易于使用且非常灵活。而且,XPath 已在目前几乎所有的平台、语言、甚至数据库中实现。因此,如果使用 XPath 作为规则语言,则这些规则可以通过其他语言、工具、甚至数据库在其他平台上实现。这意味着,业务规则可以应用到处理链中的任何环节,甚至是在客户端。

使用 XPath 来进行有关 XML 文档的断言并不是一个新想法。Schematron 是推荐的语言,它能够以标准的方式完成这一工作(请参阅 http://www.ascc.net/xml/schematron)。Schematron 可以与 XML 架构定义集成在一起,并在验证的后处理过程中进行计算。Daniel Cazzulino 编写了用于 .NET Framework 的 Schematron 开放源码实现,这可以从 http://sourceforge.net/projects/schematron-net 上获得。

我们认为,在 ValidationExtension 类中构建这种类型的 XPath 约束处理是很有用的。为此,我们定义了一个名为 AssertAttribute 的新属性类,它允许您为个别方法或整个类(适用于所有 WebMethod)指定 XPath 断言。这些属性的用法示例显示在图 12 中。

Figure 12 Assertions

[AssertNamespaceBinding("s",
"http://schemas.xmlsoap.org/soap/envelope/")]
[AssertNamespaceBinding("t",
"http://example.org/geometry/")]
[WebService(Namespace="http://example.org/geometry/")]
[Assert(@"//t:width >= 0", "width must be greater than 0")]
[Assert("(//t:length > //t:width)",
"Length must be greater than width")]
public class Geometry
{
[WebMethod]
[Validation]
[Assert("(//t:length * //t:width) > 100",
"Area must be greater than 100")]
[Assert("(//t:length div //t:width) = 2",
"Length must be twice the size of width")]
public double CalcArea(double length, double width)
{
return length * width;
}
[WebMethod]
[Validation]
public double CalcPerimeter(double length, double width)
{
return length * width;
}
}

图12

有关项目中这部分内容如何实现的详细信息,请不要错过下一篇文章,我们将继续描述 XPath 断言功能以及其他热门话题,例如扩展 WSDL 生成过程以包含业务规则描述。但是,如果现在您等不及要先看看这些话题,可以从本文开头的链接处下载源代码并查看示例。

小结

WebMethod 基础结构为添加附加的用户定义行为提供了强大的扩展性框架。您可以通过 ValidationExtension 和 ValidationAttribute 类来为任何 WebMethod 添加验证支持。这一技术简化了 WebMethod 错误处理,并充分利用了 XML 架构所提供的功能和灵活性。

 

 

版权所有:UML软件工程组织