3
votes

I'm hosting my .NET 4.5 WCF services in IIS. There's a piece of information called "BusinessContext" (BC) that is stored in the OperationContext.Current instance, so that any logic downstream can reach it.

Everything worked fine until I introduced async/await, and I ran into this issue. @stephen-cleary mentioned ASP.NET uses the async-friendly AspNetSynchronizationContext to keep the HttpContext.Current across threads. Since I'm hosting in IIS I figured I should be able to take advantage of the AspNetSyncCtx in WCF, and use the HttpContext.Current instead of the OperationContext to store the BC.

I created a WCF service from scratch, which has targetFramework = 4.5, aspnet:UseTaskFriendlySynchronizationContext = true and aspNetCompatibilityEnabled = true set by default in the Web.config. I also added the AspNetCompatibilityRequirements = Required to my service.

At runtime I see the HttpContext.Current is there, but SynchronizationContext.Current is null. After an await the HttpContext becomes null, which is expected because there's no SyncCtx. Shouldn't it be set to AspNetSyncCtx when aspcompatibility is required? How does the AspNetSyncCtx get set in ASP.NET?

-- Possible solution.

Following @Stephen-cleary's here I went ahead and defined a custom SynchronizationContext to preserve the OperationContext across threads.

I'd like to hear the community's input regarding this implementation. Thanks.

public class OperationContextSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback d, object state)
    {
        OperationContext opCtx = OperationContext.Current;
        InternalState internalState = new InternalState()
        {
            OpCtx = opCtx,
            Callback = d,
            State = state,
            SyncCtx = this
        };
        ThreadPool.QueueUserWorkItem(new WaitCallback(InternalInvoker), internalState);
    }
    private void InternalInvoker(object internalState)
    {
        InternalState internalSt = internalState as InternalState;
        SynchronizationContext.SetSynchronizationContext(internalSt.SyncCtx);
        using (new OperationContextScope(internalSt.OpCtx))
        {
            internalSt.Callback.Invoke(internalSt.State);
        }
    }
    private class InternalState
    {
        public SynchronizationContext SyncCtx { get; set; }
        public OperationContext OpCtx { get; set; }
        public SendOrPostCallback Callback { get; set; }
        public object State { get; set; }
    }
}
1

1 Answers

2
votes

I ran into the issue (bug?) you mentioned where HttpContext.Current did not flow with async code in a WCF service hosted in IIS even with aspNetCompatiblity enabled and required.

I got my project working using the LogicalCallContext with CallContext.LogicalSetData and CallContext.LogicalGetData as described by Stephen Cleary in this blog post. I think setting/getting your business context thusly would work very well in your case and might be lighter weight and conceptually simpler than the custom SynchronizationContext. In your case you are having to Set your business context somewhere anyway ... might as well set it to the LogicalCallContext and I believe everything will be fine.

I'll explain my personal 'solution' in more detail, though I admit it is a bit hacky since I'm manually flowing the entire HttpContext object (and only in WCF methods). But again your case with your custom context object would seem to be a better fit.

In the ASP.NET web app I inherited, there were calls to HttpContext.Current littered throughout the business logic (not ideal). Obviously the project worked fine until I wanted several of the WCF methods in the app to be async.

A lot of the calls in the business logic would be from ASP.NET page loads, etc, where everything functions fine as it is.

I solved (kludged?) my problem in .NET 4.5 by creating a little helper class ContextHelper, and replaced all calls to HttpContext.Current with ContextHelper.CurrentHttpContext.

public class ContextHelper
{
    public static void PrepareHttpContextFlow()
    {
        CallContext.LogicalSetData("CurrentHttpContext", HttpContext.Current);
    }

    public static HttpContext CurrentHttpContext
    {
        get
        {
            return HttpContext.Current ?? 
                   CallContext.LogicalGetData("CurrentHttpContext") 
                   as HttpContext;
        }
    }
}

Now anytime the HttpContext.Current is defined, my helper method is essentially a no-op.

To make this work, the entry points for my WCF calls must call the PrepareHttpContextFlow method before the first call to await which is admittedly problematic and the main reason I consider this a kludge.

In my case this downside is mitigated by the fact that I have some OTHER manual calls required to add logical stack information for added error logging context that is lost when using async (since the physical stack information in an exception is not very useful for error logs in this case). So at least if I remember to do one of the "prepare for async" calls, I should remember to do the other :)