Keep CurrentCulture in async/await
Asked Answered
E

3

33

I have following pseudo-code

string GetData()
{
  var steps = new List<Task<string>>
  {
    DoSomeStep(),
    DoSomeStep2()
  };

  await Task.WhenAll(steps);

  return SomeResourceManagerProxy.RetrieveValuesForLocalizedStrings( steps.Select(s => s.Result) );

}

This method is called from WebService, where I set Thread.CurrentUICulture according to user's browser settings.

After await, the CurrentUICulture is lost (I run on different thread).

I have solved the issue with following:

    public class MyAwaiter<T> : INotifyCompletion
    {
        private TaskAwaiter<T> waiter;
        private CultureInfo culture;

        public MyAwaiter(TaskAwaiter<T> waiter)
        {
            this.waiter = waiter;
        }

        public PreserveCultureAwaiter<T> GetAwaiter() { return this; }

        public bool IsCompleted { get { return waiter.IsCompleted; } }

        public void OnCompleted(Action continuation)
        {
            culture = Thread.CurrentThread.CurrentUICulture;
            waiter.OnCompleted(continuation);
        }

        public T GetResult()
        {
            Thread.CurrentThread.CurrentUICulture = culture;
            return waiter.GetResult();
        }
    }

    public static MyAwaiter<T> KeepCulture<T>(this Task<T> task)
    {
        return new MyAwaiter<T>(task.GetAwaiter());
    }

...

    await Task.WhenAll(steps).KeepCulture();

This has one drawback - need for remembering to call KeepCulture() on every task that is being awaited. (I have also some extension method to keep the UI culture in task).

Is there any easier way how to preserve UI culture?

Elbrus answered 5/6, 2015 at 9:11 Comment(4)
one note: I'm writing framework for localization of stuff that was already written in async/await stuff. I'd like everything to be as much transparent to other developers as possibleElbrus
I have seen a similar solution in one of the .NET repositories a while ago. It might have been EF. I think they deleted it for some reason so you need to scan the logs.Thaler
Perhaps time to close this Q+A, the problem was fixed.Buffer
This does not work. OnCompleted is never called.Cooperation
B
47

Culture does not flow in the .NET Framework, a very notorious problem. It is very hard to solve on Windows, culture is an unmanaged property of a thread so the CLR can't ensure it is always set correctly. That makes tinkering with the CurrentCulture on the main thread a big fat mistake. The bugs you get are very hard to diagnose. Like a SortedList you create on one thread that suddenly isn't sorted anymore on another. Yuck.

Microsoft did something about it in .NET 4.5, they added the CultureInfo.DefaultThreadCurrentCulture property. Also DefaultThreadCurrentUICulture. That still does not guarantee it will be set correctly, unmanaged code you call can change it and the CLR cannot do anything about it. In other words, a bug will be much harder to diagnose. But at least you have some idea when it might change.


UPDATE: this problem was fixed thoroughly in .NET 4.6, culture now flows from one thread to another and the CultureInfo.DefaultThreadCurrentCulture hack is not longer necessary nor useful. Documented in the MSDN article for CultureInfo.CurrentCulture. Details as written right now do not appear to be entirely correct, it always flowed when I tested it and DefaultThreadCurrentCulture appear to play no role at all anymore.

Buffer answered 5/6, 2015 at 10:34 Comment(3)
I understand your advice, but unfortunately not doing that is something I cannot do at this momentElbrus
I think this should be marked as answer. To me this seems to be a suitable solution in Framework 4.5. The argument that an unmanaged call can change the culture is not relevant. The default can as well be overridden in a managed call. Or i don't understand it deeply.(?) The question maybe is why should any unmanaged code change the culture. I think normally you define the culture for all the threads in your process.Stringency
I am using NET Framework 4.8 and the CultureInfo.DefaultThreadCurrentCulture property resolves my problem on new threads.Phellem
E
5

So far I've created my own SynchronizationContext, which I've tested with both ASP.NET and console applications, and in both it keeps the culture as I want it:

/// <summary>
/// Class that captures current thread's culture, and is able to reapply it to different one
/// </summary>
internal sealed class ThreadCultureHolder
{
    private readonly CultureInfo threadCulture;
    private readonly CultureInfo threadUiCulture;

