可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am using async/await pattern in .NET 4.5 to implement some service methods in WCF.
Example service:
Contract:
[ServiceContract(Namespace = "http://async.test/")]
public interface IAsyncTest
{
Task DoSomethingAsync();
}
Implementation:
MyAsyncService : IAsyncTest
{
public async Task DoSomethingAsync()
{
var context = OperationContext.Current; // context is present
await Task.Delay(10);
context = OperationContext.Current; // context is null
}
}
The problem I am having is that after first await
OperationContext.Current
returns null
and I can't access OperationContext.Current.IncomingMessageHeaders
.
In this simple example this is not a problem since I can capture the context before the await
. But in the real world case OperationContext.Current
is being accessed from deep inside the call stack and I really don't want to change lots of code just to pass the context further.
Is there a way to get operation context after await
point without passing it down the stack manually?
回答1:
I think your best option is to actually capture it and pass it manually. You may find this improves the testability of your code.
That said, there are a couple of other options:
- Add it to the
LogicalCallContext
.
- Install your own
SynchronizationContext
which will set OperationContext.Current
when it does a Post
; this is how ASP.NET preserves its HttpContext.Current
.
- Install your own
TaskScheduler
which sets OperationContext.Current
.
You may also want to raise this issue on Microsoft Connect.
回答2:
It is unfortunate that this doesn't work and we will see about getting a fix out in a future release.
In the mean time, there is a way to reapply the context to the current thread so that you don't have to pass the object around:
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;
}
In the above example, the DoSomethingElse() method will have access to OperationContext.Current as expected.
回答3:
It seems to be fixed in .Net 4.6.2. See the announcement
回答4:
Here's a sample SynchronizationContext
implementation:
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);
}
}
And usage:
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:
Expanding on Mr. Cleary's #1 option, the following code can be placed in the constructor of the WCF service to store and retrieve the OperationContext
in the logical call context:
if (CallContext.LogicalGetData("WcfOperationContext") == null)
{
CallContext.LogicalSetData("WcfOperationContext", OperationContext.Current);
}
else if (OperationContext.Current == null)
{
OperationContext.Current = (OperationContext)CallContext.LogicalGetData("WcfOperationContext");
}
With that, anywhere you are having issues with a null context you can write something like the following:
var cachedOperationContext = CallContext.LogicalGetData("WcfOperationContext") as OperationContext;
var user = cachedOperationContext != null ? cachedOperationContext.ServiceSecurityContext.WindowsIdentity.Name : "No User Info Available";
Disclaimer: This is year-old code and I don't remember the reason I needed the else if
in the constructor, but it was something to do with async and I know it was needed in my case.
回答6:
Fortunately for us, our real-life service implementation gets instantiated via Unity
IoC container. That allowed us to create a IWcfOperationContext
which was configured to have a PerResolveLifetimeManager
which simply means that there will be only one instance of WcfOperationContext
for each instance of our RealService
.
In the constructor of WcfOperationContext
we capture OperationContext.Current
and then all the places that require it get it from IWcfOperationContext
. This is in effect what Stephen Cleary suggested in his answer.
回答7:
Update: As pointed out by in the comments below, this solution is not thread safe, so I guess the solutions discussed above is still the best way.
I get around with the problem by registering the HttpContext into my DI container (Application_BeginRequest) and resolve it whenever I need it.
Register:
this.UnityContainer.RegisterInstance<HttpContextBase>(new HttpContextWrapper(HttpContext.Current));
Resolve:
var context = Dependencies.ResolveInstance<HttpContextBase>();