107

I am using Tasks to run long running server calls in my ViewModel and the results are marshalled back on Dispatcher using TaskScheduler.FromSyncronizationContext(). For example:

var context = TaskScheduler.FromCurrentSynchronizationContext();
this.Message = "Loading...";
Task task = Task.Factory.StartNew(() => { ... })
            .ContinueWith(x => this.Message = "Completed"
                          , context);

This works fine when I execute the application. But when I run my NUnit tests on Resharper I get the error message on the call to FromCurrentSynchronizationContext as:

The current SynchronizationContext may not be used as a TaskScheduler.

I guess this is because the tests are run on worker threads. How can I ensure the tests are run on main thread ? Any other suggestions are welcome.

anivas
  • 6,437
  • 6
  • 37
  • 45
  • in my case I was using `TaskScheduler.FromCurrentSynchronizationContext()` inside a lambda and execution was deferred to another thread. getting the context outside lambda fixed the problem. – M.kazem Akhgary Dec 27 '18 at 23:13

3 Answers3

156

You need to provide a SynchronizationContext. This is how I handle it:

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ritch Melton
  • 11,498
  • 4
  • 41
  • 54
  • 7
    For MSTest: put the code above in the Method marked with the ClassInitializeAttribute. – Daniel Bişar Jul 09 '13 at 15:36
  • 7
    @SACO: Actually, I have to put it in a the method with `TestInitializeAttribute`, otherwise only the first test passes. – Thorarin Oct 14 '13 at 12:21
  • i'm using ncrunch, perhaps that's the problem? – tofutim Dec 10 '13 at 02:09
  • 2
    For xunit tests, I put it in the static type ctor, since it only needs to be setup once per fixture. – codekaizen Aug 14 '14 at 23:57
  • 3
    I do not understand at all why this answer was accepted as the solution. IT DOES NOT WORK. And the reason is simple: SynchronizationContext is a dummy class whose send/post function are useless. This class should be abstract rather than a concrete class that possibly leads people into a false sense of "it's working". @tofutim You probably want to provide your own implementation derived from SyncContext. – h9uest Mar 02 '15 at 17:07
  • This solution works fine for NUnit. Others have documented xunit and mstest tips in the comments. I'm not sure what your concern is, but IT DOES WORK. Hence the upvotes. – Ritch Melton Mar 02 '15 at 19:57
  • @RitchMelton it is not working for me, using MSTest, with a UWP unit test project. SynchronizationContext.Current is null in the test code, no matter whether I put this solution in [TestInitialize] or [ClassInitialize]. – Sapph Apr 09 '16 at 20:18
  • 1
    I think I figured it out. My TestInitialize is asynchronous. Every time there is an "await" in the TestInit, the current SynchronizationContext is lost. This is because (as @h9uest pointed out), the default implementation of SynchronizationContext just queues tasks to the ThreadPool and doesn't actually continue on the same thread. – Sapph Apr 09 '16 at 21:29
  • That does makes sense. Maybe the answer should get a #99Percentofthetimeitworks tag. – Ritch Melton Apr 09 '16 at 21:36
  • I added my workaround as a separate answer. This answer is nice and simple for most cases, but hopefully mine can save some hair pulling for the 1%. :) – Sapph Apr 09 '16 at 22:02
31

Ritch Melton's solution did not work for me. This is because my TestInitialize function is async, as are my tests, so with every await the current SynchronizationContext is lost. This is because as MSDN points out, the SynchronizationContext class is "dumb" and just queues all work to the thread pool.

What worked for me is actually just skipping over the FromCurrentSynchronizationContext call when there isn't a SynchronizationContext (that is, if the current context is null). If there's no UI thread, I don't need to synchronize with it in the first place.

TaskScheduler syncContextScheduler;
if (SynchronizationContext.Current != null)
{
    syncContextScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}
else
{
    // If there is no SyncContext for this thread (e.g. we are in a unit test
    // or console scenario instead of running in an app), then just use the
    // default scheduler because there is no UI thread to sync with.
    syncContextScheduler = TaskScheduler.Current;
}

I found this solution more straightforward than the alternatives, which where:

  • Pass a TaskScheduler to the ViewModel (via dependency injection)
  • Create a test SynchronizationContext and a "fake" UI thread for the tests to run on - way more trouble for me that it's worth

I lose some of the threading nuance, but I am not explicitly testing that my OnPropertyChanged callbacks trigger on a specific thread so I am okay with that. The other answers using new SynchronizationContext() don't really do any better for that goal anyway.

Sapph
  • 6,118
  • 1
  • 29
  • 32
  • Your `else` case will fail also in a windows service app, resulting `syncContextScheduler == null` – FindOutIslamNow Jul 10 '17 at 10:10
  • Came across the same problem, but instead I read the NUnit source code. AsyncToSyncAdapter only overrides your SynchronizationContext if it is running in a STA thread. A workaround is to mark your class with a `[RequiresThread]` attribute. – Aron Jul 30 '19 at 07:34
1

I have combined multiple solution to have guarantee for working SynchronizationContext:

using System;
using System.Threading;
using System.Threading.Tasks;

public class CustomSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback action, object state)
    {
        SendOrPostCallback actionWrap = (object state2) =>
        {
            SynchronizationContext.SetSynchronizationContext(new CustomSynchronizationContext());
            action.Invoke(state2);
        };
        var callback = new WaitCallback(actionWrap.Invoke);
        ThreadPool.QueueUserWorkItem(callback, state);
    }
    public override SynchronizationContext CreateCopy()
    {
        return new CustomSynchronizationContext();
    }
    public override void Send(SendOrPostCallback d, object state)
    {
        base.Send(d, state);
    }
    public override void OperationStarted()
    {
        base.OperationStarted();
    }
    public override void OperationCompleted()
    {
        base.OperationCompleted();
    }

    public static TaskScheduler GetSynchronizationContext() {
      TaskScheduler taskScheduler = null;

      try
      {
        taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
      } catch {}

      if (taskScheduler == null) {
        try
        {
          taskScheduler = TaskScheduler.Current;
        } catch {}
      }

      if (taskScheduler == null) {
        try
        {
          var context = new CustomSynchronizationContext();
          SynchronizationContext.SetSynchronizationContext(context);
          taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        } catch {}
      }

      return taskScheduler;
    }
}

Usage:

var context = CustomSynchronizationContext.GetSynchronizationContext();

if (context != null) 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... }, context);
}
else 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... });
}
ujeenator
  • 26,384
  • 2
  • 23
  • 28