Fork me on GitHub

使用自定义行为扩展 WCF

Windows® Communication Foundation (WCF) 提供了许多扩展点,供开发人员自定义运行时行为,从而实现服务调度和客户代理调用。您可以通过编写能以声明方式应用到服务中的自定义行为来使用这些扩展点。本月将为您介绍这一流程的工作原理。

 

WCF 可扩展性
在上期专栏中,我重点介绍了 WCF 绑定概念,您可以为 WCF 服务上的各个终结点指定绑定。绑定控制该终结点的消息传递详细信息(发生在网络上的情况)。这是 WCF 建立一个能够在字节流(网络上的消息)和 WCF 消息间转换的通道堆栈所必须遵从的方案。在整个 WCF 通道层有无数的扩展点。
WCF 在通道层的顶部还提供了一个高级运行时,主要是针对应用程序开发人员。在 WCF 文档中,它常被称为服务模型层。该高级运行时主要由一个称作调度程序(在服务主机上下文中)的组件和一个称作代理(在客户端上下文中)的组件组成。
调度程序/代理组合的主要作用是在 WCF 消息对象和 Microsoft® .NET Framework 方法调用间进行转换(请参见图 1)。这些组件按照一系列明确定义的步骤来执行此过程,并在此过程的每个步骤中都提供了可供插入的扩展点。您可以使用这些扩展点来实现各种自定义行为,包括消息或参数验证、消息日志记录、消息转换、自定义序列化/反序列化格式、输出缓存、对象共用、错误处理和授权等。下面,我将重点介绍如何实现这些类型的自定义行为。
图 1 WCF 运行时体系结构 

 

调度程序/代理扩展
调度程序和代理都提供了大量的扩展点,您可以在其中插入自己的代码;这些扩展常被称为侦听器,因为它们允许您侦听默认的运行时执行行为。不过,我通常称它们为运行时扩展。
图 2 显示了客户端代理体系结构及可用的扩展点。代理的主要作用是将调用方提供的对象(参数)转换为 WCF 消息对象,然后将后者提供给底层通道堆栈进行网络传输。
图 2 代理(客户端)扩展 
正如在第一步中看到的那样,在此过程中,您可以使用可用的第一个扩展点来执行自定义参数检查。还可以使用该扩展点执行自定义验证、值修改或特殊过滤。接着,代理使用序列化程序将提供的参数转换为 WCF 消息对象(图中步骤 2)。此时,您可以使用一个自定义的格式化程序对象来自定义序列化过程。
代理生成 Message 对象后,将使用最后的扩展点来检查产生的 Message 对象(如步骤 3 所示),然后将其提交给通道堆栈。如图 2 所示,无论调用哪个操作,该扩展都将生效。您还可以使用该扩展点实现具有广泛影响的消息传递功能,如消息日志记录、验证或转换 — 这些功能不一定是某一种操作所特有的。
可以通过 ClientOperation 和 ClientRuntime 对象在代理上配置这些扩展。您可以找到一个用于各种服务操作的 ClientOperation 对象和一个用于从整体上配置代理的 ClientRuntime 对象。ClientOperation 提供了用于管理参数检查和消息格式化扩展的属性,而 ClientRuntime 则提供了用于管理消息检查扩展的属性。
图 3 显示了调度程序扩展点。您会注意到,该图与图 2 非常相似,但是在该图中,扩展点是按相反顺序执行的,并且多了几个图 2 的客户端中不存在的扩展点。
图 3 调度程序扩展 
当调度程序收到来自通道堆栈的 Message 对象时,遇到的第一个扩展点便是消息检查。然后,调度程序必须选择一个要调用的操作(步骤 2),然后才能继续 — 这里有一个扩展点用于覆盖默认的操作选择行为。确定目标操作后,调度程序会将消息反序列化为调用目标方法时可作为参数提供的对象。此时(步骤 3),调度程序提供用于进行消息格式化(反序列化)和参数检查(步骤 4)的扩展点。调度程序的最后一步是调用提供就绪参数的目标方法。您甚至可以通过提供自定义的操作调用程序对象来替代这一步。
可以通过 DispatchRuntime 和 DispatchOperation 对象在调度程序上配置这些扩展,如图 3 所示。稍后我将简要介绍如何访问这些对象,不过,首先让我们讨论一下如何实现它们。

 

