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
без передачи его вниз по стеку вручную?
Ответы
Ответ 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, как ожидалось.
Ответ 3
Кажется, исправлено в .Net 4.6.2. См. объявление
Ответ 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>();