OperationContext.Current равен нулю после первого ожидания при использовании 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
указать, не передавая его вниз по стеку вручную?
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.Current, как и ожидалось.
Расширяя опцию г-на Клири № 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
в конструкторе, но это было как-то связано с асинхронностью, и я знаю, что это было необходимо в моем случае.
Вот образец 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);
}
К счастью для нас, наша реальная реализация сервиса реализуется через Unity
Контейнер IoC. Это позволило нам создать IWcfOperationContext
который был настроен на PerResolveLifetimeManager
это просто означает, что будет только один случай WcfOperationContext
для каждого экземпляра нашего RealService
,
В конструкторе WcfOperationContext
мы захватили OperationContext.Current
и тогда все места, которые требуют это, получают это от IWcfOperationContext
, По сути, это то, что Стивен Клири предложил в своем ответе.
Обновление: Как указано в комментариях ниже, это решение не является потокобезопасным, поэтому я думаю, что решения, рассмотренные выше, все еще являются лучшим способом.
Я справляюсь с этой проблемой, регистрируя HttpContext в моем DI-контейнере (Application_BeginRequest) и решаю его всякий раз, когда мне это нужно.
Регистр:
this.UnityContainer.RegisterInstance<HttpContextBase>(new HttpContextWrapper(HttpContext.Current));
Разрешить:
var context = Dependencies.ResolveInstance<HttpContextBase>();