实现自定义扩展
上述的每个扩展点都是根据 .NET 接口定义来模拟的(请参见图 4)。注意,在某些情况下,相同的逻辑扩展类型要求在调度程序和代理端之间使用另一接口。下面我将详细介绍如何实现其中的部分接口。
假设您要构建一个具有以下约定的邮政编码查询服务:
[ServiceContract]
public interface IZipCodeService
{
    [OperationContract]
    string Lookup(string zipcode);
}
Lookup 方法使用一个单独的字符串类型 zipcode 参数,并将一个字符串返回给调用方。调用方应该提供一个邮政编码值,然后,服务会返回位置(按城市、省格式)。用户不易看出的是提供的邮政编码必须采用正式的邮政编码 + 4 格式:#####-####。例如,我家的邮政编码是 84041-1501。按照该要求,服务实现必须验证每个进入的邮政编码值。

 

参数检查器
在 Lookup 方法自身中实现邮政编码 + 4 验证逻辑并不困难,但是,如果结果是接受邮政编码的大量操作,最好是将验证逻辑作为能够以声明方式应用到任意操作的 IParameterInspector 扩展来实现。
为此,您必须编写一个可实现 IParameterInspector 的类,该类定义两个方法:AfterCall 和 BeforeCall。顾名思义,运行时将在对服务实例调用目标方法之前调用 BeforeCall,而在调用完成之后调用 AfterCall。这样就给您提供了用于检查参数和返回值的前侦听点和后侦听点,这些参数和返回值是作为对象数组提供给这些方法的。
图 5 显示了一个完整的 IParameterInspector 实现,该实现用于执行必要的邮政编码 + 4 验证。ZipCodeInspector 类用于实现 IParameterInspector,不过,我只实现了 BeforeCall,因为我只需要输入验证。BeforeCall 根据邮政编码 + 4 正则表达式 ("\d{5}-\d{4}") 来验证提供的邮政编码,如果不匹配,它将继续并引发 FaultException。
有了 ZipCodeInspector,您便可以轻松地将此验证逻辑应用到接受邮政编码值的任何操作。而且,可以在网格两端(客户端或服务中)都使用该实现。再举几个例子后,我将介绍如何绑定此参数检查器。

 

消息检查器
不管是什么操作,假设您要检查的是流入和流出服务的消息,而不是参数。在这种情况下,您需要使用消息检查扩展点。与参数检查不同,此时用于调度程序和代理(分别是 IDispatchMessageInspector 和 IClientMessageInspector)的消息检查接口是不同的。不过,当需要支持两端时,始终可以实现这两个接口。
IDispatchMessageInspector 有两个方法:AfterReceiveRequest 和 BeforeSendReply,这样您就有前侦听点和后侦听点来检查 WCF 消息对象了。IClientMessageInspector 还有两个提供相反点的方法:AfterReceiveReply 和 BeforeSendRequest。
假定您要实现一个将所有传入和传出消息打印到控制台窗口的诊断实用工具。图 6 提供了执行此操作的完整示例。注意,ConsoleMessageTracer 实现了两个消息检查器接口,因此它可用于网络的两端。每个方法都只是复制传入的消息,并将其打印到控制台窗口。
public class ConsoleMessageTracer : IDispatchMessageInspector, 
    IClientMessageInspector
{
    private Message TraceMessage(MessageBuffer buffer)
    {
        Message msg = buffer.CreateMessage();
        Console.WriteLine("\n{0}\n", msg);
        return buffer.CreateMessage();
    }
    public object AfterReceiveRequest(ref Message request, 
        IClientChannel channel, 
        InstanceContext instanceContext)
    {
        request = TraceMessage(request.CreateBufferedCopy(int.MaxValue));
        return null;
    }
    public void BeforeSendReply(ref Message reply, object
        correlationState)
    {
        reply = TraceMessage(reply.CreateBufferedCopy(int.MaxValue));
    }

    public void AfterReceiveReply(ref Message reply, object
        correlationState)
    {
        reply = TraceMessage(reply.CreateBufferedCopy(int.MaxValue));
    }
    public object BeforeSendRequest(ref Message request, 
        IClientChannel channel)
    {
        request = TraceMessage(request.CreateBufferedCopy(int.MaxValue));
        return null;
    }
}

如果您想了解为什么我在图 6 中使用了消息复制技术,请参阅我以前的专栏“WCF 消息传递基础”,阅读有关消息生存期的部分(请参见 msdn.microsoft.com/msdnmag/issues/07/04/ServiceStation)。

 

