Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

Globalization Patterns in WCF (WS-I18N implementation)

Rate me:
Please Sign up or sign in to vote.
4.59/5 (20 votes)
28 Sep 2006CPOL12 min read 87.3K   725   33   11
Pablo Cibraro describes the internationalization of WCF services (Windows Communication Foundation) using the globalization support provided by the .NET framework.

Introduction

As business becomes more global every day, there is an emerging need to make applications and services multi-lingual and culturally aware. The .NET framework already provides comprehensive support for internationalization, but it is not always clear how to apply this support to the design of services. In this article, I will describe some of the available globalization patterns for web services, and how to successfully apply them using the extensibility points provided by WCF. In addition, I will align some code samples with the recently released WCF working draft for Web Services Internationalization (WS-I18N).

Globalization Patterns

There are at least three different globalization patterns that can be applied to a WCF service during the design or development phase. These patterns are mutually exclusive; they can not be used together, so it is essential to know their differences before starting work.

  • Locale Neutral: In this pattern, most aspects of the services are not locale-affected. This is the simplest case, and additional considerations are not required. For example, a “Calculator” service that performs arithmetic operations.
  • Service Determined: Here, the service always runs in a determined locale, which can be the host’s default locale, or a locale specifically configured for that service. For example, a web service that always returns messages in English.
  • Client Influenced: In this case, the service may run in a locale provided by the client application. As the “WS-I18N” states, the service is “influenced” because it can either consider the locale in which it is running or not depending on how the service is being implemented.

These patterns do not take into account situations in which different service implementations and data contracts are used to serve clients applications in a multilingual or cross-cultural setting.

Let’s look at each of these patterns in detail.

Locale Neutral

This is the simplest case, there is no need to consider or attend to any globalization feature.

C#
[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;
    }
}

The code above is quite straightforward. It only performs some simple arithmetic operations, but, as you can see, these operations are not influenced by any locale. The operation’s output will always be the same, whether the host’s locale is English or French.

Service Determined

Service determined scenarios are useful when all the clients for the service are well-known, and all of them agree on the use of a fixed locale, for instance, American English. This specific locale is usually the host’s default locale, or a locale specifically configured for the service. All the threads running in the host have a default System.Globalization.CultureInfo instance, where you can get the proper locale, and use it for different purposes, like number formatting, calendaring, or retrieving specific messages from a resource file.

You can get that instance through the static properties CultureInfo.CurrentCulture and Thread.CurrentThread.CurrentCulture. The bad news is that there is a second instance of CultureInfo per thread, namely the UI display culture, that you can read using the static properties CultureInfo.CurrentUICulture and Thread.CurrentThread.CurrentUICulture.

As you prepare to code a service, you will probably decide to use CultureInfo.CurrentUICulture because the display culture is typically used by the ResourceManager class for resource loading in WinForm applications.

Let’s take a look to some code for a simple “HelloWorld” scenario:

C#
// 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;
    }
}

In the code above, I defined a simple contract and implementation for a service that only returns a “Hello World” message. The Messages class seen here is a typed resource class, one of the new features in .NET 2.0. If you create a resource file in your application, “Messages.resx” for instance, you can use the tool “resgen.exe” to automatically generate a typed class, with a property for each message in the resource file. In addition, this typed class automatically loads the resource file corresponding to the current thread culture, i.e., the one you can get using the static properties CultureInfo.CurrentCulture and Thread.CurrentThread.CurrentCulture. This is definitely good news, since you no longer have to worry about loading the right resource file and getting the messages from it. The “Messages.resx” file in the application looks like this:

NameValueComments
HelloWorldHello World!!! 

As a result, after executing the service, the client application will always receive the same “Hello World!!!” message, whether it is able to understand that message or not.

Client Influenced

This scenario is perhaps the most complex of the three, because the client needs to pass the expected locale in some way, and the service must decide whether it accepts that locale or not. The client and service also need to agree on the format and mechanism to exchange that locale. There are two ways to exchange the international preferences between the client and the service: an out-of-band mechanism through a message header, or an additional argument in the message body. In my opinion, the first option is usually preferable because it does not imply modifying each operation to include an additional argument. Since the Web Services Internationalization (WS-I18N) specification describes a mechanism to implement this scenario, I have chosen to align some code samples with the latest draft of this specification. In the following paragraphs, I will discuss the main aspects of this specification, and will then focus on a concrete implementation for WCF.

Web Services Internationalization (WS-I18N)

WS-I18N provides a mechanism to pass international preferences to a service invoked through SOAP, and to understand the format and language of any message returned. In other words, the main feature of this recently released specification is to influence the service with some cultural preferences specified by the client application. These preferences include:

  • Locale or language preference
  • Time zone
  • Optional information

WS-I18N Elements

