Async WebApi Thread.CurrentCulture
У меня есть самостоятельный хостинг OWIN размещенный веб-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);
}
Все работает хорошо, но проблема возникает, если я создаю свои методы контроллера async.
Когда я использую ожидание в этом методе, он может продолжаться в другом потоке, поэтому мои CurrentCulture и CurrentUICulture теряются.
Вот небольшой пример, который я использовал, чтобы найти эту проблему.
public async Task<HttpResponseMessage> PostData(MyData data)
{
Thread currentThread = Thread.CurrentThread;
await SomeThing();
if (Thread.CurrentThread.CurrentCulture != currentThread.CurrentCulture)
Debugger.Break();
}
Я не всегда ломаю строку Debugger.Break, но большую часть времени я делаю.
Вот пример, когда я действительно использую свой 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)
{
}
}
Некоторая дополнительная информация: у меня есть целая куча исключений, подобных этому, и все они попадают в пользовательский ExceptionFilterAttribute, который затем создает ответ.
Так что было бы очень много кода, чтобы всегда устанавливать культуру прямо перед тем, как я ее использую.
Ответы
Ответ 1
Как отметил Джо, культура передается HttpContext
в ASP.NET. Способ ASP.NET делает это, установив SynchronizationContext
при запуске запроса, и этот контекст также используется для возобновления асинхронных методов (по умолчанию).
Итак, есть несколько способов подойти к проблеме: вы можете написать свой собственный 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
более сложный, но как только он будет настроен, его будет проще использовать. Я не знаю хорошего примера ASP.NET-подобного контекста, но хорошей отправной точкой является моя статья MSDN.
Ответ 2
Из .NET 4.5, чтобы установить культуру по умолчанию для всех потоков, используйте:
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Ответ 3
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;