web-dev-qa-db-fra.com

Async WebApi Thread.CurrentCulture

J'ai un projet auto-hébergé [~ # ~] owin [~ # ~] hébergé API Web fournissant des méthodes de base REST pour moi .

Je veux avoir des messages d'erreur multilingues, donc j'utilise des fichiers Resource et un BaseController qui définit le Thread.CurrentCulture et Thread.CurrentUICulture à l'en-tête Accept-Language de la demande.

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);
}

Tout cela fonctionne bien, mais le problème apparaît si je fais asynchroniser mes méthodes de contrôleur.

Lorsque j'utilise wait dans la méthode, cela peut continuer dans un autre thread, et donc mes CurrentCulture et CurrentUICulture sont perdus .

Voici un petit exemple que j'ai utilisé pour trouver ce problème.

public async Task<HttpResponseMessage> PostData(MyData data)
{
    Thread currentThread = Thread.CurrentThread;

    await SomeThing();

    if (Thread.CurrentThread.CurrentCulture != currentThread.CurrentCulture)
        Debugger.Break();
}

Je ne casse pas toujours la ligne Debugger.Break, mais la plupart du temps je le fais.

Voici un exemple où j'utilise réellement mon Fichier de ressources.

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) 
    {

    }
}

Quelques informations supplémentaires: J'ai tout un tas d'exceptions comme celle-ci, et elles sont toutes prises dans une exception ExceptionFilterAttribute personnalisée qui crée ensuite la réponse.

Il serait donc beaucoup de code de toujours définir la culture juste avant de l'utiliser.

40
Daniel Häfele

Comme Joe l'a souligné, la culture est transférée par le HttpContext dans ASP.NET. La façon dont ASP.NET procède est d'installer un SynchronizationContext lorsqu'une demande démarre, et ce contexte est également utilisé pour reprendre les méthodes asynchrones (par défaut).

Il existe donc deux façons d'aborder le problème: vous pouvez soit écrire votre propre SynchronizationContext qui préservera la culture par défaut, soit vous pouvez explicitement préserver la culture sur chaque await.

Pour préserver la culture à chaque await, vous pouvez utiliser le code de 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(); 
    } 
}

L'approche SynchronizationContext est plus compliquée mais une fois configurée, elle sera plus facile à utiliser. Je ne connais pas un bon exemple de contexte de type ASP.NET, mais un bon point de départ est mon article MSDN .

25
Stephen Cleary

À partir de .NET 4.5, pour définir une culture par défaut pour tous les threads, utilisez:

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
7
Mike Fuchs

Thread.CurrentCulture n'est pas synchronisé entre les threads. Cependant, votre HttpContext le fait. Vous feriez mieux d'obtenir vos informations culturelles directement depuis votre HttpContext. Vous pouvez faire quelque chose comme

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); 
}

puis, dans n'importe quelle tâche, vous avez besoin de la culture:

var culture = HttpContext.Current != null ? HttpContext.Current.Items["Culture"] as CultureInfo : Thread.CurrentThread.CurrentCulture;
3
Joe Enzminger