The main component of the WS-I18N specification is the “International” element. This element is a SOAP header that provides a mechanism for attaching international preferences and contextual information to a SOAP message targeted towards a specific receiver or SOAP actor. Thus, a SOAP message may have multiple “International” headers, each one for a different receiver (it is not possible to specify two different “International” headers for the same actor). The following sample shows a simple “International” header:

XML
<i18n:international S:mustUnderstand="true" S:actor="http://myorg.uri">
    <locale>en-US</locale>
    <tz>GTM-0300</tz>
    <preferences>
        ...
    </preferences>
</i18n:international>

As you can se in the above sample, some additional elements are specified in the “International” header. Let’s discuss all of them in detail:

  • Locale: The “locale” element represents the requested user locale. The content of this element must be either a valid language tag (RFC3066bis) or one of the values “$default” or “$neutral” (same as Invariant Culture in .NET). For example, “en-US” for American English, or “es” for Spanish.
  • Tz: The “tz” element represents the time zone of the client application or requester.
  • Preferences: The “preferences” element represents a way to specify optional information and international preferences.

WS-I18N Implementation for WCF

So far, we have looked at three different globalization patterns, and have gone through a brief overview of the WS-I18N specification. We are now ready to start coding our implementation for WCF.

Creating the Contract Definition for the “International” Header

As the first step, we need to create a Data Contract for the “International” header. The data contract definition for a message header in WCF follows the same rules as that for normal messages. In the following code, we will see how to create the data contract for the desired header:

C#
[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; }
    }
}

Here, I have defined a class with a property for each element in the header. Since locale and time zone are simple types, we can represent them with string properties. However, the “Preferences” element, by definition, can contain any custom information or XML data, so, we need to create a specific data type for that element.

C#
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);
    }
}

The serializer for data contracts in WCF supports serialization with IXmlSerializable types, but not with an XmlNode type. Fortunately, we can overcome this obstacle by defining a class that exposes an XmlNode property and implements the IXmlSerializable interface, so we know how to write and read that XML from an XML stream.

Adding the “International” Header to the SOAP Messages

Once we have the header definition, as second step, we need to include that header in every message that goes from the client application to the server. In WCF, we have two ways to perform this task: we can explicitly define a message contract for the service and then, include the “International” header in that contract, or we can use some extensibility point to automatically add the header in every message at runtime. In my opinion, the first option is quite more rigid than the second one, since we have to modify or create a message contract for each operation in order to include the header. On other hand, the “International” header is explicitly exposed in the WSDL definition for the service. We will explore both alternatives in the following paragraphs, so you can choose the one you consider better.

Let’s start working first on the explicit version, that is, including the “International” header in a service message contract.

C#
[MessageContract()]
public class HelloWorldRequest
{
    [MessageHeader()] 
    public International International; 
}

To make things simpler, I only included the class representing the “International” header that we previously defined. This message contract does not specify any additional parameter in the message body or header. The message implementation for this service is also quite simple; just take a look at the code below:

C#
// 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;
    }
}

First of all, we have to verify that the “International” header came with the request message, otherwise, we will only receive an ugly “NullArgument” exception at the moment we use the header. Once we know that the header is there, we can use some of its properties, such as the Locale or Tz, to influence the locale settings in the service. In this sample, I am only changing the locale for the typed resource class, so it will try to load the proper resource file and return a “HelloWorld” message. Note: This code can generate an exception if the resource class is not able to find the file for the specified locale.

As I said before, using one of the WCF extensibility points is another way to include the header in the request messages to the service. Basically, WCF is divided into two layers, the lower-level channel layer that permits you to gain control over the messaging aspect of the applications, and the service layer that makes possible to build high-level applications without the need to deal with lower level aspects of the implementation. While we can develop extensions to plug our code in both layers, in this case, the service layer is usually the best option for simplicity sake. In the service layer, this can be done by means of Message Inspectors. As its name says, a Message Inspector is the mechanism provided by WCF to intercept and change SOAP messages. This is what we want to do in order to insert the “International” header in each message. The WCF application layer infrastructure provides two kinds of message inspectors: client message inspectors, which run on the client side, and service message inspectors, which do the same on the service side. For this reason, you will find two different interfaces, IClientMessageInspector for client inspectors, and IDispatchMessageInspector for service inspectors.

C#
public interface IClientMessageInspector
{
    void AfterReceiveReply(ref Message reply, object correlationState);
    object BeforeSendRequest(ref Message request, IClientChannel channel);
}

The method names on this interface say everything: the BeforeSendRequest method is executed just before sending a request message to the service, and the AfterReceiveReply, just after receiving the response message from the service. Since WCF may run different instances of the same inspector before sending the message and after receiving it, in order to keep a state between both methods, the object correlation state is provided. The IDispatchMessageInspector interface is quite similar, and looks as follows:

