Search code examples
wcfsynchronizationcontext

How to use SynchronizationContext with WCF


I'm reading up on SynchronizationContext and trying to make sure I'm not messing anything up by trying to flow the OperationContext to all threads, even after an await call.

I have this SynchronizationContext class:

public class OperationContextSynchronizationContext : SynchronizationContext
{

    // Track the context to make sure that it flows through to the next thread.

    private readonly OperationContext _context;

    public OperationContextSynchronizationContext(OperationContext context)
    {
        _context = context;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        OperationContext.Current = _context;
        d(state);
    }
}

Which is then called like this around every method call (using a Ninject IInterceptor):

var original = SynchronizationContext.Current;
try
{
    // Make sure that the OperationContext flows across to the other threads,
    // since we need it for ContextStack.  (And also it's cool to have it.)
    SynchronizationContext.SetSynchronizationContext(new OperationContextSynchronizationContext(OperationContext.Current));

    // Process the method being called.
    invocation.Proceed();
}
finally
{
    SynchronizationContext.SetSynchronizationContext(original);
}

It seems to work (I'm able to use the OperationContext as needed), but is this the right way to do it? Am I missing anything important that might bite me later?

EDITed with some of Stephen Cleary's comments:

public class OperationContextSynchronizationContext : SynchronizationContext, IDisposable
{

    // Track the context to make sure that it flows through to the next thread.

    private readonly OperationContext _context;
    private readonly SynchronizationContext _previous;

    public OperationContextSynchronizationContext(OperationContext context)
    {
        _context = context;
        _previous = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(this);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        OperationContext.Current = _context;
        d(state);
        //(_previous ?? new SynchronizationContext()).Post(d, state);
    }

    private bool _disposed = false;
    public void Dispose()
    {
        if (!_disposed)
        {
            SynchronizationContext.SetSynchronizationContext(_previous);
            _disposed = true;
        }
    }
}

FINAL:

public class OperationContextSynchronizationContext : SynchronizationContext, IDisposable
{

    // Track the operation context to make sure that it flows through to the next call context.

    private readonly OperationContext _context;
    private readonly SynchronizationContext _previous;

    public OperationContextSynchronizationContext()
    {
        _context = OperationContext.Current;
        _previous = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(this);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        var context = _previous ?? new SynchronizationContext();
        context.Post(
            s =>
            {
                OperationContext.Current = _context;
                try
                {
                    d(s);
                }
                catch (Exception ex)
                {
                    // If we didn't have this, async void would be bad news bears.
                    // Since async void is "fire and forget," they happen separate
                    // from the main call stack.  We're logging this separately so
                    // that they don't affect the main call (and it just makes sense).

                    // log here
                }
            },
            state
        );
    }

    private bool _disposed = false;
    public void Dispose()
    {
        if (!_disposed)
        {
            // Return to the previous context.
            SynchronizationContext.SetSynchronizationContext(_previous);
            _disposed = true;
        }
    }
}

Solution

  • There are a few things that stand out to me.

    First, I can't recommend the use of a SynchronizationContext for this. You're trying to solve an application problem with a framework solution. It would work; I just find this questionable from an architectural perspective. The only alternatives aren't as clean, though: probably the most fitting would be to write an extension method for Task that returns a custom awaiter that preserves the OperationContext.

    Secondly, the implementation of OperationContextSynchronizationContext.Post executes the delegate directly. There are a couple of problems with this: for one thing, the delegate should be executed asynchronously (I suspect there are a few places in the .NET framework or TPL that assume this). For another, this SynchronizationContext has a specific implementation; it seems to me that it would be better if the custom SyncCtx wrapped an existing one. Some SyncCtx have specific threading requirements, and right now OperationContextSynchronizationContext is acting as a replacement for those rather than a supplement.

    Thirdly, the custom SyncCtx does not set itself as the current SyncCtx when it calls its delegate. So, it would not work if you have two awaits in the same method.