OperationContext.Current имеет значение null после первого ожидания при использовании async/await в службе WCF

Я использую шаблон async/await в .NET 4.5 для реализации некоторых методов службы в WCF. Пример сервиса:

договора:

[ServiceContract(Namespace = "http://async.test/")]
public interface IAsyncTest
{
    Task DoSomethingAsync();
}

реализация:

MyAsyncService : IAsyncTest
{
    public async Task DoSomethingAsync()
    {
        var context = OperationContext.Current; // context is present

        await Task.Delay(10);

        context = OperationContext.Current; // context is null
    }
}

проблема, с которой я сталкиваюсь, заключается в том, что после первого await OperationContext.Current возвращает null и я не могу открыть OperationContext.Current.IncomingMessageHeaders.

в этом простом примере это не проблема, так как я могу захватить контекст перед await. Но в реальном мире дело OperationContext.Current осуществляется от глубоко внутри стека вызовов, и я действительно не хочу менять много кода, чтобы передать контекст дальше.

есть ли способ получить контекст операции после await point, не передавая его вниз по стеку вручную?

7 ответов


Я думаю, что ваш лучший вариант - фактически захватить его и передать вручную. Вы можете найти это улучшает тестируемость кода.

тем не менее, есть несколько других вариантов:

  1. добавить его в LogicalCallContext.
  2. Установите свой собственный SynchronizationContext который установит OperationContext.Current, когда он Post; вот как ASP.NET сохраняет его HttpContext.Current.
  3. Установите свой собственный TaskScheduler, который устанавливает OperationContext.Current.

вы можете также хотите поднять эту проблему в Microsoft Connect.


к сожалению, это не работает, и мы увидим, как получить исправление в будущем выпуске.

в то же время есть способ повторно применить контекст к текущему потоку, чтобы вам не пришлось передавать объект вокруг:

    public async Task<double> Add(double n1, double n2)
    {

        OperationContext ctx = OperationContext.Current;

        await Task.Delay(100);

        using (new OperationContextScope(ctx))
        {
            DoSomethingElse();
        }
        return n1 + n2;
    }  

В приведенном выше примере DoSomethingElse() метод будет иметь доступ к OperationContext.Текущий, как и ожидалось.


кажется, что это исправлено в .Net 4.6.2. См.объявление


вот пример SynchronizationContext реализация:

public class OperationContextSynchronizationContext : SynchronizationContext
{
    private readonly OperationContext context;

    public OperationContextSynchronizationContext(IClientChannel channel) : this(new OperationContext(channel)) { }

    public OperationContextSynchronizationContext(OperationContext context)
    {
        OperationContext.Current = context;
        this.context = context;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        OperationContext.Current = context;
        d(state);
    }
}

и использование:

var currentSynchronizationContext = SynchronizationContext.Current;
try
{
    SynchronizationContext.SetSynchronizationContext(new OperationContextSynchronizationContext(client.InnerChannel));
    var response = await client.RequestAsync();
    // safe to use OperationContext.Current here
}
finally
{
    SynchronizationContext.SetSynchronizationContext(currentSynchronizationContext);
}

расширение на опции #1 г-на Клири, следующий код может быть помещен в конструктор службы WCF для хранения и извлечения OperationContext в контексте логического вызова:

if (CallContext.LogicalGetData("WcfOperationContext") == null)
{
     CallContext.LogicalSetData("WcfOperationContext", OperationContext.Current);
}
else if (OperationContext.Current == null)
{
     OperationContext.Current = (OperationContext)CallContext.LogicalGetData("WcfOperationContext");
}

С этим, в любом месте у вас возникли проблемы с нулевым контекстом, вы можете написать что-то вроде следующего:

var cachedOperationContext = CallContext.LogicalGetData("WcfOperationContext") as OperationContext;
var user = cachedOperationContext != null ? cachedOperationContext.ServiceSecurityContext.WindowsIdentity.Name : "No User Info Available";

отказ от ответственности: это годовой код, и я не помню, почему мне нужен else if в конструкторе, но это было что-то делать с async и я знаю это было необходимо в моем случае.


к счастью для нас, наша реализация реальных услуг получает экземпляр через Unity IoC-контейнер. Это позволило нам создать IWcfOperationContext, который был настроен, чтобы иметь PerResolveLifetimeManager что просто означает, что будет только один экземпляр WcfOperationContext для каждого экземпляра нашей RealService.
В конструкторе WcfOperationContext мы фиксируем OperationContext.Current а затем все места, которые требуют его получить от IWcfOperationContext. На самом деле это то, что предложил Стивен Клири в своем ответе.


Update: как указано в комментариях ниже, это решение не является потокобезопасным, поэтому я думаю, что решения, рассмотренные выше, по-прежнему являются лучшим способом.

я справляюсь с проблемой, регистрируя HttpContext в моем контейнере DI (Application_BeginRequest) и разрешаю его всякий раз, когда мне это нужно.

Регистрация:

this.UnityContainer.RegisterInstance<HttpContextBase>(new HttpContextWrapper(HttpContext.Current));

устранение:

var context = Dependencies.ResolveInstance<HttpContextBase>();