42

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.

Daniel Häfele
  • 872
  • 2
  • 8
  • 13

3 Answers3

26

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Why not just store the culture in `HttpContext.Current.Items`? – John Saunders Dec 16 '13 at 02:00
  • @JohnSaunders: This is self-hosted, so there's no `HttpContext`. If this was on ASP.NET, it wouldn't be a problem in the first place because ASP.NET takes care of it by default. – Stephen Cleary Dec 16 '13 at 02:54
  • I wonder if this is also an issue in Web Api 2 or the .NET Async Managed Handlers? I did find that adding the httpRuntime targetFramework="4.5" to the web.config file causes Web Api 2 to run in non-quirks mode, so maybe that solves it just like asp.net. – Brain2000 Mar 30 '17 at 18:12
8

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

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • 1
    @SandRock. Please be more specific. It certainly helps me to set the culture in all threads of my application domain, and not to lose my CurrentUICulture if a new thread is spawned. – Mike Fuchs Jan 11 '16 at 09:23
  • 1
    This does not help in the multi-threaded context of the OP's web service. Your answer has nothing to do with the async-cross-thread issue. – SandRock Jan 11 '16 at 14:28
  • 1
    Well it works perfectly for me in async situations in which I call a WCF webservice where I want to preserve a culture, so still not sure what you are talking about. – Mike Fuchs Jan 12 '16 at 11:18
  • 9
    Setting the *default culture* for all thread fails in a multi-user environment where users may choose **different cultures**. – SandRock Jan 13 '16 at 10:04
  • _Now_ we are finally getting somewhere, being a bit more specific. The way I understood it, the code that resolves the multilingual error messages is on the client side, which means one user per appdomain. After reading the question and answers a couple times, I'm still not sure on which side the await call is made. So if you are sure, keep the downvote and I'll keep the answer here for all other people who actually encounter this problem on the client side, and can solve it this way. – Mike Fuchs Jan 14 '16 at 11:00
  • This worked with me in WebApi where I override `Initialize()` in my base controller – Korayem Jul 01 '16 at 18:47
  • But I am worried this won't work in case multiple users are accessing the website requesting different languages simultaneously. – Korayem Jul 01 '16 at 19:37
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;
Joe Enzminger
  • 11,110
  • 3
  • 50
  • 75
  • 3
    I forgot to mention that my **OWIN** service is self-hosted, so I don't have any **HttpContext**. However that would help either, because I use .NET Resource Files for multilingual error messages, and the designer created code relies on the **Thread.CurrentThread.CurrentCulture** / **Thread.CurrentThread.CurrentUICulture** to resolve the message in the correct language. – Daniel Häfele Dec 16 '13 at 00:38
  • @DanielHäfele What about OperationContext and int MessageProperties property? – Tony Dec 16 '13 at 00:54
  • I think that won't help either, if I want to access the language header I can just do that like `var language = this.ControllerContext.Request.Headers.AcceptLanguage.First().Value;` – Daniel Häfele Dec 16 '13 at 01:07
  • I'm afraid that you are going to have to build that functionality into your framework yourself. For instance, in your custom ExceptionFilterAttribute, you can set the Thread.CurrentCulture from the ControllerContext information before processing the exception. – Joe Enzminger Dec 16 '13 at 01:17
  • [this answer](http://stackoverflow.com/questions/20067053/async-call-to-the-wcf-service-doesnt-preserve-currentculture) to a similar question might shed some light on what you need to do. It also indicates I might be wrong about CurrentCulture not being synchronized when using the asp.net runtime. – Joe Enzminger Dec 16 '13 at 01:21