diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs
index e9f9ca61eb..7f7dac2130 100644
--- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs
+++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ForegroundDispatcher.cs
@@ -3,6 +3,7 @@
using System;
using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor
@@ -11,6 +12,10 @@ namespace Microsoft.CodeAnalysis.Razor
{
public abstract bool IsForegroundThread { get; }
+ public abstract TaskScheduler ForegroundScheduler { get; }
+
+ public abstract TaskScheduler BackgroundScheduler { get; }
+
public virtual void AssertForegroundThread([CallerMemberName] string caller = null)
{
if (!IsForegroundThread)
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs
index 72e27447a5..e0f8400e27 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioForegroundDispatcher.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Composition;
+using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Shell;
@@ -12,6 +13,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
[ExportWorkspaceService(typeof(ForegroundDispatcher), ServiceLayer.Host)]
internal class VisualStudioForegroundDispatcher : ForegroundDispatcher
{
+ public override TaskScheduler BackgroundScheduler { get; } = TaskScheduler.Default;
+
+ public override TaskScheduler ForegroundScheduler { get; } = TaskScheduler.FromCurrentSynchronizationContext();
+
public override bool IsForegroundThread => ThreadHelper.CheckAccess();
}
}
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs
deleted file mode 100644
index 3c7d626984..0000000000
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System.Threading;
-using Microsoft.CodeAnalysis.Razor;
-
-namespace Microsoft.VisualStudio.LanguageServices.Razor
-{
- public abstract class ForegroundDispatcherTestBase
- {
- internal ForegroundDispatcher Dispatcher { get; } = new SingleThreadedForegroundDispatcher();
-
- private class SingleThreadedForegroundDispatcher : ForegroundDispatcher
- {
- private Thread Thread { get; } = Thread.CurrentThread;
-
- public override bool IsForegroundThread => Thread.CurrentThread == Thread;
- }
- }
-}
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj
index 18d2b92a3a..29cf10f6e8 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj
@@ -13,6 +13,10 @@
+
+
+
+
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundDispatcherTestBase.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundDispatcherTestBase.cs
new file mode 100644
index 0000000000..226b0b2211
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundDispatcherTestBase.cs
@@ -0,0 +1,52 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Razor;
+
+namespace Xunit
+{
+ public abstract class ForegroundDispatcherTestBase
+ {
+ internal ForegroundDispatcher Dispatcher { get; } = new SingleThreadedForegroundDispatcher();
+
+ private class SingleThreadedForegroundDispatcher : ForegroundDispatcher
+ {
+ public SingleThreadedForegroundDispatcher()
+ {
+ ForegroundScheduler = SynchronizationContext.Current == null ? new ThrowingTaskScheduler() : TaskScheduler.FromCurrentSynchronizationContext();
+ BackgroundScheduler = TaskScheduler.Default;
+ }
+
+ public override TaskScheduler ForegroundScheduler { get; }
+
+ public override TaskScheduler BackgroundScheduler { get; }
+
+ private Thread Thread { get; } = Thread.CurrentThread;
+
+ public override bool IsForegroundThread => Thread.CurrentThread == Thread;
+ }
+
+ private class ThrowingTaskScheduler : TaskScheduler
+ {
+ protected override IEnumerable GetScheduledTasks()
+ {
+ return Enumerable.Empty();
+ }
+
+ protected override void QueueTask(Task task)
+ {
+ throw new InvalidOperationException("Use [ForegroundFactAttribute]");
+ }
+
+ protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
+ {
+ throw new InvalidOperationException("Use [ForegroundFactAttribute]");
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactAttribute.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactAttribute.cs
new file mode 100644
index 0000000000..e111c5b330
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactAttribute.cs
@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor.Test
+{
+ // Similar to WpfFactAttribute https://github.com/xunit/samples.xunit/blob/969d9f7e887836f01a6c525324bf3db55658c28f/STAExamples/WpfFactAttribute.cs
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ [XunitTestCaseDiscoverer(nameof(ForegroundFactAttribute), nameof(Microsoft.VisualStudio.LanguageServices.Razor))]
+ internal class ForegroundFactAttribute : FactAttribute
+ {
+ }
+}
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactDiscoverer.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactDiscoverer.cs
new file mode 100644
index 0000000000..02b0e0c383
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Xunit/ForegroundFactDiscoverer.cs
@@ -0,0 +1,117 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Threading;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Xunit
+{
+ internal class ForegroundFactTestCase : LongLivedMarshalByRefObject, IXunitTestCase
+ {
+ private IXunitTestCase _inner;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("Called by the de-serializer", error: true)]
+ public ForegroundFactTestCase()
+ {
+ }
+
+ public ForegroundFactTestCase(IXunitTestCase testCase)
+ {
+ _inner = testCase;
+ }
+
+ public string DisplayName => _inner.DisplayName;
+
+ public IMethodInfo Method => _inner.Method;
+
+ public string SkipReason => _inner.SkipReason;
+
+ public ISourceInformation SourceInformation
+ {
+ get => _inner.SourceInformation;
+ set => _inner.SourceInformation = value;
+ }
+
+ public ITestMethod TestMethod => _inner.TestMethod;
+
+ public object[] TestMethodArguments => _inner.TestMethodArguments;
+
+ public Dictionary> Traits => _inner.Traits;
+
+ public string UniqueID => _inner.UniqueID;
+
+ public void Deserialize(IXunitSerializationInfo info)
+ {
+ _inner = info.GetValue("InnerTestCase");
+ }
+
+ public void Serialize(IXunitSerializationInfo info)
+ {
+ info.AddValue("InnerTestCase", _inner);
+ }
+
+ public Task RunAsync(
+ IMessageSink diagnosticMessageSink,
+ IMessageBus messageBus,
+ object[] constructorArguments,
+ ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ var tcs = new TaskCompletionSource();
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());
+
+ var worker = _inner.RunAsync(diagnosticMessageSink, messageBus, constructorArguments, aggregator, cancellationTokenSource);
+
+ Exception caught = null;
+ var frame = new DispatcherFrame();
+ Task.Run(async () =>
+ {
+ try
+ {
+ await worker;
+ }
+ catch (Exception ex)
+ {
+ caught = ex;
+ }
+ finally
+ {
+ frame.Continue = false;
+ }
+ });
+
+ Dispatcher.PushFrame(frame);
+
+ if (caught == null)
+ {
+ tcs.SetException(caught);
+ }
+ else
+ {
+ tcs.SetResult(worker.Result);
+ }
+ }
+ catch (Exception e)
+ {
+ tcs.SetException(e);
+ }
+ });
+
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+
+ return tcs.Task;
+ }
+ }
+}