Erlend Graff
Erlend Graff

Reputation: 1528

Does SynchronizationContext no longer flow with ExecutionContext (going from .NET Framework to .NET Core)?

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?

Upvotes: 5

Views: 1240

Answers (2)

Erlend Graff
Erlend Graff

Reputation: 1528

Microsoft has acknowledged that ExecutionContext behavior has indeed changed with .NET Core, and are clarifying this in an update to their official documentation. As part of the change, they've added the following sentence:

Also in .NET Core, the synchronization context does not flow with the execution context, whereas in .NET Framework it may in some cases.

Upvotes: 4

avo
avo

Reputation: 10701

It looks like in .NET Core, at least in the current version 3.1, ExecutionContext doesn't capture SynchronizationContext anymore. See it here in the ExecutionContext source code. Still, if the synchronization context have changed inside the ExecutionContext.Run callback, it will be restored.

I think it makes sense, given that with.NET Core, SynchronizationContext is only really relevant in the front-end code. They aim to optimize the server-side code as much as possible, so they removed that piece.

Upvotes: 0

Related Questions