通过“四大行为”对WCF的扩展[实例篇]
为了让读者对如何利用相应的行为对WCF进行扩展有个深刻的认识,在这里我提供一个简单的实例演示。本实例模拟的场景是这样的:我们创建一个支持多语言的资源服务,该服务旨在为调用者提供基于某种语言的文本型资源。但是,我们不希望客户端在每次调用服务的时候都显式地制定具体的语言,而是根据客户端服务调用线程表示语言文化的上下文来自动识别所需的语言。[源代码从这里下载]
要让资源服务具有识别语言文化的能够,我们必须将客户端服务调用线程当前的语言文化信息(具体来说就是Thread的两个属性:CurrentUICulture和CurrentCulture)自动传递到服务端。我们具体的实现原理是这样的:我们将客户端服务调用线程的CurrentUICulture和CurrentCulture的语言文化代码保存在出栈消息的SOAP报头中,并为它们起一个预定义的名称和命名空间;在服务操作在服务端执行之前,我们根据这个预定义SOAP报头名称和命名空间将这两个语言文化代码从入栈消息中获取出来,创建相应的CultureInfo对象并作为服务操作执行线程的CurrentUICulture和CurrentCulture。那么服务操作在执行的时候,只需要根据当前线程的语言文化上下文提供相应资源就可以了。接下来,我们就来一步一步地实现这样一个简单的扩展。
目录
步骤一、创建自定义CallContextInitializer:CultureReceiver
步骤二、自定义ClientMessageInspector:CultureSender
步骤三、创建行为:CulturePropagationBehaviorAttribute
步骤四、为CulturePropagationBehaviorAttribute定义配置元素
步骤五、创建实例应用检验语言文化的自动传播
步骤一、创建自定义CallContextInitializer:CultureReceiver
所谓客户端当前语言文化信息的传递,无外乎是客户端将当前线程的CurrentUICulture和CurrentCulture放到出栈消息中;而服务端将其从入栈消息中取出,并对当前线程的CurrentUICulture和CurrentCulture进行相应的设置。我们先来实现在服务端用于进行语言文化信息获取的组件,我将其命名为CultureReceiver。
由于CultureReceiver在从入栈消息中获取表示客户端线程的CurrentUICulture和CurrentCulture信息的时,需要预先知道相应报头的名称和命名空间(命名空间仅仅用于SOAP报头),为此我们将这些定义成如下一个名称为CultureMessageHeaderInfo的类。属性CurrentCultureName,CurrentUICultureName和Namespace分别表示代码客户端线程CurrentCulture和CurrentUICulture的报头名称。
1: namespace Artech.WcfExtensions.CulturePropagation
2: {
3: internal class CultureMessageHeaderInfo
4: {
5: public string Namespace{ get; set; }
6: public string CurrentCultureName{ get; set; }
7: public string CurrentUICultureName { get; set; }
8: }
9: }
而我们进行语言文化报头接收以及对服务操作执行线程当前语言文化的设置的CultureReceiver组件被定义成一个实现了接口ICallContextInitializer的CallContextInitializer对象。在介绍WCF服务端运行时框架的时候,我们已经对CallContextInitializer进行了说明。我们说CallContextInitializer的两个方法BeforeInvoke和AfterInvoke方法分别在操作方法执行前后被调用。
而我们恰好可以通过实现BeforeInvoke方法将存放在入栈消息报头的表示客户端线程CurrentCulture和CurrentUICulture的内容取出,并以此创建相应的CultureInfo作为当前线程的CurrentCulture和CurrentUICulture。由于WCF服务端采用线程池的机制处理客户端请求,线程会被重用,所以我们有必要在操作方法执行之后将当前线程的语言文化设置恢复到之前的状态,而这恰好可以实现在AfterInvoke方法中。下面的代码片断表示CultureReceiver的全部定义。
1: using System.Globalization;
2: using System.ServiceModel;
3: using System.ServiceModel.Channels;
4: using System.ServiceModel.Dispatcher;
5: using System.Threading;
6: namespace Artech.WcfExtensions.CulturePropagation
7: {
8: internal class CultureReceiver : ICallContextInitializer
9: {
10: public CultureMessageHeaderInfo messageHeaderInfo;
11: public CultureReceiver(CultureMessageHeaderInfo messageHeaderInfo)
12: {
13: this.messageHeaderInfo = messageHeaderInfo;
14: }
15:
16: public void AfterInvoke(object correlationState)
17: {
18: CultureInfo[] cultureInfos = correlationState as CultureInfo[];
19: if (null != cultureInfos)
20: {
21: Thread.CurrentThread.CurrentCulture = cultureInfos[0];
22: Thread.CurrentThread.CurrentUICulture = cultureInfos[1];
23: }
24: }
25:
26: public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
27: {
28: CultureInfo[] originalCulture = new CultureInfo[] { CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture };
29: CultureInfo currentCulture = null;
30: CultureInfo currentUICulture = null;
31: if (message.Headers.FindHeader(this.messageHeaderInfo.CurrentCultureName, this.messageHeaderInfo.Namespace) > -1)
32: {
33: currentCulture = new CultureInfo(message.Headers.GetHeader<string>(this.messageHeaderInfo.CurrentCultureName,
34: this.messageHeaderInfo.Namespace));
35: Thread.CurrentThread.CurrentCulture = currentCulture;
36: }
37: if (message.Headers.FindHeader(this.messageHeaderInfo.CurrentUICultureName, this.messageHeaderInfo.Namespace) > -1)
38: {
39: currentUICulture = new CultureInfo(message.Headers.GetHeader<string>(this.messageHeaderInfo.CurrentUICultureName,
40: this.messageHeaderInfo.Namespace));
41: Thread.CurrentThread.CurrentUICulture = currentUICulture;
42: }
43: return originalCulture;
44: }
45: }
46: }
步骤二、自定义ClientMessageInspector:CultureSender
对于客户端,我们通过自定义一个ClientMessageInspector利用“消息检验”机制将代表当前线程CurrentCulture和CurrentUICulture的语言文化代码以SOAP报头的形式植入请求消息中。我们将改自定义的ClientMessageInspector称为CultureSender。如下面的代码所示,上述的关于客户端线程当前语言文化信息的发送实现在BeforeSendRequest方法中。
1: using System.Globalization;
2: using System.ServiceModel;
3: using System.ServiceModel.Channels;
4: using System.ServiceModel.Dispatcher;
5: namespace Artech.WcfExtensions.CulturePropagation
6: {
7: internal class CultureSender: IClientMessageInspector
8: {
9: private CultureMessageHeaderInfo messageHeaderInfo;
10:
11: public CultureSender(CultureMessageHeaderInfo messageHeaderInfo)
12: {
13: this.messageHeaderInfo = messageHeaderInfo;
14: }
15:
16: public void AfterReceiveReply(ref Message reply, object correlationState) { }
17: public object BeforeSendRequest(ref Message request, IClientChannel channel)
18: {
19: request.Headers.Add(MessageHeader.CreateHeader(this.messageHeaderInfo.CurrentCultureName, this.messageHeaderInfo.Namespace, CultureInfo.CurrentCulture.Name));
20: request.Headers.Add(MessageHeader.CreateHeader(this.messageHeaderInfo.CurrentUICultureName, this.messageHeaderInfo.Namespace, CultureInfo.CurrentUICulture.Name));
21: return null;
22: }
23: }
24: }
步骤三、创建行为:CulturePropagationBehaviorAttribute
到目前为止,真正实现语言文化信息从客户端到服务端传播的自定义CallContextInitialier(CultureReceiver)和ClientMessageInspector(CultureSender)都已经创建好了。我们目前需要做的是通过定义相应的行为将这两个自定义组件分别应用到WCF的服务端和客户端运行时框架中去。具体来说,我们需要创建CultureReceiver对象并将其添加到相应DispatchOperation的CallContextInitializer列表之中,创建CultureSender对象并将其添加到ClientRuntime的MessageInspector列表之中。
以下定义的CulturePropagationBehaviorAttribute就是这样一个行为。从下面给出的代码中我们可以看到,CulturePropagationBehaviorAttribute实现了三个行为接口(IServiceBehavior, IEndpointBehavior, IContractBehavior),所以它既是一个服务行为,同时也是一个终结点行为和契约行为。我们自定义的CultureReceiver和CultureSender分别通过ApplyDispatchBehavior和ApplyClientBehavior方法被应用到服务端和客户端运行时框架。
1: using System;
2: using System.Collections.ObjectModel;
3: using System.ServiceModel;
4: using System.ServiceModel.Channels;
5: using System.ServiceModel.Description;
6: using System.ServiceModel.Dispatcher;
7: namespace Artech.WcfExtensions.CulturePropagation
8: {
9: public class CulturePropagationBehaviorAttribute: Attribute, IServiceBehavior,
10: IEndpointBehavior, IContractBehavior
11: {
12: private CultureMessageHeaderInfo messageHeaderInfo;
13:
14: public const string DefaultNamespace = "http://www.artech.com/culturepropagation";
15: public const string DefaultCurrentCultureName = "CurrentCultureName";
16: public const string DefaultCurrentUICultureName = "CurrentUICultureName";
17:
18: public string Namespace
19: {
20: get{return messageHeaderInfo.Namespace;}
21: set{messageHeaderInfo.Namespace = value;}
22: }
23: public string CurrentCultureName
24: {
25: get { return messageHeaderInfo.CurrentCultureName; }
26: set { messageHeaderInfo.CurrentCultureName = value; }
27: }
28: public string CurrentUICultureName
29: {
30: get { return messageHeaderInfo.CurrentUICultureName; }
31: set { messageHeaderInfo.CurrentUICultureName = value; }
32: }
33:
34: public CulturePropagationBehaviorAttribute()
35: {
36: messageHeaderInfo = new CultureMessageHeaderInfo
37: {
38: Namespace = DefaultNamespace,
39: CurrentCultureName = DefaultCurrentCultureName,
40: CurrentUICultureName = DefaultCurrentUICultureName
41: };
42: }
43:
44: //IServiceBehavior
45: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase,
46: Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) {}
47: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
48: {
49: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
50: {
51: foreach (EndpointDispatcher endpoint in channelDispatcher.Endpoints)
52: {
53: foreach (DispatchOperation operation in endpoint.DispatchRuntime.Operations)
54: {
55: operation.CallContextInitializers.Add(new CultureReceiver(messageHeaderInfo));
56: }
57: }
58: }
59: }
60: public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
61:
62: //IEndpointBehavior
63: public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
64: public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
65: {
66: clientRuntime.MessageInspectors.Add(new CultureSender(messageHeaderInfo));
67: }
68: public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
69: {
70: foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
71: {
72: operation.CallContextInitializers.Add(new CultureReceiver(messageHeaderInfo));
73: }
74: }
75: public void Validate(ServiceEndpoint endpoint) { }
76:
77: //IContractBehavior
78: public void AddBindingParameters(ContractDescription contractDescription,
79: ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
80: public void ApplyClientBehavior(ContractDescription contractDescription,
81: ServiceEndpoint endpoint, ClientRuntime clientRuntime)
82: {
83: clientRuntime.MessageInspectors.Add(new CultureSender(messageHeaderInfo));
84: }
85: public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint,
86: DispatchRuntime dispatchRuntime)
87: {
88: foreach (DispatchOperation operation in dispatchRuntime.Operations)
89: {
90: operation.CallContextInitializers.Add(new CultureReceiver(messageHeaderInfo));
91: }
92: }
93: public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { }
94: }
95: }
由于作为服务行为和契约行为的CulturePropagationBehaviorAttribute又是一个自定义特性,所以我们可以直接将其应用到服务契约接口(作为契约行为)或者服务类型(作为服务行为)上。在应用该行为特性时,你可以设置CurrentCultureName、CurrentUICultureName和Namespace属性改变封装客户端线程CurrentCulture和CurrentUICulture的SOAP报头的名称和命名空间。如果此三个属性没有经过显式设置,它们具有默认值。
作为契约行为:
1: [ServiceContract]
2: [CulturePropagationBehavior(CurrentCultureName = "culture", CurrentUICultureName = "uiCulture")]
3: public interface IResourceService
4: {
5: [OperationContract]
6: string GetString(string key);
7: }
作为服务行为:
1: [CulturePropagationBehavior(CurrentCultureName = "culture", CurrentUICultureName = "uiCulture")]
2: public class ResourceService : IResourceService
3: {
4: public string GetString(string key)
5: {
6: //省略实现
7: }
8: }
步骤四、为CulturePropagationBehaviorAttribute定义配置元素
除了通过编程的方式应用行为之外,对于WCF四种类型的行为,契约行为和操作行为只能通过以自定义特性的方式以声明的方式分别应用到服务契约接口(或者类)和操作契约方法或者操作实现方法上。终结点行为只能通过配置的方式应用到对应的终结点。而服务行为,则可以同时采用声明和配置的方式应用到目标服务上面。
由于我们定义的CulturePropagationBehaviorAttribute既是服务行为,也是终结点行为,为了实现以配置的方式来使用该行为,我们需要为之创建一个配置元素的类。作为服务行为或者终结点行为的配置元素类均继承自抽象类BehaviorExtensionElement。
在这里我们创建了如下一个CulturePropagationBehaviorElement类来定义CulturePropagationBehaviorAttribute的配置元素。在BehaviorExtensionElement中,我们定义了三个可选(IsRequired = false)的配置属性CurrentCultureName、CurrentUICultureName和Namespace分别代表用于封装客户端线程CurrentCulture和CurrentUICulture的SOAP报头名称和命名空间。
1: using System;
2: using System.Configuration;
3: using System.ServiceModel.Configuration;
4: namespace Artech.WcfExtensions.CulturePropagation
5: {
6: public class CulturePropagationBehaviorElement: BehaviorExtensionElement
7: {
8: [ConfigurationProperty("namespace",IsRequired =false,
9: DefaultValue = CulturePropagationBehaviorAttribute.DefaultNamespace)]
10: public string Namespace
11: {
12: get { return (string)this["namespace"]; }
13: set { this["namespace"] = value; }
14: }
15: [ConfigurationProperty("currentCultureName", IsRequired = false,
16: DefaultValue = CulturePropagationBehaviorAttribute.DefaultCurrentCultureName)]
17: public string CurrentCultureName
18: {
19: get { return (string)this["currentCultureName"]; }
20: set { this["currentCultureName"] = value; }
21: }
22: [ConfigurationProperty("currentUICultureName", IsRequired = false,
23: DefaultValue = CulturePropagationBehaviorAttribute.DefaultCurrentUICultureName)]
24: public string CurrentUICultureName
25: {
26: get { return (string)this["currentUICultureName"]; }
27: set { this["currentUICultureName"] = value; }
28: }
29: public override Type BehaviorType
30: {
31: get { return typeof(CulturePropagationBehaviorAttribute); }
32: }
33: protected override object CreateBehavior()
34: {
35: return new CulturePropagationBehaviorAttribute
36: {
37: Namespace = this.Namespace,
38: CurrentCultureName = this.CurrentCultureName,
39: CurrentUICultureName = this.CurrentUICultureName
40: };
41: }
42: }
43: }
下面的XML片断反映了如何将CulturePropagationBehaviorAttribute作为服务行为以配置的方式应用到目标服务上。首先,我们需要将基于服务行为的配置元素类型以行为或者的形式定义在<extensions>/<behaviorExtensions>结点下,并给它一个名称(在这里将我们定义的扩展起名为culturePropagation)。在配置服务行为的时候,我们只需要在行为配置节点中添加以行为扩展名为元素名的XML结点,并对定义在配置元素类型中的配置属性进行相应的设置即可。在本例中,我们定义了一个名称为defaultSvcBehavior的服务行为。该行为结点的字结点<culturePropagation>代表我们自定义的CulturePropagationBehaviorAttribute行为。在<culturePropagation>结点下,我们对namespace、currentCultureName和currentUICultureName作了显式设置(由于三个配置属性可选的,如果没有对它们进行显式设置,它们将会具有一个默认值)。最终这个名称为defaultSvcBehavior的服务行为被应用到了ResourceService服务上(behaviorConfiguration="defaultSvcBehavior")。
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service behaviorConfiguration="defaultSvcBehavior"
5: name="Artech.WcfServices.Servicies.ResourceService">
6: <endpoint address = “http://127.0.0.1:3721/resourceservice”
7: binding="ws2007HttpBinding"
8: contract="Artech.WcfServices.Contracts.IResourceService" />
9: </service>
10: </services>
11: <behaviors>
12: <serviceBehaviors>
13: <behavior name="defaultSvcBehavior">
14: <culturePropagation
15: namespace="http://www.artech.com/"
16: currentCultureName="cultureName"
17: currentUICultureName="uiCultureName"/>
18: </behavior>
19: </serviceBehaviors>
20: </behaviors>
21: <extensions>
22: <behaviorExtensions>
23: <add name="culturePropagation"
24: type="Artech.WcfExtensions.CulturePropagation.CulturePropagationBehaviorElement, Artech.WcfExtensions.Lib" />
25: </behaviorExtensions>
26: </extensions>
27: </system.serviceModel>
28: </configuration>
如果你需要将CulturePropagationBehaviorAttribute以终结点行为的方式应用到服务的某个终结点上,配置方式与此类似。首先,你同样需要将代表行为配置元素的类型名称定义成行为扩展。然后将代表该行为配置的XML结点添加到终结点配置节点即可。在下面这段配置中,ResourceService服务具有唯一的终结点,该终结点应用了一个名称为defaultEndpointBehavior的终结点行为。而该种节点行为包含了CulturePropagationBehaviorAttribute的相关配置。
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Servicies.ResourceService">
5: <endpoint address = "http://127.0.0.1:3721/resourceservice"
6: binding = "ws2007HttpBinding"
7: contract ="Artech.WcfServices.Contracts.IResourceService"
8: behaviorConfiguration="defaultEndpointBehavior"/>
9: </service>
10: </services>
11: <behaviors>
12: <endpointBehaviors>
13: <behavior name="defaultEndpointBehavior">
14: <culturePropagation
15: Namespace ="http://www.artech.com/"
16: currentCultureName ="cultureName"
17: currentUICultureName="uiCultureName"/>
18: </behavior>
19: </endpointBehaviors>
20: </behaviors>
21: <extensions>
22: <behaviorExtensions>
23: <add name="culturePropagation" type="Artech.WcfExtensions.CulturePropagation.CulturePropagationBehaviorElement, Artech.WcfExtensions.Lib" />
24: </behaviorExtensions>
25: </extensions>
26: </system.serviceModel>
27: </configuration>
步骤五、创建实例应用检验语言文化的自动传播
到目前为止,关于实现语言文化从客户端自动传播到服务端的所有扩展实现均已完成。为了检验我们自定义的行为CulturePropagationBehaviorAttribute是否真的能够实现这个目标,我们需要通过建立一个简单的WCF应用程序来检验。
我们采用之前介绍过的文本型资源提供服务,为此我们创建了如下一个简单的代表服务契约接口IResourceService,该服务契约具有一个唯一的操作契约方法GetString用于获取基于给定的键值得到的对应的文本型资源的内容。需要注意的是,我们定义的CulturePropagationBehaviorAttribute以契约行为的形式应用到IResourceService接口之上。
1: using System.ServiceModel;
2: using Artech.WcfExtensions.CulturePropagation;
3: namespace Artech.WcfServices.Contracts
4: {
5: [ServiceContract(Namespace = "http://www.artech.com/")]
6: [CulturePropagationBehavior]
7: public interface IResourceService
8: {
9: [OperationContract]
10: string GetString(string key);
11: }
12: }
为了提供对于多语言的支持,我们将资源文本的内容定义在资源文件中。为此我们在定义服务类型的项目中添加了如下图所示的两个资源文件。其中Resources.resx代表语言文化中性的资源文件,而Resources.zh-CN.resx则代表基于中国(大陆)简体中文的资源文件。两个资源文件定义了英文和中文作为内容的两个文本资源条目HappyNewYear和MerryChristmas。
在默认的情况下,添加语言文化中性的资源文件会自动生成方便访问资源条目的代码。在服务类型的GetString方法中,我就直接使用定义在自动生成的Resources类的静态属性ResourceManager(相应类型为System.Resources.ResourceManager)来获取给定键值的相应文本资源的内容。
1: using Artech.WcfServices.Contracts;
2: using Artech.WcfServices.Servicies.Properties;
3: namespace Artech.WcfServices.Servicies
4: {
5: public class ResourceService : IResourceService
6: {
7: public string GetString(string key)
8: {
9: return Resources.ResourceManager.GetString(key);
10: }
11: }
12: }
然后,服务ResourceService以控制台应用作为宿主进行简单的自我寄宿。下面是服务端配置和客户端配置,由于我们自定义的CulturePropagationBehaviorAttribute是以声明的方式作为契约行为应用到契约接口上,所以配置中并不包含相关的内容。
服务寄宿端配置:
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Servicies.ResourceService">
5: <endpoint address = "http://127.0.0.1:3721/resourceservice"
6: binding ="ws2007HttpBinding"
7: contract ="Artech.WcfServices.Contracts.IResourceService"/>
8: </service>
9: </services>
10: </system.serviceModel>
11: </configuration>
客户端配置:
1: <configuration>
2: <system.serviceModel>
3: <client>
4: <endpoint name ="resourceservice"
5: address = "http://127.0.0.1:3721/resourceservice"
6: binding ="ws2007HttpBinding"
7: contract ="Artech.WcfServices.Contracts.IResourceService"/>
8: </client>
9: </system.serviceModel>
10: </configuration>
客户端同样是一个控制台应用,下面是进行服务调用的代码。从中我们可以看到,我们一共进行了四次针对GetString操作的服务调用,在调用之前我们对当前线程的CurrentUICulture(它决定了语言的种类和对资源文件的选择)。前面两次和后面两次是在CurrentUICulture为en-US和zh-CN情况下进行调用的。从输出结果我们可以清晰地看到:客户端得到的资源文本的语言正好是和当前线程的CurrentUICulture一致的,而这正是应用在契约接口上CulturePropagationBehaviorAttribute特性所致。
1: using System;
2: using System.ServiceModel;
3: using System.Threading;
4: using Artech.WcfServices.Contracts;
5: namespace Client
6: {
7: class Program
8: {
9: static void Main(string[] args)
10: {
11: using(ChannelFactory<IResourceService> channelFactory = new ChannelFactory<IResourceService>("resourceservice"))
12: {
13: IResourceService proxy = channelFactory.CreateChannel();
14: Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
15: Console.WriteLine(proxy.GetString("HappyNewYear"));
16: Console.WriteLine(proxy.GetString("MerryChristmas")+"\n");
17:
18: Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-CN");
19: Console.WriteLine(proxy.GetString("HappyNewYear"));
20: Console.WriteLine(proxy.GetString("MerryChristmas"));
21: }
22: Console.Read();
23: }
24: }
25: }
输出结果:
1: Happy New Year!
2: Merry Christmas!
3:
4: 新年快乐!
5: 圣诞快乐!