Async WebApi Thread.CurrentCulture
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;
Daniel Häfele
Updated on July 21, 2022Comments
-
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 over 10 yearsI 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 over 10 years@DanielHäfele What about OperationContext and int MessageProperties property?
-
Daniel Häfele over 10 yearsI 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 over 10 yearsI'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 over 10 yearsthis 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 over 10 yearsWhy not just store the culture in
HttpContext.Current.Items
? -
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 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 over 8 yearsThis 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 over 8 yearsWell 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 over 8 yearsSetting the default culture for all thread fails in a multi-user environment where users may choose different cultures.
-
Mike Fuchs over 8 yearsNow 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 almost 8 yearsThis worked with me in WebApi where I override
Initialize()
in my base controller -
IDIR Samir almost 8 yearsBut I am worried this won't work in case multiple users are accessing the website requesting different languages simultaneously.
-
Brain2000 about 7 yearsI 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.