I have a self-hosted OWIN hosted Web API project providing some basic REST methods for me.

I want to have multilingual error messages, so I use Resource files and a BaseController that sets the Thread.CurrentCulture and Thread.CurrentUICulture to the Accept-Language header of the request.

public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
{
    if (controllerContext.Request.Headers.AcceptLanguage != null && 
        controllerContext.Request.Headers.AcceptLanguage.Count > 0)
    {
        string language = controllerContext.Request.Headers.AcceptLanguage.First().Value;
        var culture = CultureInfo.CreateSpecificCulture(language);

        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
    }

    base.ExecuteAsync(controllerContext, cancellationToken);
}

That all works nice, but the problem appears if I make my controller methods async.

When I use await in the method, it might continue in another thread, and so my CurrentCulture and CurrentUICulture are lost.

Here's an little example I used to find this issue.

public async Task<HttpResponseMessage> PostData(MyData data)
{
    Thread currentThread = Thread.CurrentThread;

    await SomeThing();

    if (Thread.CurrentThread.CurrentCulture != currentThread.CurrentCulture)
        Debugger.Break();
}

I don't always break in the Debugger.Break line, but most of the time I do.

Here's an example where I actually use my Resource File.

public async Task<HttpResponseMessage> PostMyData(MyData data)
{
    //Before this if I'm in the correct thread and have the correct cultures
    if (await this._myDataValidator.Validate(data) == false)
        //However, I might be in another thread here, so I have the wrong cultures
        throw new InvalidMyDataException(); 
}

public class InvalidMyDataException : Exception
{
    public InvalidMyDataException()
        //Here I access my resource file and want to get the error message depending on the current culture, which might be wrong
        : base(ExceptionMessages.InvalidMyData) 
    {

    }
}

Some additional information: I have a whole bunch of exceptions like this, and they all get caught in an custom ExceptionFilterAttribute which then creates the response.

So it would be much code to always set the culture right before I use it.


Solution 1:

As Joe pointed out, culture is transferred by the HttpContext in ASP.NET. The way ASP.NET does this is by installing a SynchronizationContext when a request starts, and that context is also used to resume asynchronous methods (by default).

So, there are a couple of ways to approach the problem: you can either write your own SynchronizationContext that will preserve culture by default, or you can explicitly preserve the culture across each await.

To preserve the culture at each await, you can use code from Stephen Toub:

public static CultureAwaiter WithCulture(this Task task) 
{ 
    return new CultureAwaiter(task); 
}

public class CultureAwaiter : INotifyCompletion
{ 
    private readonly TaskAwaiter m_awaiter; 
    private CultureInfo m_culture;

    public CultureAwaiter(Task task) 
    { 
        if (task == null) throw new ArgumentNullException("task"); 
        m_awaiter = task.GetAwaiter(); 
    }

    public CultureAwaiter GetAwaiter() { return this; }

    public bool IsCompleted { get { return m_awaiter.IsCompleted; } }

    public void OnCompleted(Action continuation) 
    { 
        m_culture = Thread.CurrentThread.CurentCulture; 
        m_awaiter.OnCompleted(continuation); 
    }

    public void GetResult() 
    { 
        Thread.CurrentThread.CurrentCulture = m_culture; 
        m_awaiter.GetResult(); 
    } 
}

The SynchronizationContext approach is more complicated but once it's set up, it will be easier to use. I don't know of a good example of an ASP.NET-like context, but a good starting point is my MSDN article.

Solution 2:

From .NET 4.5, to set a default culture for all threads, use:

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

Solution 3:

Thread.CurrentCulture doesn't get synchronized across threads. However, your HttpContext does. You would be better off getting your culture information from your HttpContext directly. You can do something like

public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
{
    if (controllerContext.Request.Headers.AcceptLanguage != null && 
        controllerContext.Request.Headers.AcceptLanguage.Count > 0)
    {
        string language = controllerContext.Request.Headers.AcceptLanguage.First().Value;
        var culture = CultureInfo.CreateSpecificCulture(language);
        HttpContext.Current.Items["Culture"] = culture;
        //Thread.CurrentThread.CurrentCulture = culture;
        //Thread.CurrentThread.CurrentUICulture = culture;
    }

    base.ExecuteAsync(controllerContext, cancellationToken); 
}

and then, in any task you need the culture:

var culture = HttpContext.Current != null ? HttpContext.Current.Items["Culture"] as CultureInfo : Thread.CurrentThread.CurrentCulture;