在上一篇文章中, 我列出了WCF一系列的可擴展對象和元素,並簡單介紹了他們各自的功能、適合的場景和具體解決的問題。從本篇開始我將通過一個個具體的例子來介紹如何利用這些擴展點對WCF進行擴展,從而解決一些我們在實現的項目開發中可能出現的問題。
今天,我們將討論如何通過WCF extension實現多語言、本地化的功能。我們模擬這樣的一個場景:我們現在有一個支持多語言的項目,假設通過支持英文(en-US)和簡體中文(zh-CN)。我們需要創建一個service為整個系統提供message。對於這個message service,簡單起見,我們將基於不同的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端,當調用我們的message service的時候,當前culture被自動放到message header裡傳到service端;在service端,該culture 信息自動地被取出,並將service端的當前線程的UI culture設置成該值,那麼service只需要根據當前線程的culture去取message就可以了。此外考慮到我們改變線程culture可能帶來的不可預知的影響,在方法執行完畢將culture重置。
在這裡我們先來實現service端的功能:如何從message header中取出culture,並設置當前線程culture。至於Client端的實現,我們將在另一個場景中進行單獨介紹。
如何看過前一篇文章的朋友,也許會記得,在列出的8大dispatching system可擴展對象中,有一個對象很適合我們今天的多語言的場景:CallContextInitializer。顧名思義,CallContext表示基於當前線程的關於Call stack的上下文信息,這樣的信息本存放在TLS(Thread Local Storage)中。CallContextInitializer就是用於去初始化這些context的。實際上,除了call context的初始化工作之外,CallContextInitializer還可以用於call context的清理工作。
1、Message Service
在正式介紹CallContextInitializer之前,我們閒來介紹一下我們的message service。對於message service的模擬,我們仍然采用我們傳統的4層結構:Contract、Service、Hosting和Client。
對於Contract,僅僅是下面一個簡單的interface:
namespace Artech.Messages.Contract
在service layer,我通過Project property窗口定義了一個默認的Resources.Resources.resx;該resource文件會被保存在Properties目錄中;再添加一個新的Resource文件:Resources.zh-CN.resx,並把它拖到Properties目錄中。在這兩個Resource中定義相同的resource item:
{
[ServiceContract]
public interface IMessage
{
[OperationContract]
string GetMessage();
}
}
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>
以及Client端的Code:
<system.serviceModel>
<client>
<endpoint address="http://127.0.0.1/messageservice" binding="basicHttpBinding"
contract="Artech.Messages.Contract.IMessage" name="messageservice" />
</client>
</system.serviceModel>
</configuration>
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為定義的常量。將message header放到OutgoingMessageHeaders集合中,通過proxy對象調用message service獲得對應的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
由於我們只想改變執行service方法過程中線程的culture,所以我們在BeforeInvoke中先通過一個Array將CurrentCulture和CurrentUICulture保存起來,然後根據message header的local name和namespace將client端傳入的culture獲取出來,將此值設置到當前線程。最有返回保存有原來CurrentCulture和CurrentUICulture的Array。在AfterInvoke中通過correlationState參數將這個Array取出,重置CurrentCulture和CurrentUICulture。
{
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
}
}
3、通過OperationBehavior應用CallContextInitializer
由於CallContextInitializer是DispatchOperation的屬性,DispatchOperation又可以通過DispatchRuntime的Operations集合中獲得。所以我們可以有兩個方式將我們創建的CultureSettingCallContextInitializer應用到Dispatching system中。一是通過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去掉,我們一樣可以得到同上面一樣的輸出結果。
本文配套源碼