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 ответов
Я думаю, что ваш лучший вариант - фактически захватить его и передать вручную. Вы можете найти это улучшает тестируемость кода.
тем не менее, есть несколько других вариантов:
- добавить его в
LogicalCallContext
. - Установите свой собственный
SynchronizationContext
который установитOperationContext.Current
, когда онPost
; вот как ASP.NET сохраняет егоHttpContext.Current
. - Установите свой собственный
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.Текущий, как и ожидалось.
вот пример 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>();