C#
public interface IDispatchMessageInspector
{
    object AfterReceiveRequest(ref Message request, IClientChannel channel, 
                               InstanceContext instanceContext);
    void BeforeSendReply(ref Message reply, object correlationState);
}

As part of our sample, we will have to implement both interfaces, a client inspector to add the custom header on the client side, and the service inspector to remove it on the service side.

First, we need to have our custom message inspector, which we will name InternationalizationMessageInspector, implementing the IClientMessageInspector and IDispatchMessageInspector interfaces. We will provide a constructor to specify the locale and time zone that can be included in the header.

C#
public class InternationalizationMessageInspector : 
             IClientMessageInspector, IDispatchMessageInspector
{
    public InternationalizationMessageInspector(string locale, string timeZone)
    {
        this.locale = locale;
        this.timeZone = timeZone;
    }

Now, let’s turn to the rest of the implementation.

C#
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;
}

The BeforeSendRequest implementation creates an instance of our “International” header class, and afterwards, it adds that instance to the Headers collection in the message received as a parameter in the method. On the other side, the message inspector should get the header from the message and process it.

C#
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;
}

In this case, I only moved the header to the “UnderstoodHeaders” collection. This implicitly removes the header from the Headers collection, so if the header was marked as “MustUnderstand”, WCF does not throw an exception. There is no need to perform any additional process on the header. Finally, our service implementation must check whether the “International” header is in the “UnderstoodHeaders” collection or not, and execute some code after verifying that condition. The code I implemented to find the header in the service looks as follows:

C#
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;
}

Then, I used the helper method to get the “International” header and set the resource manager with the locale preferences specified there.

C#
[Microsoft.ServiceModel.Samples.InternationalizationAttribute()]
public string HelloWorld()
{
    International internationalHeader = 
       International.GetHeaderFromIncomeMessage();

    if (internationalHeader != null)
    {
        Messages.Culture = 
           new CultureInfo(internationalHeader.Locale);
    }
            
    return Messages.HelloWorld;
}

The service contract in this scenario is exactly the same as the one I used in the “Service Determined” pattern, no message contract or header definition is required. You probably noticed the existence of a “InternationalizationAttribute” definition just above the operation signature. That attribute is a IOperationBehavior implementation required to inject our message inspector at runtime in the message inspectors for the service. A IOperationBehavior is another extensibility point provided by WCF, it can be mainly used to customize the process of building a channel.

C#
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()); 
    }
}

The “ApplyClientBehavior” and “ApplyDispatchBehavior” methods modify the client and service channels to include our MessageInspector implementation.

Conclusion

In this article, we saw three different patterns that can be applied in the design or development of a WCF service. In order to know which of these patterns is best for your service, you need to know whether the service is locale neutral or not, and the requirements for the client applications (the service consumers). We also discussed a concrete implementation of the WS-I18N specification using some of the extensibility points provided by WCF.

History

Uses the initial version for the WCF September RC release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Pablo Cibraro is an independent consultant and expert on Microsoft Technologies.
He has over eight years of experience designing and developing software solutions for a broad range of corporate clients, principally in Argentina. He was recently awarded as MVP in the “Windows Server System – Connected System Developer” category.
Pablo can be reached via his weblog at http://weblogs.asp.net/cibrax

Comments and Discussions

 
GeneralMy vote of 4 Pin
Member 963511815-Jul-13 9:18
Member 963511815-Jul-13 9:18 
QuestionSample project does not build Pin
bilal07417-May-12 4:42
bilal07417-May-12 4:42 
GeneralSilverlight Pin
valeriob10-Feb-11 9:32
valeriob10-Feb-11 9:32 
GeneralI just reference you! Pin
Vincent-Philippe Lauzon31-Dec-10 4:44
Vincent-Philippe Lauzon31-Dec-10 4:44 
Generalplease help me how to do send locale from client to WCF service Pin
loverboyibm17-Dec-10 1:48
loverboyibm17-Dec-10 1:48 
GeneralMy vote of 4 Pin
loverboyibm15-Dec-10 20:06
loverboyibm15-Dec-10 20:06 
Generalneed clarification on source project attached Pin
loverboyibm15-Dec-10 19:25
loverboyibm15-Dec-10 19:25 
Generalstorage of the messages.... Pin
Michael Eber16-Nov-10 8:20
Michael Eber16-Nov-10 8:20 
QuestionArithmetic isn't &quot;influenced by any locale&quot;? Pin
BCdotNET4-Sep-07 1:10
BCdotNET4-Sep-07 1:10 
GeneralThanks Pin
alexandre.abric23-Feb-07 2:59
alexandre.abric23-Feb-07 2:59 
GeneralNeutral cultures Pin
Peter Ritchie28-Sep-06 8:10
Peter Ritchie28-Sep-06 8:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.