操作调用程序
作为最后一个示例,我们了解一下操作调用程序扩展点。您可以使用该扩展点替代具有自定义调用程序对象的默认过程。在邮政编码示例中,可以使用操作调用程序来实现一个简单的输出缓存功能。对于一个给定的邮政编码,结果将总是相同的,因此如果缓存该结果,您仅需为该邮政编码值调用一次服务实例即可。如果遇到成本高昂或需花费大量时间来完成的某些服务逻辑,这可以极大地改善性能并减少响应时间。
图 7 显示了一个完整的示例。这里,ZipCodeCacher 类实现了 IOperationInvoker,并在 Invoke 方法中使用了邮政编码缓存功能。Invoke 方法先尝试在其缓存中查询邮政编码位置,如果找不到,它将在服务实例上调用该方法(使用默认调用程序)。它将新结果存储到缓存中供以后调用。构造时,使用此扩展的用户必须提供调度程序的默认调用程序对象作为其剩余方法的委托。
我已经介绍了几个常见的构造自定义扩展的示例。还有其他一些示例我来不及介绍,我把它们留给您自己研究。我要谈论的更为重要的一点是,如何将这些扩展绑定到调度程序/代理。这时行为就派上用场了。

 

使用行为应用自定义扩展
行为是一种特殊类型的类,它在 ServiceHost/ChannelFactory 初始化过程中扩展运行时行为。有四种类型的行为:服务、终结点、约定和操作。每个类型都允许在不同的作用域应用扩展(请参见图 8)。每种行为类型也是通过不同的接口定义来模拟的,但它们都共用一组相同的方法(请参见图 9)。一个例外是,IServiceBehavior 没有 ApplyClientBehavior 方法,因为服务行为不能用于客户端。
图 8 所示,服务行为用于将扩展应用于整个服务;您可以将它们应用于服务本身,也可以应用于特定终结点、约定和操作。终结点行为则用于将扩展应用于某个特殊的终结点(或者可能是该终结点的约定或操作)。约定和操作行为用于将扩展应用到约定和操作。终结点、约定和操作行为都可以应用于服务和客户端,而服务行为只能应用于服务。
尽管每个行为接口的方法是相同的,但其特征却完全不同。它们是为对该特殊作用域提供适当的运行时对象而量身定制的。ApplyDispatchBehavior 和 ApplyClientBehavior 分别是将自定义扩展应用于调度程序和代理的核心方法。当运行时调用这些方法时,它为您提供 DispatchRuntime、DispatchOperation、ClientRuntime 和 ClientOperation 对象,以插入扩展(请参见上文的图 23)。
图 10 显示了如何实现若干操作行为。使用操作行为来应用 ZipCodeInspector 和 ZipCodeCacher 扩展是合理的,因为您只需在处理邮政编码的方法中使用它们。如您所见,ZipCodeValidation 将 ZipCodeInspector 的实例添加到提供的 DispatchOperation 和 ClientOperation 对象的 ParameterInspectors 集合中。ZipCodeCaching 将自定义的 ZipCodeCacher 指定给提供的 DispatchOperation 对象的 Invoker 属性。
现在,是决定 ConsoleMessageTracer 消息检查器使用什么类型的行为的时候了。我可以根据需要的用途,通过约定、终结点或服务行为来应用它。图 11 中的示例介绍如何实现同时充当服务和终结点行为的类,以便将 ConsoleMessageTracer 应用到适当的 MessageInspectors 集合中。
既然您已经了解了如何实现一些行为,您就可以学习如何将行为添加到 WCF 运行时了。

 

将行为添加到运行时
当构造 ServiceHost 或客户端 ChannelFactory 时,运行时反射服务类型,读取配置文件,并开始构建一个位于内存中的服务说明。在 ServiceHost 中,可以通过 Description 属性(ServiceDescription 类型)获得此说明。在 ChannelFactory 中,可以通过 Endpoint 属性(ServiceEndpoint 类型)获得此说明;客户端的说明仅限于目标终结点。
ServiceDescription 包含对服务和每个终结点 (ServiceEndpoint) 的完整说明,包括约定 (ContractDescription) 和操作 (OperationDescription)。ServiceDescription 提供了模拟服务行为集合的 Behaviors 属性(一个 IServiceBehavior 类型的集合)。每个 ServiceEndpoint 还有一个模拟单个终结点行为的 Behaviors 属性(一个 IEndpointBehavior 类型的集合)。同样,ContractDescription 和 OperationDescription 也各有一个相应的 Behaviors 属性。
在 ServiceHost 和 ChannelFactory 构造过程中,会使用在您的代码(通过属性)或配置文件中发现的任何行为自动填充这些行为集合(稍后详细介绍)。构造完毕后,还可以手动向这些集合中添加行为。以下示例显示如何将 ConsoleMessageTracing 作为服务行为添加到主机中:
ServiceHost host = new ServiceHost(typeof(ZipCodeService));
host.Description.Behaviors.Add(new ConsoleMessageTracing());
此示例遍历所有 ServiceEndpoint 对象,然后将 ConsoleMessageTracing 作为终结点行为添加到主机中:
ServiceHost host = new ServiceHost(typeof(ZipCodeService));
foreach (ServiceEndpoint se in host.Description.Endpoints)
    se.Behaviors.Add(new ConsoleMessageTracing());
