tl;dr
In .NET Framework, SynchronizationContext is one of the contexts flown by ExecutionContext. Is this no longer true in .NET Core?
The long question
In Stephen Toub's blog post ExecutionContext vs SynchronizationContext from 2012, he writes about how SynchronizationContext is a part of ExecutionContext:
Isn’t SynchronizationContext part of ExecutionContext?
I’ve glossed over some details up until this point, but I can’t avoid them any further.
The main thing I glossed over is that of all the contexts ExecutionContext is capable of flowing (e.g. SecurityContext, HostExecutionContext, CallContext, etc.), SynchronizationContext is actually one of them. This is, I personally believe, a mistake in API design, one that’s caused a few problems since it was instituted in .NET many versions ago. Nevertheless, it’s the design we have and have had for a long time, and changing it now would be a breaking change.
The blog post goes on to elaborate on when the SynchronizationContext is being flown as part of ExecutionContext, and when that flowing is being suppressed:
The story now gets a bit messier: ExecutionContext actually has two Capture methods, but only one of them is public. The internal one (internal to mscorlib) is the one used by most asynchronous functionality exposed from mscorlib, and it optionally allows the caller to suppress the capturing of SynchronizationContext as part of ExecutionContext; corresponding to that, there’s also an internal overload of the Run method that supports ignoring a SynchronizationContext that’s stored in the ExecutionContext, in effect pretending one wasn’t captured (this is, again, the overload used by most functionality in mscorlib). What this means is that pretty much any asynchronous operation whose core implementation resides in mscorlib won’t flow SynchronizationContext as part of ExecutionContext, but any asynchronous operation whose core implementation resides anywhere else will flow SynchronizationContext as part of ExecutionContext.
However, Stephen Toub clearly talks about .NET Framework here, and reading through some of the source code for how ExecutionContext is implemented in .NET Core, it seems that this might have changed in .NET Core. The boolean preserveSyncCtx argument that was part of the internal .NET Framework ExecutionContext methods are nowhere to be found in the more modern .NET Core implementations.
But the Microsoft documentation for ExecutionContext is the same for .NET Framework and .NET Core, and states
The ExecutionContext class provides a single container for all information relevant to a logical thread of execution. This includes security context, call context, and synchronization context.
and
Wherever the compressed stack flows, the managed principal, synchronization, locale, and user context also flow.
which seems to indicate that SynchronizationContext should still be part of ExecutionContext.
To try and figure out if there is a difference, I wrote the following NUnit test:
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
[TestFixture]
public class ExecutionContextFlowTests
{
private class TestSynchronizationContext : SynchronizationContext
{
/// <inheritdoc />
public override SynchronizationContext CreateCopy()
{
return new TestSynchronizationContext();
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is TestSynchronizationContext;
}
/// <inheritdoc />
public override int GetHashCode()
{
return 0;
}
}
[Test]
public async Task Test()
{
/* Arrange */
var syncCtx = new TestSynchronizationContext();
Task<ExecutionContext> t;
using (ExecutionContext.SuppressFlow())
{
t = Task.Run(() =>
{
SynchronizationContext prevCtx = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(syncCtx);
try
{
return ExecutionContext.Capture();
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevCtx);
}
});
}
ExecutionContext capturedContext = await t.ConfigureAwait(false);
Assert.That(capturedContext, Is.Not.Null);
Assert.That(SynchronizationContext.Current, Is.Not.EqualTo(syncCtx));
/* Act */
var syncCtxBox = new StrongBox<SynchronizationContext>();
ExecutionContext.Run(
capturedContext,
box => ((StrongBox<SynchronizationContext>)box).Value = SynchronizationContext.Current,
syncCtxBox
);
Assert.That(syncCtxBox.Value, Is.EqualTo(syncCtx));
}
}
and, voilà, the assert passes if run with .NET Framework, but fails on .NET Core (I used .NET Framework 4.2.7 and .NET Core 3.1).
So my question: is the blog post and Microsoft documentation simply outdated, and the "mistake in API design" that Stephen Toub talks about has been "fixed" in .NET Core, or am I missing something?