Подтвердить что ты не робот

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

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

Contract:

[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 без передачи его вниз по стеку вручную?

4b9b3361

Ответ 1

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

Тем не менее, есть еще несколько вариантов:

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

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

Ответ 2

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

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

    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.Current, как ожидалось.

Ответ 4

Здесь пример реализации 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);
}

Ответ 5

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

Ответ 6

Расширяясь по опции Mr. Cleary # 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 в конструкторе, но это было связано с асинхронным, и я знаю, что это было необходимо в моем случае.

Ответ 7

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

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

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

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

Resolve:

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