正如我所述,这与客户端的情况类似,说明着重于一个终结点,没有服务行为。以下示例说明了如何将 ConsoleMessageTracing 作为客户端终结点行为添加到主机中:
ZipCodeServiceClient client = new ZipCodeServiceClient();
client.ChannelFactory.Endpoint.Behaviors.Add(
    new ConsoleMessageTracing());
您可以使用类似的方法手动将行为添加到特定的约定(使用 ServiceEndpoint.Contract.Behaviors)或约定中的单个操作(遍历 ServiceEndpoint.Contract.Operations 集合并访问 OperationDescription 中的 Behaviors 集合)。
打开 ServiceHost/ChannelFactory(通过 ICommunicationObject.Open)后,运行时遍历 ServiceDescription,并通过调用 ApplyDispatchBehavior 和 ApplyClientBehavior 为每个行为提供一次应用其调度程序/代理扩展的机会(请参见图 12)。当此过程完成后,便不能向运行时添加其他行为或扩展了。
图 12 将行为添加到运行时 (单击该图像获得较大视图)

 

添加具有属性和配置的行为
在 ServiceHost/ChannelFactory 构造过程中,运行时反射服务类型和配置文件,并自动将其发现的所有行为添加到 ServiceDescription 中相应行为的集合内。
运行时首先查找服务代码中的 .NET 属性,该服务代码派生自图 8 中列出的行为接口之一。运行时找到这样的一个属性后,会立即自动将该属性添加到相应的集合中。例如,我在这里用三个与我以前定义的行为对应的属性对我的服务进行注释:
[ServiceContract]
public interface IZipCodeService
{
    [ZipCodeCaching]
    [ZipCodeValidation]
    [OperationContract]
    string Lookup(string zipcode);
}

[ConsoleMessageTracing]
public class ZipCodeService : IZipCodeService
{
    ...
}
当我定义这些行为类时,即已确保它们派生自 Attribute(除 IServiceBehavior 和 IOperationBehavior 外),因此我能够以这种方式配置它们。当为上述 ZipCodeService 类构造 ServiceHost 时,运行时会自动向 ServiceDescription 添加一个服务行为 (ConsoleMessageTracing) 和两个操作行为(ZipCodeCaching 和 ZipCodeValidation)。
约定行为属性可以应用于服务约定接口或服务类。应用于服务类时,您可能希望限制约定行为仅在终结点使用特定约定时才生效。您可以通过在约定行为属性上实现 IContractBehaviorAttribute 并通过 TargetContract 属性指定所需约对此进行控制。
反射过程完成后,运行时还要检查应用程序配置文件,并将从 <system.serviceModel> 区找到的信息加载到 ServiceDescription 中。WCF 提供了一个 <behaviors> 区域,用于配置服务和终结点行为。在此区域发现的任何服务/终结点行为都将自动添加到 ServiceDescription 中。
为了将自定义行为放入该配置区域,必须首先编写一个派生自 BehaviorElementExtension 的类,如下所示:
public class ConsoleMessageTracingElement : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(ConsoleMessageTracing); }
    }
    protected override object CreateBehavior()
    {
        return new ConsoleMessageTracing();
    }
}
然后,必须将 BehaviorExtensionElement 注册到 <extensions> 区域,并将其映射到一个元素名。一切就绪后,您可以使用 <behaviors> 区域中注册的元素名来配置行为。图 13 提供了一个显示如何配置 ConsoleMessageTracing 行为的完整示例。
您可以使用属性添加服务、约定或操作行为,但不能使用它们添加终结点行为。您可以通过配置文件添加服务和终结点行为,但不能使用它添加约定或操作行为。最后,您可以手动向 ServiceDescription 添加任何类型的行为。图 14 总结了这些差异。
而且,请注意,您可以通过将属性应用到代理类型来利用客户端的约定和操作行为,而终结点行为是能够通过配置应用到客户端的唯一类型。

 

