Async WebApi Thread.CurrentCulture
我有一个自我托pipe的OWIN托pipe的Web API项目,为我提供了一些基本的REST方法。
我想要多语言的错误消息,所以我使用资源文件和一个BaseController将Thread.CurrentCulture和Thread.CurrentUICulture设置为请求的Accept-Language标头。
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); }
这一切都很好,但问题出现,如果我使我的控制器方法asynchronous 。
当我在方法中使用await时,它可能会在另一个线程中继续,所以CurrentCulture和CurrentUICulture将会丢失。
这是我用来find这个问题的一个小例子。
public async Task<HttpResponseMessage> PostData(MyData data) { Thread currentThread = Thread.CurrentThread; await SomeThing(); if (Thread.CurrentThread.CurrentCulture != currentThread.CurrentCulture) Debugger.Break(); }
我并不总是在Debugger.Break中断,但大部分时间我都这么做。
这是一个我真正使用我的资源文件的例子。
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) { } }
一些额外的信息:我有一大堆这样的exception,他们都陷入了一个自定义的ExceptionFilterAttribute然后创build响应。
所以在我使用它之前,总是要设置很多代码。
正如Joe指出的,文化是通过ASP.NET中的HttpContext
传递的。 ASP.NET执行此操作的方式是在请求启动时安装SynchronizationContext
,并且该上下文也用于恢复asynchronous方法(默认情况下)。
因此,有两种方法可以解决这个问题:您可以编写自己的SynchronizationContext
,默认情况下可以保留文化,也可以在每个await
显式保留文化。
为了在每一个await
保存文化,你可以使用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(); } }
SynchronizationContext
方法比较复杂,但一旦build立,使用起来就会简单一些。 我不知道类似ASP.NET的上下文的一个很好的例子,但是一个好的起点是我的MSDN文章 。
从.NET 4.5开始,为所有线程设置默认文化,使用:
CultureInfo.DefaultThreadCurrentCulture = culture; CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentCulture不会跨线程同步。 但是,你的HttpContext呢。 你最好直接从你的HttpContext获取文化信息。 你可以做类似的事情
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); }
然后,在任何你需要的文化任务:
var culture = HttpContext.Current != null ? HttpContext.Current.Items["Culture"] as CultureInfo : Thread.CurrentThread.CurrentCulture;