ASP.NET Web API provides very streamlined support in the framework to handle content negotiation. In HTTP specs, following Accept series of headers are used in a HTTP client to set in the request for content:
1 2 3 4 |
Accept Accept-Charset Accept-Encoding Accept-Language |
ASP.NET Web API supports first two headers out of the box. The framework itself would select the right MediaTypeFormatter class based upon the Accept header, and a selected MediaTypeFormatter is able to handle Accept-Charset to return the result in the right character set. For example, when a HTTP client sends
1 2 |
Accept: application/json Accept-Charset: UTF-8 |
The Web API framework will select JsonMediaTypeFormatter as the media type formatter for the response of this request. In turn JsonMediaTypeFormatter is able to take advantage of the backing library JSON.NET and returns the JSON serialization result in the right character set.
As stated in the link above, Accept-Language is not yet automatically supported in the framework. Web API provides several nice extension points to solve such a problem.
Checking the Pipeline
Picking an extension point of such a feature may not be the easiest thing to do. The Web API has the following extension points in the order of execution when the HTTP server receive a request:
- Global message handlers
- Per-route message handlers
- Authorization filters
- Action Filters
If we consider the importance of current thread’s Culture — when we choose to change the current thread’s culture, all subsequent calls to format dates, numbers, currencies and retrieving resources will return different results — I would like changing current thread’s culture to be as early as possible.
Here is a snapshot from the following chart:
The chart shows very clearly that the earliest extension point to an incoming request is the global message handler queue. We can certainly implement a custom message handler, insert it into this queue and ask Web API to use this custom message handler to process the HTTP header first.
Implementing the Message Handler
Here is the message handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class ThreadCultureMessageHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 1. prioritize languages based upon quality var langauges = new List<StringWithQualityHeaderValue>(); if (request.Headers.AcceptLanguage != null) { // then check the Accept-Language header. langauges.AddRange(request.Headers.AcceptLanguage); } // sort the languages with quality so we can check them in order. langauges = langauges.OrderByDescending(l => l.Quality).ToList(); CultureInfo culture = null; // 2. try to find one language that's available foreach (StringWithQualityHeaderValue lang in langauges) { try { culture = CultureInfo.GetCultureInfo(lang.Value); break; } catch (CultureNotFoundException) { // ignore the error } } // 3. if a language is available, set the thread culture if (culture != null) { Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; } return base.SendAsync(request, cancellationToken); } } |
As stated in the HTTP protocol, Accept-Language uses quality values to note the request’s preferences of languages. The first part of code above prioritizes languages based upon quality. The second part finds an available language. The third part sets the thread culture.
Now we have the message handler ready, we can insert this message handler into the pipeline:
1 2 3 4 5 6 |
public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); // insert the message handler into the pipeline, and make sure it is the first one. config.MessageHandlers.Insert(0, new ThreadCultureMessageHandler()); } |
This should be implemented in your WebApiConfig.Register() method, and get called when the Web API application starts.