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;
}
}
}
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 await
s in the same method.