    /// <summary>
    /// Captures culture from currently running thread
    /// </summary>
    public ThreadCultureHolder()
    {
        threadCulture = Thread.CurrentThread.CurrentCulture;
        threadUiCulture = Thread.CurrentThread.CurrentUICulture;
    }

    /// <summary>
    /// Applies stored thread culture to current thread
    /// </summary>
    public void ApplyCulture()
    {
        Thread.CurrentThread.CurrentCulture = threadCulture;
        Thread.CurrentThread.CurrentUICulture = threadUiCulture;
    }

    public override string ToString()
    {
        return string.Format("{0}, UI: {1}", threadCulture.Name, threadUiCulture.Name);
    }
}

/// <summary>
/// SynchronizationContext that passes around current thread's culture
/// </summary>
internal class CultureAwareSynchronizationContext : SynchronizationContext
{
    private readonly ThreadCultureHolder cultureHolder;
    private readonly SynchronizationContext synchronizationImplementation;

    /// <summary>
    /// Creates default SynchronizationContext, using current(previous) SynchronizationContext 
    /// and captures culture information from currently running thread
    /// </summary>
    public CultureAwareSynchronizationContext()
        : this(Current)
    {}

    /// <summary>
    /// Uses passed SynchronizationContext (or null, in that case creates new empty SynchronizationContext) 
    /// and captures culture information from currently running thread
    /// </summary>
    /// <param name="previous"></param>
    public CultureAwareSynchronizationContext(SynchronizationContext previous)
        : this(new ThreadCultureHolder(), previous)
    {
    }

    internal CultureAwareSynchronizationContext(ThreadCultureHolder currentCultureHolder, SynchronizationContext currentSynchronizationContext)
    {
        cultureHolder = currentCultureHolder;
        synchronizationImplementation = currentSynchronizationContext ?? new SynchronizationContext();
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        cultureHolder.ApplyCulture();
        synchronizationImplementation.Send(d, state);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        synchronizationImplementation.Post(passedState =>
        {
            SetSynchronizationContext(this);
            cultureHolder.ApplyCulture();
            d.Invoke(s);
        }, state);
    }

    public override SynchronizationContext CreateCopy()
    {
        return new CultureAwareSynchronizationContext(cultureHolder, synchronizationImplementation.CreateCopy());
    }

    public override string ToString()
    {
        return string.Format("CultureAwareSynchronizationContext: {0}", cultureHolder);
    }
}

Usage:

/// code that detects Browser's culture 
void Detection()
{
        Thread.CurrentThread.CurrentUICulture = new CultureInfo("cs");
        SynchronizationContext.SetSynchronizationContext(new CultureAwareSynchronizationContext());
}

This solution suffers from possible issues mentioned by Hans Passant.

Elbrus answered 5/6, 2015 at 11:28 Comment(5)
This is the best solution for now. Note that there are several ASP.NET helper methods that assume the current context is AspNetSynchronizationContext. So this solution must be used with care.Demantoid
It seems very invasive to replace the built-in ASP.NET sync context. Also, this might break assumptions that the framework and libraries make about what a sync context does or does not do. The sync context is global state - be careful with it.Thaler
@StephenCleary, can you please be more specific? Only direct references to AspNetSynchronizatonContext I've found is in Page.cs - for example referencesource.microsoft.com/#System.Web/UI/Page.cs,5095 - those are used for async loading of ASP.NET controls. My SynchronizationContext should have the same behavior as SynchronizationContext that was in use before (except the CurrentCulture) - it wraps and calls the previous one.Elbrus
@Yossarian: I've observed this behavior with ASP.NET MVC child actions.Demantoid
In case anyone is curious, ASP.NET Core doesn't use a synchronization context, so this shouldn't be an issue.Diatom
D
4

Great explanation is in official doc.

Shortly for .Net 4.6+, you need to set the Culture in the calling thread, before await. Thus the Culture will be passed from Current thread to ALL next async/await (read as subsequent threads).

public async Task SomeFunction(){
  Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(locale);
  Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture;
  await OtherFunction();
}
Dad answered 15/1, 2021 at 14:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.