Async call to the WCF service doesn't preserve CurrentCulture
Asked Answered
M

2

6

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"?

Mizzle answered 19/11, 2013 at 8:55 Comment(5)
What do you see when you change GetIntAsync to: await Task.Delay(100); return 1;?Comfortable
Interesting, it appears the culture doesn't flow under AspNetSynchronizationContext when your code continues on a different thread after await. You could work it around with a customer awaiter like Stephen Toub's CultureAwaiter from here: blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspxDelineator
The culture does flow under AspNetSynchronizationContext when there is a thread switch, verified. So it has to be something inside wcfClient.GetResultAsync().Delineator
@Noseratio I'm not sure we're dealing with AspNetSynchronizationContext hence this is a self-hosted ASP.NET MVC Web API (.net 4.5), which is based on WCF Web API (please correct me if I'm wrong).Mizzle
@DmitryLobanov, correct, there is no AspNetSynchronizationContext for self-hosted Web API runtime. See Stephen's comments to my answer.Delineator
D
2

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.
Dewar answered 20/11, 2013 at 14:56 Comment(1)
BTW, I believe I discovered a bug in culture handling of the ASP.NET synchronization context when researching this question (thought that research turned out to be unnecessary).Dewar
D
1

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.

Delineator answered 20/11, 2013 at 4:7 Comment(2)
The op is self-hosting, so he doesn't have an AspNetSynchronizationContext. So, nothing should be explicitly preserving the culture, but on the other hand I'm really surprised that a WCF client proxy is changing it.Comfortable
@StephenCleary, good point, I missed the part about self-hosting.Delineator

© 2022 - 2024 — McMap. All rights reserved.