Making your WCF Web Apis to speak in multiple languages

One of the key aspects of how the web works today is content negotiation. The idea of content negotiation is based on the fact that a single resource can have multiple representations, so user agents (or clients) and servers can work together to chose one of them.

The http specification defines several “Accept” headers that a client can use to negotiate content with a server, and among all those, there is one for restricting the set of natural languages that are preferred as a response to a request, “Accept-Language”. For example, a client can specify “es” in this header for specifying that he prefers to receive the content in spanish or “en” in english.

However, there are certain scenarios where the “Accept-Language” header is just not enough, and you might want to have a way to pass the “accepted” language as part of the resource url as an extension. For example, http://localhost/ProductCatalog/Products/1.es” returns all the descriptions for the product with id “1” in spanish. This is useful for scenarios in which you want to embed the link somewhere, such a document, an email or a page. 

Supporting both scenarios, the header and the url extension, is really simple in the new WCF programming model. You only need to provide a processor implementation for any of them.

Let’s say I have a resource implementation as part of a product catalog I want to expose with the WCF web apis.

[ServiceContract]
[Export]
public class ProductResource
{
    IProductRepository repository;
 
    [ImportingConstructor]
    public ProductResource(IProductRepository repository)
    {
        this.repository = repository;
    }
 
    [WebGet(UriTemplate = "{id}")]
    public Product Get(string id, HttpResponseMessage response)
    {
        var product = repository.GetById(int.Parse(id));
        if (product == null)
        {
            response.StatusCode = HttpStatusCode.NotFound;
            response.Content = new StringContent(Messages.OrderNotFound);
        }
 
        return product;
    }
}

The Get method implementation in this resource assumes the desired culture will be attached to the current thread (Thread.CurrentThread.Culture). Another option is to pass the desired culture as an additional argument in the method, so my processor implementation will handle both options. This method is also using an auto-generated class for handling string resources, Messages, which is available in the different cultures that the service implementation supports. For example,

Messages.resx contains “OrderNotFound”: “Order Not Found”

Messages.es.resx contains “OrderNotFound”: “No se encontro orden”

The processor implementation bellow tackles the first scenario, in which the desired language is passed as part of the “Accept-Language” header.

public class CultureProcessor : Processor<HttpRequestMessage, CultureInfo>
{
    string defaultLanguage = null;
 
    public CultureProcessor(string defaultLanguage = "en")
    {
        this.defaultLanguage = defaultLanguage;
        
        this.InArguments[0].Name = HttpPipelineFormatter.ArgumentHttpRequestMessage;
        this.OutArguments[0].Name = "culture";
    }
 
    public override ProcessorResult<CultureInfo> OnExecute(HttpRequestMessage request)
    {
        CultureInfo culture = null;
                    
        if (request.Headers.AcceptLanguage.Count > 0)
        {
            var language = request.Headers.AcceptLanguage.First().Value;
            culture = new CultureInfo(language);
        }
        else
        {
            culture = new CultureInfo(defaultLanguage);
        }
 
        Thread.CurrentThread.CurrentCulture = culture;
        Messages.Culture = culture;
 
        return new ProcessorResult<CultureInfo>
        {
            Output = culture
        };
    }
}
 
As you can see, the processor initializes a new CultureInfo instance with the value provided in the “Accept-Language” header, and set that instance to the current thread and the auto-generated resource class with all the messages. In addition, the CultureInfo instance is returned as an output argument called “culture”, making possible to receive that argument in any method implementation
 
The following code shows the implementation of the processor for handling languages as url extensions.
 
public class CultureExtensionProcessor : Processor<HttpRequestMessage, Uri>
{
    public CultureExtensionProcessor()
    {
        this.OutArguments[0].Name = HttpPipelineFormatter.ArgumentUri;
    }
 
    public override ProcessorResult<Uri> OnExecute(HttpRequestMessage httpRequestMessage)
    {
        var requestUri = httpRequestMessage.RequestUri.OriginalString;
 
        var extensionPosition = requestUri.LastIndexOf(".");
 
        if (extensionPosition > -1)
        {
            var extension = requestUri.Substring(extensionPosition + 1);
 
            var query = httpRequestMessage.RequestUri.Query;
 
            requestUri = string.Format("{0}?{1}", requestUri.Substring(0, extensionPosition), query); ;
 
            var uri = new Uri(requestUri);
 
            httpRequestMessage.Headers.AcceptLanguage.Clear();
 
            httpRequestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(extension));
 
            var result = new ProcessorResult<Uri>();
 
            result.Output = uri;
 
            return result;
        }
 
        return new ProcessorResult<Uri>();
    }
}

The last step is to inject both processors as part of the service configuration as it is shown bellow,

public void RegisterRequestProcessorsForOperation(HttpOperationDescription operation, IList<Processor> processors, MediaTypeProcessorMode mode)
{
    processors.Insert(0, new CultureExtensionProcessor());
    processors.Add(new CultureProcessor());
}

Once you configured the two processors in the pipeline, your service will start speaking different languages :).

Note: Url extensions don’t seem to be working in the current bits when you are using Url extensions in a base address. As far as I could see, ASP.NET intercepts the request first and tries to route the request to a registered ASP.NET Http Handler with that extension. For example, “http://localhost/ProductCatalog/products.es” does not work, but “http://localhost/ProductCatalog/products/1.es” does.

No Comments