OperationContext.Current is null after first await

2019-01-10 22:56发布

问题:

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:

  1. Add it to the LogicalCallContext.
  2. Install your own SynchronizationContext which will set OperationContext.Current when it does a Post; this is how ASP.NET preserves its HttpContext.Current.
  3. 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>();