Async WebApi Thread.CurrentCulture

14,233

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;
Share:
14,233
Daniel Häfele
Author by

Daniel Häfele

Updated on July 21, 2022

Comments

  • Daniel Häfele
    Daniel Häfele almost 2 years

    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
    Daniel Häfele over 10 years
    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.
  • Tony
    Tony over 10 years
    @DanielHäfele What about OperationContext and int MessageProperties property?
  • Daniel Häfele
    Daniel Häfele over 10 years
    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;
  • Joe Enzminger
    Joe Enzminger over 10 years
    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
    Joe Enzminger over 10 years
    this answer 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.
  • John Saunders
    John Saunders over 10 years
    Why not just store the culture in HttpContext.Current.Items?
  • Stephen Cleary
    Stephen Cleary over 10 years
    @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.
  • Mike Fuchs
    Mike Fuchs over 8 years
    @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.
  • SandRock
    SandRock over 8 years
    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.
  • Mike Fuchs
    Mike Fuchs over 8 years
    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.
  • SandRock
    SandRock over 8 years
    Setting the default culture for all thread fails in a multi-user environment where users may choose different cultures.
  • Mike Fuchs
    Mike Fuchs over 8 years
    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.
  • IDIR Samir
    IDIR Samir almost 8 years
    This worked with me in WebApi where I override Initialize() in my base controller
  • IDIR Samir
    IDIR Samir almost 8 years
    But I am worried this won't work in case multiple users are accessing the website requesting different languages simultaneously.
  • Brain2000
    Brain2000 about 7 years
    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.