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;