行为验证和绑定配置
除了添加自定义运行时扩展外,还设计了让您执行另外两个任务的行为:自定义验证和绑定配置。请注意图 9 中的 Validate 和 AddBindingParameters。
在 ServiceDescription 被初始化后,剩余的运行时被构建前,您可以使用 Validate 方法对 ServiceDescription 执行自定义验证。此时,您可以遍历 ServiceDescription 树(或客户端的 ServiceEndpoint),并根据自己的标准对其进行验证。如果有什么不能满足您的要求,可以通过引发一个异常来阻止 ServiceHost/ChannelFactory 打开。
以下服务行为用于验证 ServiceDescription,以确保没有终结点使用 BasicHttpBinding:
public class NoBasicEndpointValidator : Attribute, IServiceBehavior
{
    #region IServiceBehavior Members
    public void Validate(ServiceDescription desc, ServiceHostBase host)
    {
        foreach (ServiceEndpoint se in desc.Endpoints)
            if (se.Binding.Name.Equals("BasicHttpBinding"))
                throw new FaultException(
                    "BasicHttpBinding is not allowed");
    }

    ... //remaining methods empty
}
将该行为应用于某个服务后,运行时将不再允许您在配置终结点时使用 BasicHttpBinding,它会强制您选择一个安全的绑定。
AddBindingParameters 使您在初始化运行时时能够添加其他绑定参数。绑定参数将被提供给底层的通道层,以便影响通道堆栈的创建。自定义绑定元素能够访问这些绑定参数,并可以对自定义绑定元素进行设计以查找这些参数(关于自定义绑定的详细信息,请参阅我在 2007 年 7 月一期的“WCF 深度绑定”专栏,网址是:msdn.microsoft.com/msdnmag/issues/07/07/ServiceStation)。这是一个更加高级的扩展点,不像我介绍的其他扩展点用得那样普遍。

 

在扩展之间共享状态
当您开始在调度程序/代理中使用多个扩展时,就需要了解如何在它们之间共享状态。幸运的是,WCF 提供了可用于存储用户定义的状态的扩展对象。
扩展对象的存储位置决定了它的停留时间。可以以全局方式将它存储在 ServiceHost、InstanceContext 或 OperationContext 上。上述的每一种类都提供了一个 Extensions 集合,该集合管理派生自 IExtension<T> 的对象(其中,T 为 ServiceHostBase、InstanceContext 或 OperationContext,具体取决于集合)。
ServiceHost 扩展对象在 ServiceHost 的整个生存期内都保留在内存中,而 InstanceContext 和 OperationContext 扩展对象则只在服务实例或操作调用的生存期内保留在内存中。您的自定义调度程序/代理扩展可以使用这些集合存储(并查询)整个管道中用户定义的状态。

 

总结
WCF 提供了一个强大的扩展体系结构,可用于进行大量的运行时自定义。它在整个调度程序/代理中提供了一些关键扩展阶段,用于执行诸如参数检查、消息格式化、消息检查、操作选择和调用等任务。您可以通过实现适当的扩展接口来编写这些自定义扩展,然后通过自定义行为将您的扩展应用到调度程序/代理中。
调度程序上还提供了一些更高级的扩展点,由于版面所限,我这里就不再介绍了。这些扩展点用于处理诸如实例化、并发、寻址以及安全等事项。尽管内置的 [ServiceBehavior] 和 [OperationBehavior] 行为满足了您在这些领域的大多数行为需求,但当它们没有提供您需要的一切时,还可以编写自定义的行为来扩展运行时的那些方面。
请务必下载与本专栏有关的示例代码,以便详细研究,并在实际运用中了解它们。

 

请将您希望向 Aaron 咨询的问题和提出的意见发送至 sstation@microsoft.com.

 

Aaron Skonnard 是 Microsoft .NET 培训提供商 Pluralsight 的创始人之一。Aaron 是 Pluralsight 推出的“Web Services 2.0 应用”(Applied Web Services 2.0)、“BizTalk Server 2006 应用”(Applied BizTalk Server 2006) 和“Windows Communication Foundation 入门”(Introducing Windows Communication Foundation) 等众多课程的作者。Aaron 多年来一直从事课程研发、会议研讨和专业开发人员的培训工作。您可以通过 pluralsight.com/aaron 与他联系。
posted @ 2008-12-09 23:12  张善友  阅读(1856)  评论(1编辑  收藏  举报