在上一篇文章中, 我列出了WCF一系列的可扩展对象和元素,并简单介绍了他们各自的功能、适合的场景和具体解决的问题。从本篇开始我将通过一个个具体的例子来介绍如何利用这些扩展点对WCF进行扩展,从而解决一些我们在实现的项目开发中可能出现的问题。
今天,我们将讨论如何通过WCFextension实现多语言、本地化的功能。我们模拟这样的一个场景:我们现在有一个支持多语言的项目,假设通过支持英文(en-US)和简体中文(zh-CN)。我们需要创建一个service为整个系统提供message。对于这个messageservice,简单起见,我们将基于不同的culture的message存储于不同的Resource文件中,客户端通过访问service来获取基于它自己本地culture的message。比如,如果某一个客户端当前的culture是en-US,那么会得到英文的message,如果是zh-CN将会得到简体中文的message。
我们很多人会说,在获取message的时候将client端本地的culture作为API的参数传递到service端,service再根据相应的culture从对应的resource文件中获取message不就可以了吗?这样做不是不可以,但是不过优雅。从业务逻辑和非业务逻辑的分离来讲是不是一个好的解决方案,因为从某种意义上讲,culture信息是业务无关的,不适合作为API的一部分,API应该只和具体的业务逻辑相关联。
今天给出的解决方式基于这样的实现原理:在Client端,当调用我们的messageservice的时候,当前culture被自动放到message header里传到service端;在service端,该culture信息自动地被取出,并将service端的当前线程的UIculture设置成该值,那么service只需要根据当前线程的culture去取message就可以了。此外考虑到我们改变线程culture可能带来的不可预知的影响,在方法执行完毕将culture重置。
在这里我们先来实现service端的功能:如何从message header中取出culture,并设置当前线程culture。至于Client端的实现,我们将在另一个场景中进行单独介绍。
如何看过前一篇文章的朋友,也许会记得,在列出的8大dispatchingsystem可扩展对象中,有一个对象很适合我们今天的多语言的场景:CallContextInitializer。顾名思义,CallContext表示基于当前线程的关于Call stack的上下文信息,这样的信息本存放在TLS(Thread LocalStorage)中。CallContextInitializer就是用于去初始化这些context的。实际上,除了callcontext的初始化工作之外,CallContextInitializer还可以用于call context的清理工作。
1、Message Service
在正式介绍CallContextInitializer之前,我们闲来介绍一下我们的message service。对于message service的模拟,我们仍然采用我们传统的4层结构:Contract、Service、Hosting和Client。
对于Contract,仅仅是下面一个简单的interface:
namespace Artech.Messages.Contract
{
[ServiceContract]
public interface IMessage
{
[OperationContract]
string GetMessage();
}
}
在service layer,我通过Projectproperty窗口定义了一个默认的Resources.Resources.resx;该resource文件会被保存在Properties目录中;再添加一个新的Resource文件:Resources.zh-CN.resx,并把它拖到Properties目录中。在这两个Resource中定义相同的resource item:
Service的代码很简单,仅仅是以强类型的方式获取该resource item而已:
namespace Artech.Messages.Service
{
public class MessageService:IMessage
{
IMessage Members#region IMessage Members
public string GetMessage()
{
return Resources.HelloWorld;
}
#endregion
}
}
下面是Hosting的Code和configuraion:
namespace Artech.Messages.Hosting
{
public class Program
{
public static void Main()
{
using (ServiceHost host = new ServiceHost(typeof(MessageService)))
{
host.Opened += delegate
{
Console.WriteLine("Message service has been started up!");
};
host.Open();
Console.Read();
}
}
}
}
<
configuration
>
<
system.serviceModel
>
<
services
>
<
service
name
="Artech.Messages.Service.MessageService"
>
<
endpoint
binding
="basicHttpBinding"
contract
="Artech.Messages.Contract.IMessage"
/>
<
host
>
<
baseAddresses
>
<
add
baseAddress
="http://127.0.0.1/messageservice"
/>
</
baseAddresses
>
</
host
>
</
service
>
</
services
>
</
system.serviceModel
>
</
configuration
>
这是Client端的Configuration:
<
configuration
>
<
system.serviceModel
>
<
client
>
<
endpoint
address
="http://127.0.0.1/messageservice"
binding
="basicHttpBinding"
contract
="Artech.Messages.Contract.IMessage"
name
="messageservice"
/>
</
client
>
</
system.serviceModel
>
</
configuration
>
以及Client端的Code:
namespace Artech.Messages.Client
{
class Program
{
private const string CultureInfoHeadLocalName = "__CultureInfo";
private const string CultyreInfoHeaderNamespace = "urn:artech.com";
static void Main(string[] args)
{
using (ChannelFactory<IMessage> channelFactory = new ChannelFactory<IMessage>("messageservice"))
{
IMessage proxy = channelFactory.CreateChannel();
using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
{
MessageHeader<CultureInfo> header = new MessageHeader<CultureInfo>(Thread.CurrentThread.CurrentUICulture);
OperationContext.Current.OutgoingMessageHeaders.Add(header.GetUntypedHeader(CultureInfoHeadLocalName,CultyreInfoHeaderNamespace));
Console.WriteLine("The UI culture of current thread is {0}", Thread.CurrentThread.CurrentUICulture);
Console.WriteLine(proxy.GetMessage());
}
Thread.CurrentThread.CurrentUICulture = new CultureInfo("zh-CN");
using (OperationContextScope contextScope = new OperationContextScope(proxy as IContextChannel))
{
MessageHeader<CultureInfo> header = new MessageHeader<CultureInfo>(Thread.CurrentThread.CurrentUICulture);
OperationContext.Current.OutgoingMessageHeaders.Add(header.GetUntypedHeader(CultureInfoHeadLocalName, CultyreInfoHeaderNamespace));
Console.WriteLine("The UI culture of current thread is {0}", Thread.CurrentThread.CurrentUICulture);
Console.WriteLine(proxy.GetMessage());
}
}
Console.Read();
}
}
}
在这里做一些简单的介绍:通过ChannelFactory创建Channel(proxy)对象,利用此Channel(proxy)创建OperationContextScope(OperationContextScope和OperationContext的关系就如同TransactionScope和Transaction的关系一样,相当于定义OperationContext的作用范围)。在此OperationContextScope作用范围内创建MessageHeader,内容为当前线程的UICulture,Localname和Namespace为定义的常量。将messageheader放到OutgoingMessageHeaders集合中,通过proxy对象调用messageservice获得对应的service,由于第一次调用使用的是默认的culture(en-US),我们希望返回的结果是英文,而第二次service invocation的culture为zh-CN,所以我们希望返回的结果是中文。
2、创建CallContextInitializer
为了实现Localization,我们先创建一个CallContextInitializer,我们姑且叫它CultureSettingCallContextInitializer。所有的CallContextInitializer都实现接口:System.ServiceModel.Dispatcher.ICallContextInitializer。
public interface ICallContextInitializer
{
void AfterInvoke(object correlationState);
object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message);
}
该结构定义了两个方法:BeforeInvoke和AfterInvoke,允许你在真正的service方法执行前和执行后对CallContext进行初始化和清理。如何你希望在BeforeInvoke创建的对象能够被AfterInvoke,你可以将该对象作为BeforeInvoke 的返回值,在执行AfterInvoke的时候,该值将作为其中的参数。
这是CultureSettingCallContextInitializer的定义:
namespace Artech.CallContextInitializers
{
public class CultureSettingCallContextInitializer:ICallContextInitializer
{
private const string CultureInfoHeadLocalName = "__CultureInfo";
private const string CultyreInfoHeaderNamespace = "urn:artech.com";
ICallContextInitializer Members#region ICallContextInitializer Members
public void AfterInvoke(object correlationState)
{
CultureInfo[] currentCulture = correlationState as CultureInfo[];
Thread.CurrentThread.CurrentCulture = currentCulture[0];
Thread.CurrentThread.CurrentUICulture = currentCulture[1];
}
public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
{
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
CultureInfo currentUICulture = Thread.CurrentThread.CurrentUICulture;
if (message.Headers.FindHeader(CultureInfoHeadLocalName, CultyreInfoHeaderNamespace) > -1)
{
CultureInfo cultureInfo = message.Headers.GetHeader<CultureInfo>(CultureInfoHeadLocalName, CultyreInfoHeaderNamespace);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = cultureInfo;
}
return new CultureInfo[] { currentCulture, currentUICulture };
}
#endregion
}
}
由于我们只想改变执行service方法过程中线程的culture,所以我们在BeforeInvoke中先通过一个Array将CurrentCulture和CurrentUICulture保存起来,然后根据message header的localname和namespace将client端传入的culture获取出来,将此值设置到当前线程。最有返回保存有原来CurrentCulture和CurrentUICulture的Array。在AfterInvoke中通过correlationState参数将这个Array取出,重置CurrentCulture和CurrentUICulture。
3、通过OperationBehavior应用CallContextInitializer
由于CallContextInitializer是DispatchOperation的属性,DispatchOperation又可以通过DispatchRuntime的Operations集合中获得。所以我们可以有两个方式将我们创建的CultureSettingCallContextInitializer应用到Dispatchingsystem中。一是通过OperationBehavior,而是通过EndpointBehavior。
我们现在介绍OperationBehavior的解决方案,由于OperationBehavior是通过Attribute的形式被使用,所以我们的将OperationBehavior定义成继承Attribute的class:CultureSettingBehaviorAttribute。
namespace
Artech.CallContextInitializers
{
public class CultureSettingBehaviorAttribute:Attribute,IOperationBehavior
{
IOperationBehavior Members#region IOperationBehavior Members
public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
{}
public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
{}
public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
{
dispatchOperation.CallContextInitializers.Add(new CultureSettingCallContextInitializer());
}
public void Validate(OperationDescription operationDescription)
{}
#endregion
}
}
实际上只有一句有意思的code,将CultureSettingCallContextInitializer对象设置到应用了OperationBehavior
对应的DispatchOperation对象上:dispatchOperation.CallContextInitializers.Add(new CultureSettingCallContextInitializer());
那么我们就可以将此CultureSettingBehaviorAttribute直接应用到Contract的Operation上、或者Service的对应的method上。比如我们应用到Contract的GetMessage上,那么它将影响到所有实现了该contract的所有service。
namespace Artech.Messages.Contract
{
[ServiceContract]
public interface IMessage
{
[OperationContract]
[CultureSettingBehavior]
string GetMessage();
}
}
这时候我们运行程序,将会得到如何的输出:
4、通过EndpointBehavior运用CallContextInitializer
我们接着来讨论另一种运用CallContextInitializer的方式:通过EndpointBehavior。为此u,我们创建了我们的EndpointBehavior:CultureSettingBehavior。
namespace Artech.CallContextInitializers
{
public class CultureSettingBehavior: IEndpointBehavior
{
IEndpointBehavior Members#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
foreach (var operation in endpointDispatcher.DispatchRuntime.Operations)
{
operation.CallContextInitializers.Add(new CultureSettingCallContextInitializer());
}
}
public void Validate(ServiceEndpoint endpoint)
{}
#endregion
}
}
有效的代码也是在ApplyDispatchBehavior中,我们通过遍历endpointDispatcher.DispatchRuntime.Operations集合,将每个DispatchOperation的CallContextInitializers中加上我们的CultureSettingCallContextInitializer对象。
EndpointBehavior只能通过Configuration的方式使用,我们还需要为此创建一个BehaviorExtensionElement:CultureSettingBehaviorElement。
namespace Artech.CallContextInitializers
{
public class CultureSettingBehaviorElement: BehaviorExtensionElement
{
public override Type BehaviorType
{
get
{
return typeof(CultureSettingBehavior);
}
}
protected override object CreateBehavior()
{
return new CultureSettingBehavior();
}
}
}
那么我们就可以根据配置文件来应用我们的自定义的EndpointBehavior了:
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="cultureSettingBehavior">
<cultureSettingElement />
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="cultureSettingElement" type="Artech.CallContextInitializers.CultureSettingBehaviorElement, Artech.CallContextInitializers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<services>
<service name="Artech.Messages.Service.MessageService">
<endpoint behaviorConfiguration="cultureSettingBehavior" binding="basicHttpBinding"
contract="Artech.Messages.Contract.IMessage" />
<host>
<baseAddresses>
<add baseAddress="http://127.0.0.1/messageservice" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
在contract中,将CultureSettingBehaviorAttribute去掉,我们一样可以得到同上面一样的输出结果。