Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async call to the WCF service doesn't preserve CurrentCulture

According to the answer in this question async/await call should preserve CurrentCulture. In my case, when I call my own async methods, the CurrentCulture is preserved. But if I call some method of WCF service, CurrentCulture isn't preserved, it's being changed to something that looks like server's default thread's culture.

I've looked at what managed threads are invoked. It happens so that every line of code was executed on a single managed thread (ManagedThreadId stays the same). And CultureInfo stays the same after a call to my own async method. But when I call a WCF's method, ManagedTrheadId stays the same, but CurrentCulture is changed.

The simplified code looks like this::

private async Task Test()
{
    // Befoe this code Thread.CurrentThread.CurrentCulture is "ru-RU", i.e. the default server's culture

    // Here we change current thread's culture to some desired culture, e.g. hu-HU
    Thread.CurrentThread.CurrentCulture = new CultureInfo("hu-HU");

    var intres = await GetIntAsync();
    // after with await Thread.CurrentThread.CurrentCulture is still "hu-HU"

    var wcfres = await wcfClient.GetResultAsync();
    // And here Thread.CurrentThread.CurrentCulture is "ru-RU", i.e. the default server's culture
}


private async Task<int> GetIntAsync()
{
    return await Task.FromResult(1);
}

The wcfClient is an instance of auto generated WCF client (inheritor of System.ServiceModel.ClientBase)

This all happens in ASP.NET MVC Web API (self-hosted), I use Accept-Language header to set CurrentCulture to access it later and use it to return localized resources. I could move along without CurrentCulture and just pass CultureInfo to every method, but I don't like this approach.

Why CurrentThread's CurrentCulture is changed after a call to WCF service and remains the same after a call to my own async method? Can it be "fixed"?

like image 943
Dmitrii Lobanov Avatar asked Nov 19 '13 08:11

Dmitrii Lobanov


2 Answers

Resetting culture back to what it was before the await is usually the job of the synchronization context. But since (as far as I understand it), you don't have any synchronization context, the await won't modify the thread culture in any way.

This means that if you're back on the same thread after await, you're going to see the culture that you set. But if you resume on a different thread, you will see the default culture (unless somebody else modified it for that thread too).

When are you going to be on the same thread after an await, when there is no synchronization context? You're guaranteed to be on the same thread if the async operation completes synchronously (like your GetIntAsync()), because in that case, the method just synchronously continues after the await. You can also resume on the same thread if you're lucky, but you can't rely on that. This can be non-deterministic, so your code might seem to work sometimes, and sometimes not.

What you should do when you want to flow culture with awaits and you don't have a synchronization context that does it for you? Basically, you have two options:

  1. Use a custom awaiter (see Stephen Toub's WithCulture(), as linked by Noseratio). This means that you need to add this to all your awaits where you need to flow the culture, which could be cumbersome.
  2. Use a custom synchronization context that will flow the culture automatically for every await. This means you can set up the context only once for each operation and it will work correctly (similarly to what ASP.NET synchronization context does). This is probably the better solution in the long run.
like image 133
svick Avatar answered Nov 12 '22 04:11

svick


I don't have a definitive answer on why the described behavior may take place, especially given the statement that the whole chain of calls stay on the same thread (ManagedThreadId remains the same). Moreover, my assumption that culture doesn't flow with the execution context under AspNetSynchronizationContext was wrong, it does flow, in fact. I took the point from @StephenCleary's comment about trying await Task.Delay and verified that with the following little research:

// GET api/values/5
public async Task<string> Get(int id)
{
    // my default culture is en-US
    Log("Get, enter");

    Thread.CurrentThread.CurrentCulture = new CultureInfo("hu-HU");

    Log("Get, before Task.Delay");
    await Task.Delay(200);
    Thread.Sleep(200);

    Log("Get, before Task.Run");
    await Task.Run(() => Thread.Sleep(100));
    Thread.Sleep(200);

    Log("Get, before Task.Yield");
    await Task.Yield();

    Log("Get, before exit");
    return "value";
}

static void Log(string message)
{
    var ctx = SynchronizationContext.Current;
    Debug.Print("{0}; thread: {1}, context: {2}, culture {3}",
        message,
        Thread.CurrentThread.ManagedThreadId,
        ctx != null ? ctx.GetType().Name : String.Empty,
        Thread.CurrentThread.CurrentCulture.Name);
}

Output:

Get, enter; thread: 12, context: AspNetSynchronizationContext, culture en-US
Get, before Task.Delay; thread: 12, context: AspNetSynchronizationContext, culture hu-HU
Get, before Task.Run; thread: 11, context: AspNetSynchronizationContext, culture hu-HU
Get, before Task.Yield; thread: 10, context: AspNetSynchronizationContext, culture hu-HU
Get, before exit; thread: 11, context: AspNetSynchronizationContext, culture hu-HU

Thus, I could only imagine something inside wcfClient.GetResultAsync() actually changes the current thread's culture. A workaround for this could be to use a customer awaiter like Stephen Toub's CultureAwaiter. However, this symptom is worrying. Maybe you should search the generated WCF client proxy code for "culture" and check what's going on in there. Try stepping it through and find out at what point Thread.CurrentThread.CurrentCulture gets reset.

like image 1
noseratio Avatar answered Nov 12 '22 04:11

noseratio