WCF 中的全球化模式 (WS-I18N 实现)






4.59/5 (18投票s)
Pablo Cibraro 描述了如何使用 .NET 框架提供的全球化支持来实现 WCF (Windows Communication Foundation) 服务的国际化。
引言
随着商业活动的日益全球化,应用程序和服务变得多语言和文化意识的需求日益增长。 .NET 框架已经为国际化提供了全面的支持,但如何将其应用于服务设计中并不总是很清楚。在本文中,我将描述一些可用于 Web 服务的全球化模式,以及如何使用 WCF 提供的扩展点成功应用它们。此外,我还将根据最近发布的 WCF Web 服务国际化 (WS-I18N) 工作草案,提供一些代码示例。
全球化模式
在设计或开发阶段,至少有三种不同的全球化模式可应用于 WCF 服务。这些模式是相互排斥的;它们不能一起使用,因此在开始工作之前了解它们的区别至关重要。
- 区域设置中性:在此模式下,服务的大多数方面不受区域设置影响。这是最简单的情况,不需要额外的考虑。例如,“计算器”服务执行算术运算。
- 服务确定:在此,服务始终在一个确定的区域设置下运行,该区域设置可以是主机默认的区域设置,也可以是为此服务专门配置的区域设置。例如,一个始终返回英语消息的 Web 服务。
- 客户端影响:在这种情况下,服务可能会在客户端应用程序提供的区域设置下运行。正如“WS-I18N”所述,该服务是“受影响的”,因为它可以根据其实现方式来考虑其运行的区域设置,或者不考虑。
这些模式未考虑使用不同的服务实现和数据协定来为多语言或跨文化环境中的客户端应用程序提供服务的情况。
让我们详细看看这些模式。
区域设置中性
这是最简单的情况,无需考虑或处理任何全球化功能。
[ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples")]
public interface ICalculator
{
[OperationContract]
double Add(double n1, double n2);
[OperationContract]
double Subtract(double n1, double n2);
}
// Service class which implements the service contract.
public class CalculatorService : ICalculator
{
public double Add(double n1, double n2)
{
return n1 + n2;
}
public double Subtract(double n1, double n2)
{
return n1 - n2;
}
}
上面的代码非常直接。它只执行一些简单的算术运算,但正如您所看到的,这些运算不受任何区域设置的影响。无论主机的区域设置是英语还是法语,操作的输出始终相同。
服务确定
当服务的所有客户端都为人熟知,并且它们都同意使用固定区域设置(例如,美式英语)时,服务确定的场景非常有用。此特定区域设置通常是主机默认区域设置,或为此服务专门配置的区域设置。主机中运行的所有线程都有一个默认的 System.Globalization.CultureInfo
实例,您可以通过它获取正确的区域设置,并将其用于不同目的,如数字格式化、日历或从资源文件中检索特定消息。
您可以通过静态属性 CultureInfo.CurrentCulture
和 Thread.CurrentThread.CurrentCulture
获取该实例。坏消息是,每个线程有两个 CultureInfo
实例,即 UI 显示区域设置,您可以使用静态属性 CultureInfo.CurrentUICulture
和 Thread.CurrentThread.CurrentUICulture
读取。
在准备编写服务代码时,您可能会决定使用 CultureInfo.CurrentUICulture
,因为显示区域设置通常由 ResourceManager
类用于 WinForm 应用程序中的资源加载。
让我们看一个简单的“HelloWorld”场景的代码。
// Define a service contract.
[ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples")]
public interface IHelloWorld
{
[OperationContract]
string HelloWorld();
}
// Service class which implements the service contract.
public class HelloWorldService : IHelloWorld
{
public string HelloWorld()
{
return Messages.HelloWorld;
}
}
在上面的代码中,我为仅返回“Hello World”消息的服务定义了一个简单的协定和实现。此处看到的 Messages
类是 .NET 2.0 中的一项新功能,即类型化资源类。如果在应用程序中创建一个资源文件(例如,“Messages.resx”),则可以使用“resgen.exe”工具自动生成一个类型化类,该类具有资源文件中每个消息的一个属性。此外,此类型化类会自动加载与当前线程区域设置对应的资源文件,即您可以使用静态属性 CultureInfo.CurrentCulture
和 Thread.CurrentThread.CurrentCulture
获取的那个。这绝对是个好消息,因为您不再需要担心加载正确的资源文件并从中获取消息。应用程序中的“Messages.resx”文件如下所示:
名称 | 值 | 注释 |
HelloWorld | Hello World!!! |
因此,在执行服务后,客户端应用程序将始终收到相同的“Hello World!!!”消息,而不管它是否能够理解该消息。
客户端影响
这种情况可能是三者中最复杂的一种,因为客户端需要以某种方式传递预期的区域设置,而服务必须决定是否接受该区域设置。客户端和服务还需要就交换区域设置的格式和机制达成一致。有两种方法可以在客户端和服务之间交换国际化首选项:通过消息头的带外机制,或在消息正文中添加一个额外的参数。在我看来,第一种选择通常更可取,因为它不需要修改每个操作来包含额外的参数。由于 Web 服务国际化 (WS-I18N) 规范描述了一种实现此场景的机制,因此我选择使某些代码示例与该规范的最新草案保持一致。在接下来的段落中,我将讨论该规范的主要方面,然后重点介绍 WCF 的具体实现。
Web 服务国际化 (WS-I18N)
WS-I18N 提供了一种机制,用于将国际化首选项传递给通过 SOAP 调用的服务,并理解返回的任何消息的格式和语言。换句话说,此最新发布的规范的主要功能是通过客户端应用程序指定的某些文化首选项来影响服务。这些首选项包括:
- 区域设置或语言首选项
- 时区
- 可选信息
WS-I18N 元素
WS-I18N 规范的主要组件是“International
”元素。此元素是一个 SOAP 头,提供了一种机制,用于将国际化首选项和上下文信息附加到针对特定接收者或 SOAP 参与者的 SOAP 消息。因此,一个 SOAP 消息可能包含多个“International
”头,每个头都针对不同的接收者(不允许为同一参与者指定两个不同的“International
”头)。以下示例显示了一个简单的“International
”头:
<i18n:international S:mustUnderstand=”true” S:actor=”http://myorg.uri”>
<locale>en-US</locale>
<tz>GTM-0300</tz>
<preferences>
...
</preferences>
</i18n:international>
正如您在上面的示例中看到的,在“International
”头中指定了一些附加元素。让我们详细讨论所有这些元素:
Locale
:“locale
”元素表示请求的用户区域设置。此元素的内容必须是有效的语言标记(RFC3066bis)或“$default”或“$neutral”(与 .NET 中的 Invariant Culture 相同)之一。例如,“en-US”表示美式英语,或“es”表示西班牙语。Tz
:“tz
”元素表示客户端应用程序或请求者的时区。Preferences
:“preferences
”元素表示指定可选信息和国际化首选项的方式。
WCF 的 WS-I18N 实现
到目前为止,我们已经了解了三种不同的全球化模式,并简要概述了 WS-I18N 规范。现在,我们准备开始为 WCF 编写实现。
创建“International”头的协定定义
作为第一步,我们需要为“International
”头创建一个数据协定。WCF 中消息头的这数据协定定义遵循与普通消息相同的规则。在下面的代码中,我们将看到如何为所需的头创建数据协定。
[DataContract(Name=”International”,
Namespace=”http://www.w3.org/2005/09/ws-i18n”)]
public class International
{
private string locale;
private string tz;
private List<Preferences> preferences;
public International()
{
}
[DataMember(Name = “Locale”)]
public string Locale
{
get { return locale; }
set { locale = value; }
}
[DataMember(Name = “TZ”)]
public string Tz
{
get { return tz; }
set { tz = value; }
}
[DataMember(Name=”Preferences”)]
public List<Preferences> Preferences
{
get { return preferences; }
set { preferences = value; }
}
}
在此,我定义了一个类,其中包含头中每个元素的属性。由于区域设置和时区是简单类型,我们可以用 string
属性表示它们。但是,“Preferences
”元素根据定义,可以包含任何自定义信息或 XML 数据,因此我们需要为该元素创建一个特定的数据类型。
public class Preferences : IXmlSerializable
{
private XmlNode anyElement;
public XmlNode AnyElement
{
get { return anyElement; }
set { anyElement = value; }
}
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
XmlDocument document = new XmlDocument();
anyElement = document.ReadNode(reader);
}
public void WriteXml(XmlWriter writer)
{
anyElement.WriteTo(writer);
}
}
WCF 中数据协定的序列化器支持使用 IXmlSerializable
类型进行序列化,但不支持 XmlNode
类型。幸运的是,通过定义一个公开 XmlNode
属性并实现 IXmlSerializable
接口的类,我们可以克服这个障碍,这样我们就可以知道如何从 XML 流中读取和写入该 XML。
将“International”头添加到 SOAP 消息
一旦有了头定义,第二步就是将该头包含在从客户端应用程序到服务器的所有消息中。在 WCF 中,我们有两种方法可以执行此任务:我们可以显式定义服务的消息协定,然后将“International
”头包含在该协定中,或者我们可以使用某种扩展点在运行时自动将头添加到每条消息中。在我看来,第一种选择比第二种选择更加僵化,因为我们必须修改或创建每个操作的消息协定才能包含该头。另一方面,“International
”头在服务的 WSDL 定义中显式公开。我们将在以下段落中探讨这两种选择,以便您可以选择您认为更好的选项。
让我们先从显式版本开始,即在服务消息协定中包含“International
”头。
[MessageContract()]
public class HelloWorldRequest
{
[MessageHeader()]
public International International;
}
为了简化操作,我只包含了我们之前定义的代表“International
”头的类。此消息协定未在消息正文或头中指定任何附加参数。此服务的消息实现也相当简单;请看下面的代码:
// Service class which implements the service contract.
public class HelloWorldService : IHelloWorld
{
public HelloWorldResponse HelloWorldWithMessages(HelloWorldRequest request)
{
if (request.International != null)
{
Messages.Culture = new CultureInfo(request.International.Locale);
}
HelloWorldWithMessagesResponse response =
new HelloWorldWithMessagesResponse();
response.Result = Messages.HelloWorld;
return response;
}
}
首先,我们必须验证请求消息是否附带了“International
”头,否则,在我们使用该头时,将只收到一个糟糕的“NullArgument
”异常。一旦知道头存在,我们就可以使用它的某些属性,例如 Locale
或 Tz
,来影响服务中的区域设置。在此示例中,我只更改了类型化资源类的区域设置,因此它将尝试加载适当的资源文件并返回“HelloWorld”消息。注意:如果资源类找不到指定区域设置的文件,此代码可能会引发异常。
如前所述,使用 WCF 扩展点之一是将头包含到服务请求消息中的另一种方法。基本上,WCF 分为两个层:低级通道层允许您控制应用程序的消息传递方面,以及服务层,它使得无需处理实现中的低级方面即可构建高级应用程序。虽然我们可以开发扩展来将我们的代码插入这两个层,但在这种情况下,出于简单性的考虑,服务层通常是最佳选择。在服务层中,这可以通过消息检查器来完成。顾名思义,消息检查器是 WCF 提供的用于拦截和更改 SOAP 消息的机制。这正是我们想要为每条消息插入“International
”头所要做的。WCF 应用程序层基础结构提供两种类型的消息检查器:客户端消息检查器,在客户端运行;服务消息检查器,在服务端执行相同的操作。因此,您会发现两个不同的接口,IClientMessageInspector
用于客户端检查器,IDispatchMessageInspector
用于服务检查器。
public interface IClientMessageInspector
{
void AfterReceiveReply(ref Message reply, object correlationState);
object BeforeSendRequest(ref Message request, IClientChannel channel);
}
此接口上的方法名称说明了一切:BeforeSendRequest
方法在将请求消息发送到服务之前执行,而 AfterReceiveReply
方法在从服务接收响应消息之后执行。由于 WCF 在发送消息之前和接收消息之后可能会运行同一检查器的不同实例,为了在两个方法之间保持状态,提供了对象关联状态。IDispatchMessageInspector
接口非常相似,如下所示:
public interface IDispatchMessageInspector
{
object AfterReceiveRequest(ref Message request, IClientChannel channel,
InstanceContext instanceContext);
void BeforeSendReply(ref Message reply, object correlationState);
}
作为我们示例的一部分,我们将不得不实现这两个接口,一个客户端检查器在客户端添加自定义头,一个服务检查器在服务端删除它。
首先,我们需要有一个自定义消息检查器,我们将其命名为 InternationalizationMessageInspector
,并实现 IClientMessageInspector
和 IDispatchMessageInspector
接口。我们将提供一个构造函数来指定可以包含在头中的区域设置和时区。
public class InternationalizationMessageInspector :
IClientMessageInspector, IDispatchMessageInspector
{
public InternationalizationMessageInspector(string locale, string timeZone)
{
this.locale = locale;
this.timeZone = timeZone;
}
现在,让我们看看其余的实现。
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
International internationalHeader = new International();
if(!String.IsNullOrEmpty(locale))
internationalHeader.Locale = locale;
if (!String.IsNullOrEmpty(timeZone))
internationalHeader.Tz = timeZone;
MessageHeader header = MessageHeader.CreateHeader(
WSI18N.ElementNames.International,
WSI18N.NamespaceURI, internationalHeader);
request.Headers.Add(header);
return null;
}
BeforeSendRequest
的实现创建我们“International
”头类的实例,然后将其添加到方法中作为参数接收的消息的 Headers
集合中。另一方面,消息检查器应该从消息中获取头并处理它。
public object AfterReceiveRequest(ref Message request,
IClientChannel channel, InstanceContext instanceContext)
{
int index = request.Headers.FindHeader(
WSI18N.ElementNames.International, WSI18N.NamespaceURI);
request.Headers.UnderstoodHeaders.Add(request.Headers[index]);
return null;
}
在这种情况下,我只将头移到了“UnderstoodHeaders
”集合中。这会自动从 Headers
集合中删除该头,因此如果该头被标记为“MustUnderstand
”,WCF 不会抛出异常。无需对该头执行任何其他处理。最后,我们的服务实现必须检查“International
”头是否存在于“UnderstoodHeaders
”集合中,并在验证此条件后执行一些代码。我实现的用于在服务中查找头部的代码如下:
public International GetHeaderFromIncomeMessage()
{
MessageHeaders headers =
OperationContext.Current.IncomingMessageHeaders;
foreach (MessageHeaderInfo uheader in headers.UnderstoodHeaders)
{
if (uheader.Name == “International” && uheader.Namespace ==
“http://www.w3.org/2005/09/ws-i18n”)
{
International internationalHeader =
headers.GetHeader<International>(
uheader.Name, uheader.Namespace);
return internationalHeader;
}
}
return null;
}
然后,我使用辅助方法获取“International
”头,并使用其中指定的区域设置首选项设置资源管理器。
[Microsoft.ServiceModel.Samples.InternationalizationAttribute()]
public string HelloWorld()
{
International internationalHeader =
International.GetHeaderFromIncomeMessage();
if (internationalHeader != null)
{
Messages.Culture =
new CultureInfo(internationalHeader.Locale);
}
return Messages.HelloWorld;
}
此场景中的服务协定与我在“服务确定”模式中使用的协定完全相同,不需要任何消息协定或头定义。您可能已经注意到在操作签名上方定义了一个“InternationalizationAttribute
”。该属性是一个 IOperationBehavior
实现,需要在运行时将我们的消息检查器注入到服务的消息检查器中。IOperationBehavior
是 WCF 提供的另一个扩展点,它主要用于自定义通道的构建过程。
public class InternationalizationAttribute : Attribute, IOperationBehavior
{
private string locale;
private string timeZone;
public string Locale
{
get { return locale; }
set { locale = value; }
}
public string TimeZone
{
get { return timeZone; }
set { timeZone = value; }
}
public void ApplyClientBehavior(OperationDescription
operationDescription, ClientOperation clientOperation)
{
clientOperation.Parent.MessageInspectors.Add(new
InternationalizationMessageInspector(locale, timeZone));
}
public void ApplyDispatchBehavior(OperationDescription
operationDescription, DispatchOperation dispatchOperation)
{
dispatchOperation.Parent.MessageInspectors.Add(new
InternationalizationMessageInspector());
}
}
“ApplyClientBehavior
”和“ApplyDispatchBehavior
”方法会修改客户端和服务通道以包含我们的 MessageInspector 实现。
结论
在本文中,我们研究了三种不同的模式,可应用于 WCF 服务的 WCF 设计或开发。要了解哪种模式最适合您的服务,您需要知道该服务是否区域设置中性,以及客户端应用程序(服务使用者)的要求。我们还讨论了使用 WCF 提供的一些扩展点来实现 WS-I18N 规范的具体方法。
历史
使用了 WCF 九月 RC 版本的初始版本。