Асинхронный вызов службы WCF не сохраняет CurrentCulture

В соответствии с ответом в этот вопрос async/await вызов должен сохранить CurrentCulture. В моем случае, когда я называю свои собственные методы асинхронизации, CurrentCulture сохраняется. Но если я вызываю некоторый метод службы WCF, CurrentCulture не сохраняется, он изменяется на то, что выглядит как культура потоков по умолчанию сервера.

Я посмотрел, какие управляемые потоки вызываются. Бывает так, что каждая строка кода выполнялась на одном управляемом потоке (ManagedThreadId остается тем же). И CultureInfo остается неизменным после вызова моего собственного метода асинхронизации. Но когда я вызываю метод WCF, ManagedTrheadId остается прежним, но CurrentCulture изменен.

Упрощенный код выглядит так:

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

    // Here we change current thread 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 culture
}


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

wcfClient - это экземпляр автоматически созданного клиента WCF (наследник System.ServiceModel.ClientBase)

Все это происходит в ASP.NET MVC Web API (самообслуживании), я использую заголовок Accept-Language, чтобы установить CurrentCulture для доступа к нему позже и использовать его для возврата локализованных ресурсов. Я мог бы двигаться без CurrentCulture и просто передавать CultureInfo всем методам, но мне не нравится этот подход.

Почему CurrentThread CurrentCulture изменяется после вызова службы WCF и остается неизменным после вызова моего собственного метода асинхронизации? Может ли оно быть "исправлено"?

Ответ 1

Сброс культуры до того, что было до await, обычно является заданием контекста синхронизации. Но поскольку (насколько я понимаю), у вас нет контекста синхронизации, await никак не изменит культуру потока.

Это означает, что если вы вернетесь к тому же потоку после await, вы увидите культуру, которую вы установили. Но если вы возобновите работу в другом потоке, вы увидите культуру по умолчанию (если кто-то еще не изменил ее для этого потока тоже).

Когда вы собираетесь находиться в одном потоке после await, когда нет контекста синхронизации? Вы гарантированно будете в том же потоке, если операция async завершится синхронно (например, ваш GetIntAsync()), потому что в этом случае метод просто синхронно продолжается после await. Вы также можете возобновить работу в том же потоке, если вам повезет, но вы не можете положиться на это. Это может быть недетерминированным, поэтому иногда ваш код может работать, а иногда и нет.

Что нужно делать, если вы хотите передать культуру с помощью await, и у вас нет контекста синхронизации, который делает это для вас? В принципе, у вас есть два варианта:

  • Используйте специальный awaiter (см. Stephen Toub WithCulture(), связанный с Noseratio). Это означает, что вам нужно добавить это ко всем своим await, где вам нужно потопить культуру, которая может быть громоздкой.
  • Используйте настраиваемый контекст синхронизации, который автоматически переместит культуру для каждого await. Это означает, что вы можете настроить контекст только один раз для каждой операции, и он будет работать правильно (подобно тому, как это делает контекст синхронизации ASP.NET). Это, вероятно, лучшее решение в долгосрочной перспективе.

Ответ 2

У меня нет окончательного ответа на то, почему описанное поведение может иметь место, особенно учитывая утверждение о том, что вся цепочка вызовов остается в одном потоке (ManagedThreadId остается неизменным). Более того, мое предположение о том, что культура не течет с контекстом выполнения в AspNetSynchronizationContext, неверно, оно действительно течет. Я взял точку из комментария @StephenCleary о попытке await Task.Delay и проверил это со следующим небольшим исследованием:

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

Вывод:

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

Таким образом, я мог только представить, что внутри wcfClient.GetResultAsync() что-то меняет текущую культуру потока. Обходным путем для этого может быть использование клиента awaiter, например Stephen Toub CultureAwaiter. Однако этот симптом вызывает беспокойство. Возможно, вам нужно найти созданный код прокси-сервера WCF для "культуры" и проверить, что там происходит. Попробуйте пройти его и узнать, в какой момент Thread.CurrentThread.CurrentCulture получает reset.