Browse Source

[Components] [Fixes #6953, #7226]

* Moves the Synchronization context from the remote renderer to the base renderer.
* Removes all the locking from the base renderer.
Javier Calvarro Nelson 7 years ago
parent
commit
f456e3d153
26 changed files with 648 additions and 523 deletions
  1. 1 3
      src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs
  2. 2 2
      src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
  3. 237 237
      src/Components/Browser.JS/src/package-lock.json
  4. 1 1
      src/Components/Components/perf/RenderTreeDiffBuilderBenchmark.cs
  5. 15 3
      src/Components/Components/src/RenderHandle.cs
  6. 2 1
      src/Components/Components/src/Rendering/HtmlRenderer.cs
  7. 42 0
      src/Components/Components/src/Rendering/IDispatcher.cs
  8. 105 46
      src/Components/Components/src/Rendering/Renderer.cs
  9. 6 6
      src/Components/Components/src/Rendering/RendererSynchronizationContext.cs
  10. 3 2
      src/Components/Components/test/CascadingParameterStateTest.cs
  11. 2 3
      src/Components/Components/test/CascadingParameterTest.cs
  12. 12 12
      src/Components/Components/test/LayoutTest.cs
  13. 1 1
      src/Components/Components/test/RenderTreeBuilderTest.cs
  14. 1 1
      src/Components/Components/test/RenderTreeDiffBuilderTest.cs
  15. 54 56
      src/Components/Components/test/RendererTest.cs
  16. 69 37
      src/Components/Components/test/Rendering/HtmlRendererTests.cs
  17. 27 29
      src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs
  18. 4 9
      src/Components/Server/src/Circuits/CircuitHost.cs
  19. 8 3
      src/Components/Server/src/Circuits/DefaultCircuitFactory.cs
  20. 3 52
      src/Components/Server/src/Circuits/RemoteRenderer.cs
  21. 3 7
      src/Components/Server/test/Circuits/CircuitHostTest.cs
  22. 1 1
      src/Components/Server/test/Circuits/RenderBatchWriterTest.cs
  23. 18 1
      src/Components/Shared/test/AutoRenderComponent.cs
  24. 25 6
      src/Components/Shared/test/TestRenderer.cs
  25. 2 1
      src/Components/test/E2ETest/ServerExecutionTests/ServerComponentRenderingTest.cs
  26. 4 3
      src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/HtmlHelperComponentExtensions.cs

+ 1 - 3
src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs

@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.JSInterop;
 using Mono.WebAssembly.Interop;
 using System;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace Microsoft.AspNetCore.Blazor.Rendering
@@ -31,9 +32,6 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
             _webAssemblyRendererId = RendererRegistry.Current.Add(this);
         }
 
-        internal void DispatchBrowserEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
-            => DispatchEvent(componentId, eventHandlerId, eventArgs);
-
         /// <summary>
         /// Attaches a new root component to the renderer,
         /// causing it to be displayed in the specified DOM element.

+ 2 - 2
src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs

@@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
         {
             var renderer = new TestRenderer();
             renderer.AttachComponent(component);
-            var task = component.SetParametersAsync(ParameterCollection.Empty);
+            var task = renderer.InvokeAsync(() => component.SetParametersAsync(ParameterCollection.Empty));
             // we will have to change this method if we add a test that does actual async work.
             Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
             if (task.IsFaulted)
@@ -434,7 +434,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
 
         private class TestRenderer : Renderer
         {
-            public TestRenderer() : base(new TestServiceProvider())
+            public TestRenderer() : base(new TestServiceProvider(), CreateDefaultDispatcher())
             {
             }
 

File diff suppressed because it is too large
+ 237 - 237
src/Components/Browser.JS/src/package-lock.json


+ 1 - 1
src/Components/Components/perf/RenderTreeDiffBuilderBenchmark.cs

@@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Components.Performance
         private class FakeRenderer : Renderer
         {
             public FakeRenderer()
-                : base(new TestServiceProvider())
+                : base(new TestServiceProvider(), new RendererSynchronizationContext())
             {
             }
 

+ 15 - 3
src/Components/Components/src/RenderHandle.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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 Microsoft.AspNetCore.Components.Rendering;
@@ -49,7 +49,13 @@ namespace Microsoft.AspNetCore.Components
         /// </summary>
         /// <param name="workItem">The work item to execute.</param>
         public Task Invoke(Action workItem)
-            => _renderer.Invoke(workItem);
+        {
+            if (_renderer == null)
+            {
+                throw new InvalidOperationException("The render handle is not yet assigned.");
+            }
+            return _renderer.Invoke(workItem);
+        }
 
         /// <summary>
         /// Executes the supplied work item on the renderer's
@@ -57,6 +63,12 @@ namespace Microsoft.AspNetCore.Components
         /// </summary>
         /// <param name="workItem">The work item to execute.</param>
         public Task InvokeAsync(Func<Task> workItem)
-            => _renderer.InvokeAsync(workItem);
+        {
+            if (_renderer == null)
+            {
+                throw new InvalidOperationException("The render handle is not yet assigned.");
+            }
+            return _renderer.InvokeAsync(workItem);
+        }
     }
 }

+ 2 - 1
src/Components/Components/src/Rendering/HtmlRenderer.cs

@@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// </summary>
         /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
         /// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
-        public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder) : base(serviceProvider)
+        public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder, IDispatcher dispatcher)
+            : base(serviceProvider, dispatcher)
         {
             _htmlEncoder = htmlEncoder;
         }

+ 42 - 0
src/Components/Components/src/Rendering/IDispatcher.cs

@@ -0,0 +1,42 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components.Rendering
+{
+    /// <summary>
+    /// Dispatches external actions to be executed on the context of a <see cref="Renderer"/>.
+    /// </summary>
+    public interface IDispatcher
+    {
+        /// <summary>
+        /// Invokes the given <see cref="Action"/> in the context of the associated <see cref="Renderer"/>.
+        /// </summary>
+        /// <param name="action">The action to execute.</param>
+        /// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
+        Task Invoke(Action action);
+
+        /// <summary>
+        /// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
+        /// </summary>
+        /// <param name="asyncAction">The asynchronous action to execute.</param>
+        /// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
+        Task InvokeAsync(Func<Task> asyncAction);
+
+        /// <summary>
+        /// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
+        /// </summary>
+        /// <param name="function">The function to execute.</param>
+        /// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
+        Task<TResult> Invoke<TResult>(Func<TResult> function);
+
+        /// <summary>
+        /// Invokes the given <see cref="Func{TResult}"/> in the context of the associated <see cref="Renderer"/>.
+        /// </summary>
+        /// <param name="asyncAction">The asynchronous function to execute.</param>
+        /// <returns>A <see cref="Task{TResult}"/> that will be completed when the function has finished executing.</returns>
+        Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> asyncFunction);
+    }
+}

+ 105 - 46
src/Components/Components/src/Rendering/Renderer.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Runtime.ExceptionServices;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
 
@@ -20,28 +21,61 @@ namespace Microsoft.AspNetCore.Components.Rendering
         private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
         private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
         private readonly Dictionary<int, EventHandlerInvoker> _eventBindings = new Dictionary<int, EventHandlerInvoker>();
+        private IDispatcher _dispatcher;
 
         private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
         private bool _isBatchInProgress;
         private int _lastEventHandlerId = 0;
         private List<Task> _pendingTasks;
 
-        // We need to introduce locking as we don't know if we are executing
-        // under a synchronization context that limits the ammount of concurrency
-        // that can happen when async callbacks are executed.
-        // As a result, we have to protect the _pendingTask list and the
-        // _batchBuilder render queue from concurrent modifications.
-        private object _asyncWorkLock = new object();
+        /// <summary>
+        /// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
+        /// </summary>
+        public event UnhandledExceptionEventHandler UnhandledSynchronizationException
+        {
+            add
+            {
+                if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
+                {
+                    return;
+                }
+                rendererSynchronizationContext.UnhandledException += value;
+            }
+            remove
+            {
+                if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
+                {
+                    return;
+                }
+                rendererSynchronizationContext.UnhandledException -= value;
+            }
+        }
 
         /// <summary>
         /// Constructs an instance of <see cref="Renderer"/>.
         /// </summary>
-        /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initialising components.</param>
+        /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
         public Renderer(IServiceProvider serviceProvider)
         {
             _componentFactory = new ComponentFactory(serviceProvider);
         }
 
+        /// <summary>
+        /// Constructs an instance of <see cref="Renderer"/>.
+        /// </summary>
+        /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
+        /// <param name="dispatcher">The <see cref="IDispatcher"/> to be for invoking user actions into the <see cref="Renderer"/> context.</param>
+        public Renderer(IServiceProvider serviceProvider, IDispatcher dispatcher) : this(serviceProvider)
+        {
+            _dispatcher = dispatcher;
+        }
+
+        /// <summary>
+        /// Creates an <see cref="IDispatcher"/> that can be used with one or more <see cref="Renderer"/>.
+        /// </summary>
+        /// <returns>The <see cref="IDispatcher"/>.</returns>
+        public static IDispatcher CreateDefaultDispatcher() => new RendererSynchronizationContext();
+
         /// <summary>
         /// Constructs a new component of the specified type.
         /// </summary>
@@ -198,14 +232,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
             while (_pendingTasks.Count > 0)
             {
                 Task pendingWork;
-                lock (_asyncWorkLock)
-                {
-                    // Create a Task that represents the remaining ongoing work for the rendering process
-                    pendingWork = Task.WhenAll(_pendingTasks);
+                // Create a Task that represents the remaining ongoing work for the rendering process
+                pendingWork = Task.WhenAll(_pendingTasks);
 
-                    // Clear all pending work.
-                    _pendingTasks.Clear();
-                }
+                // Clear all pending work.
+                _pendingTasks.Clear();
 
                 // new work might be added before we check again as a result of waiting for all
                 // the child components to finish executing SetParametersAsync
@@ -238,6 +269,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="eventArgs">Arguments to be passed to the event handler.</param>
         public void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
         {
+            EnsureSynchronizationContext();
+
             if (_eventBindings.TryGetValue(eventHandlerId, out var binding))
             {
                 // The event handler might request multiple renders in sequence. Capture them
@@ -266,9 +299,24 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="workItem">The work item to execute.</param>
         public virtual Task Invoke(Action workItem)
         {
-            // Base renderer has nothing to dispatch to, so execute directly
-            workItem();
-            return Task.CompletedTask;
+            // This is for example when we run on a system with a single thread, like WebAssembly.
+            if (_dispatcher == null)
+            {
+                workItem();
+                return Task.CompletedTask;
+            }
+
+            if (SynchronizationContext.Current == _dispatcher)
+            {
+                // This is an optimization for when the dispatcher is also a syncronization context, like in the default case.
+                // No need to dispatch. Avoid deadlock by invoking directly.
+                workItem();
+                return Task.CompletedTask;
+            }
+            else
+            {
+                return _dispatcher.Invoke(workItem);
+            }
         }
 
         /// <summary>
@@ -278,8 +326,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="workItem">The work item to execute.</param>
         public virtual Task InvokeAsync(Func<Task> workItem)
         {
-            // Base renderer has nothing to dispatch to, so execute directly
-            return workItem();
+            // This is for example when we run on a system with a single thread, like WebAssembly.
+            if (_dispatcher == null)
+            {
+                workItem();
+                return Task.CompletedTask;
+            }
+
+            if (SynchronizationContext.Current == _dispatcher)
+            {
+                // This is an optimization for when the dispatcher is also a syncronization context, like in the default case.
+                // No need to dispatch. Avoid deadlock by invoking directly.
+                return workItem();
+            }
+            else
+            {
+                return _dispatcher.InvokeAsync(workItem);
+            }
         }
 
         internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
@@ -323,10 +386,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     {
                         return;
                     }
-                    lock (_asyncWorkLock)
-                    {
-                        _pendingTasks.Add(task);
-                    }
+                    _pendingTasks.Add(task);
                     break;
             }
         }
@@ -351,6 +411,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
         protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment)
         {
+            EnsureSynchronizationContext();
+
             var componentState = GetOptionalComponentState(componentId);
             if (componentState == null)
             {
@@ -359,11 +421,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 return;
             }
 
-            lock (_asyncWorkLock)
-            {
-                _batchBuilder.ComponentRenderQueue.Enqueue(
-                    new RenderQueueEntry(componentState, renderFragment));
-            }
+            _batchBuilder.ComponentRenderQueue.Enqueue(
+                new RenderQueueEntry(componentState, renderFragment));
 
             if (!_isBatchInProgress)
             {
@@ -371,6 +430,22 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
+        private void EnsureSynchronizationContext()
+        {
+            // When the IDispatcher is a synchronization context
+            // Render operations are not thread-safe, so they need to be serialized.
+            // Plus, any other logic that mutates state accessed during rendering also
+            // needs not to run concurrently with rendering so should be dispatched to
+            // the renderer's sync context.
+            if (_dispatcher is SynchronizationContext synchronizationContext && SynchronizationContext.Current != synchronizationContext)
+            {
+                throw new InvalidOperationException(
+                    "The current thread is not associated with the renderer's synchronization context. " +
+                    "Use Invoke() or InvokeAsync() to switch execution to the renderer's synchronization " +
+                    "context when triggering rendering or modifying any state accessed during rendering.");
+            }
+        }
+
         private ComponentState GetRequiredComponentState(int componentId)
             => _componentStateById.TryGetValue(componentId, out var componentState)
                 ? componentState
@@ -389,8 +464,9 @@ namespace Microsoft.AspNetCore.Components.Rendering
             try
             {
                 // Process render queue until empty
-                while (TryDequeueRenderQueueEntry(out var nextToRender))
+                while (_batchBuilder.ComponentRenderQueue.Count > 0)
                 {
+                    var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue();
                     RenderInExistingBatch(nextToRender);
                 }
 
@@ -406,23 +482,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
-        private bool TryDequeueRenderQueueEntry(out RenderQueueEntry entry)
-        {
-            lock (_asyncWorkLock)
-            {
-                if (_batchBuilder.ComponentRenderQueue.Count > 0)
-                {
-                    entry = _batchBuilder.ComponentRenderQueue.Dequeue();
-                    return true;
-                }
-                else
-                {
-                    entry = default;
-                    return false;
-                }
-            }
-        }
-
         private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
         {
             var array = updatedComponents.Array;

+ 6 - 6
src/Components/Server/src/Circuits/CircuitSynchronizationContext.cs → src/Components/Components/src/Rendering/RendererSynchronizationContext.cs

@@ -6,10 +6,10 @@ using System.Diagnostics;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace Microsoft.AspNetCore.Components.Server.Circuits
+namespace Microsoft.AspNetCore.Components.Rendering
 {
     [DebuggerDisplay("{_state,nq}")]
-    internal class CircuitSynchronizationContext : SynchronizationContext
+    internal class RendererSynchronizationContext : SynchronizationContext, IDispatcher
     {
         private static readonly ContextCallback ExecutionContextThunk = (object state) =>
         {
@@ -27,12 +27,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
         public event UnhandledExceptionEventHandler UnhandledException;
 
-        public CircuitSynchronizationContext()
+        public RendererSynchronizationContext()
             : this(new State())
         {
         }
 
-        private CircuitSynchronizationContext(State state)
+        private RendererSynchronizationContext(State state)
         {
             _state = state;
         }
@@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
         // shallow copy
         public override SynchronizationContext CreateCopy()
         {
-            return new CircuitSynchronizationContext(_state);
+            return new RendererSynchronizationContext(_state);
         }
 
         private Task Enqueue(Task antecedant, SendOrPostCallback d, object state)
@@ -259,7 +259,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
         private class WorkItem
         {
-            public CircuitSynchronizationContext SynchronizationContext;
+            public RendererSynchronizationContext SynchronizationContext;
             public ExecutionContext ExecutionContext;
             public SendOrPostCallback Callback;
             public object State;

+ 3 - 2
src/Components/Components/test/CascadingParameterStateTest.cs

@@ -373,7 +373,8 @@ namespace Microsoft.AspNetCore.Components.Test
         static CascadingValue<T> CreateCascadingValueComponent<T>(T value, string name = null)
         {
             var supplier = new CascadingValue<T>();
-            supplier.Configure(new RenderHandle(new TestRenderer(), 0));
+            var renderer = new TestRenderer();
+            supplier.Configure(new RenderHandle(renderer, 0));
 
             var supplierParams = new Dictionary<string, object>
             {
@@ -385,7 +386,7 @@ namespace Microsoft.AspNetCore.Components.Test
                 supplierParams.Add("Name", name);
             }
 
-            supplier.SetParameters(supplierParams);
+            renderer.Invoke(() => supplier.SetParametersAsync(ParameterCollection.FromDictionary(supplierParams)));
             return supplier;
         }
        

+ 2 - 3
src/Components/Components/test/CascadingParameterTest.cs

@@ -1,12 +1,11 @@
 // 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 Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.RenderTree;
-using Microsoft.AspNetCore.Components.Test.Helpers;
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Test.Helpers;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test

+ 12 - 12
src/Components/Components/test/LayoutTest.cs

@@ -28,10 +28,10 @@ namespace Microsoft.AspNetCore.Components.Test
         public void DisplaysComponentInsideLayout()
         {
             // Arrange/Act
-            _layoutDisplayComponent.SetParameters(new Dictionary<string, object>
+            _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
-            });
+            })));
 
             // Assert
             var batch = _renderer.Batches.Single();
@@ -85,10 +85,10 @@ namespace Microsoft.AspNetCore.Components.Test
         public void DisplaysComponentInsideNestedLayout()
         {
             // Arrange/Act
-            _layoutDisplayComponent.SetParameters(new Dictionary<string, object>
+            _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 { LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
-            });
+            })));
 
             // Assert
             var batch = _renderer.Batches.Single();
@@ -112,16 +112,16 @@ namespace Microsoft.AspNetCore.Components.Test
         public void CanChangeDisplayedPageWithSameLayout()
         {
             // Arrange
-            _layoutDisplayComponent.SetParameters(new Dictionary<string, object>
+            _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
-            });
+            })));
 
             // Act
-            _layoutDisplayComponent.SetParameters(new Dictionary<string, object>
+            _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 { LayoutDisplay.NameOfPage, typeof(DifferentComponentWithLayout) }
-            });
+            })));
 
             // Assert
             Assert.Equal(2, _renderer.Batches.Count);
@@ -163,16 +163,16 @@ namespace Microsoft.AspNetCore.Components.Test
         public void CanChangeDisplayedPageWithDifferentLayout()
         {
             // Arrange
-            _layoutDisplayComponent.SetParameters(new Dictionary<string, object>
+            _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
-            });
+            })));
 
             // Act
-            _layoutDisplayComponent.SetParameters(new Dictionary<string, object>
+            _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 { LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
-            });
+            })));
 
             // Assert
             Assert.Equal(2, _renderer.Batches.Count);

+ 1 - 1
src/Components/Components/test/RenderTreeBuilderTest.cs

@@ -1057,7 +1057,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
         private class TestRenderer : Renderer
         {
-            public TestRenderer() : base(new TestServiceProvider())
+            public TestRenderer() : base(new TestServiceProvider(), new RendererSynchronizationContext())
             {
             }
 

+ 1 - 1
src/Components/Components/test/RenderTreeDiffBuilderTest.cs

@@ -1527,7 +1527,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
         private class FakeRenderer : Renderer
         {
-            public FakeRenderer() : base(new TestServiceProvider())
+            public FakeRenderer() : base(new TestServiceProvider(), new RendererSynchronizationContext())
             {
             }
 

+ 54 - 56
src/Components/Components/test/RendererTest.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.ExceptionServices;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Components.RenderTree;
@@ -15,6 +16,10 @@ namespace Microsoft.AspNetCore.Components.Test
 {
     public class RendererTest
     {
+        private const string EventActionsName = nameof(NestedAsyncComponent.EventActions);
+        private const string WhatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
+        private const string LogName = nameof(NestedAsyncComponent.Log);
+
         [Fact]
         public void CanRenderTopLevelComponents()
         {
@@ -171,7 +176,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act
             var componentId = renderer.AssignRootComponentId(component);
-            await renderer.RenderRootComponentAsync(componentId);
+            await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
 
             // Assert
             Assert.Equal(5, renderer.Batches.Count);
@@ -221,9 +226,9 @@ namespace Microsoft.AspNetCore.Components.Test
             // Act/Assert
             var componentId = renderer.AssignRootComponentId(component);
             var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
-            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
-                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                [EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
                 {
                     [0] = new List<NestedAsyncComponent.ExecutionAction>
                     {
@@ -240,13 +245,13 @@ namespace Microsoft.AspNetCore.Components.Test
                         NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
                     }
                 },
-                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                [WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
                 {
                     [0] = CreateRenderFactory(new[] { 1 }),
                     [1] = CreateRenderFactory(Array.Empty<int>())
                 },
-                [nameof(NestedAsyncComponent.Log)] = log
-            }));
+                [LogName] = log
+            })));
 
             var logForParent = log.Where(l => l.id == 0).ToArray();
             var logForChild = log.Where(l => l.id == 1).ToArray();
@@ -265,9 +270,9 @@ namespace Microsoft.AspNetCore.Components.Test
             // Act/Assert
             var componentId = renderer.AssignRootComponentId(component);
             var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
-            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
-                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                [EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
                 {
                     [0] = new List<NestedAsyncComponent.ExecutionAction>
                     {
@@ -284,13 +289,13 @@ namespace Microsoft.AspNetCore.Components.Test
                         NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
                     }
                 },
-                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                [WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
                 {
                     [0] = CreateRenderFactory(new[] { 1 }),
                     [1] = CreateRenderFactory(Array.Empty<int>())
                 },
-                [nameof(NestedAsyncComponent.Log)] = log
-            }));
+                [LogName] = log
+            })));
 
             var logForParent = log.Where(l => l.id == 0).ToArray();
             var logForChild = log.Where(l => l.id == 1).ToArray();
@@ -309,9 +314,9 @@ namespace Microsoft.AspNetCore.Components.Test
             // Act/Assert
             var componentId = renderer.AssignRootComponentId(component);
             var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
-            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
-                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                [EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
                 {
                     [0] = new List<NestedAsyncComponent.ExecutionAction>
                     {
@@ -328,13 +333,13 @@ namespace Microsoft.AspNetCore.Components.Test
                         NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
                     }
                 },
-                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                [WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
                 {
                     [0] = CreateRenderFactory(new[] { 1 }),
                     [1] = CreateRenderFactory(Array.Empty<int>())
                 },
-                [nameof(NestedAsyncComponent.Log)] = log
-            }));
+                [LogName] = log
+            })));
 
             var logForParent = log.Where(l => l.id == 0).ToArray();
             var logForChild = log.Where(l => l.id == 1).ToArray();
@@ -353,9 +358,9 @@ namespace Microsoft.AspNetCore.Components.Test
             // Act/Assert
             var componentId = renderer.AssignRootComponentId(component);
             var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
-            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
-                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                [EventActionsName] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
                 {
                     [0] = new List<NestedAsyncComponent.ExecutionAction>
                     {
@@ -386,15 +391,15 @@ namespace Microsoft.AspNetCore.Components.Test
                         NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
                     }
                 },
-                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                [WhatToRenderName] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
                 {
                     [0] = CreateRenderFactory(new[] { 1, 2 }),
                     [1] = CreateRenderFactory(new[] { 3 }),
                     [2] = CreateRenderFactory(Array.Empty<int>()),
                     [3] = CreateRenderFactory(Array.Empty<int>())
                 },
-                [nameof(NestedAsyncComponent.Log)] = log
-            }));
+                [LogName] = log
+            })));
 
             var logForParent = log.Where(l => l.id == 0).ToArray();
             var logForFirstChild = log.Where(l => l.id == 1).ToArray();
@@ -553,10 +558,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var eventArgs = new UIEventArgs();
 
             // Act/Assert
-            var ex = Assert.Throws<InvalidOperationException>(() =>
-            {
-                renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
-            });
+            var ex = Assert.Throws<InvalidOperationException>(() => renderer.DispatchEvent(componentId, eventHandlerId, eventArgs));
             Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " +
                 $"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message);
         }
@@ -568,10 +570,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
 
             // Act/Assert
-            Assert.Throws<ArgumentException>(() =>
-            {
-                renderer.DispatchEvent(123, 0, new UIEventArgs());
-            });
+            Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(123, 0, new UIEventArgs()));
         }
 
         [Fact]
@@ -766,8 +765,9 @@ namespace Microsoft.AspNetCore.Components.Test
             Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs);
 
             // Act/Assert: If a disposed component requests a render, it's a no-op
-            ((FakeComponent)childComponent3).RenderHandle.Render(builder
-                => throw new NotImplementedException("Should not be invoked"));
+            var renderHandle = ((FakeComponent)childComponent3).RenderHandle;
+            renderHandle.Invoke(() => renderHandle.Render(builder
+                => throw new NotImplementedException("Should not be invoked")));
             Assert.Equal(2, renderer.Batches.Count);
         }
 
@@ -798,10 +798,7 @@ namespace Microsoft.AspNetCore.Components.Test
             component.TriggerRender();
 
             // Act/Assert 2: Can no longer fire the original event, but can fire the new event
-            Assert.Throws<ArgumentException>(() =>
-            {
-                renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
-            });
+            Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
             Assert.Equal(1, eventCount);
             Assert.Equal(0, newEventCount);
             renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null);
@@ -834,10 +831,7 @@ namespace Microsoft.AspNetCore.Components.Test
             component.TriggerRender();
 
             // Act/Assert 2: Can no longer fire the original event
-            Assert.Throws<ArgumentException>(() =>
-            {
-                renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
-            });
+            Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
             Assert.Equal(1, eventCount);
         }
 
@@ -883,10 +877,7 @@ namespace Microsoft.AspNetCore.Components.Test
             component.TriggerRender();
 
             // Act/Assert 2: Can no longer fire the original event
-            Assert.Throws<ArgumentException>(() =>
-            {
-                renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null);
-            });
+            Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null));
             Assert.Equal(1, eventCount);
         }
 
@@ -916,10 +907,7 @@ namespace Microsoft.AspNetCore.Components.Test
             component.TriggerRender();
 
             // Act/Assert 2: Can no longer fire the original event
-            Assert.Throws<ArgumentException>(() =>
-            {
-                renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
-            });
+            Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
             Assert.Equal(1, eventCount);
         }
 
@@ -1009,10 +997,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var component = new TestComponent(builder => { });
 
             // Act/Assert
-            var ex = Assert.Throws<InvalidOperationException>(() =>
-            {
-                component.TriggerRender();
-            });
+            var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
             Assert.Equal("The render handle is not yet assigned.", ex.Message);
         }
 
@@ -1317,7 +1302,7 @@ namespace Microsoft.AspNetCore.Components.Test
         }
 
         [Fact]
-        public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatch()
+        public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync()
         {
             // This represents the scenario where the same event handler is being triggered
             // rapidly, such as an input event while typing. It only applies to asynchronous
@@ -1458,7 +1443,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
         private class NoOpRenderer : Renderer
         {
-            public NoOpRenderer() : base(new TestServiceProvider())
+            public NoOpRenderer() : base(new TestServiceProvider(), new RendererSynchronizationContext())
             {
             }
 
@@ -1491,7 +1476,20 @@ namespace Microsoft.AspNetCore.Components.Test
             }
 
             public void TriggerRender()
-                => _renderHandle.Render(_renderFragment);
+            {
+                var t = _renderHandle.Invoke(() => _renderHandle.Render(_renderFragment));
+                // This should always be run synchronously
+                Assert.True(t.IsCompleted);
+                if (t.IsFaulted)
+                {
+                    var exception = t.Exception.Flatten().InnerException;
+                    while (exception is AggregateException e)
+                    {
+                        exception = e.InnerException;
+                    }
+                    ExceptionDispatchInfo.Capture(exception).Throw();
+                }
+            }
 
             public bool Disposed { get; private set; }
 
@@ -1674,10 +1672,10 @@ namespace Microsoft.AspNetCore.Components.Test
             {
                 foreach (var renderHandle in _renderHandles)
                 {
-                    renderHandle.Render(builder =>
+                    renderHandle.Invoke(() => renderHandle.Render(builder =>
                     {
                         builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}");
-                    });
+                    }));
                 }
             }
         }

+ 69 - 37
src/Components/Components/test/Rendering/HtmlRendererTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Runtime.ExceptionServices;
 using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
@@ -19,16 +20,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderEmptyElement()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", ">", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
                 rtb.OpenElement(0, "p");
                 rtb.CloseElement();
             })).BuildServiceProvider();
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -38,6 +40,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderSimpleComponent()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", ">", "Hello world!", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -45,10 +48,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.AddContent(1, "Hello world!");
                 rtb.CloseElement();
             })).BuildServiceProvider();
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -58,6 +61,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_HtmlEncodesContent()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", ">", "&lt;Hello world!&gt;", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -65,10 +69,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.AddContent(1, "<Hello world!>");
                 rtb.CloseElement();
             })).BuildServiceProvider();
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -79,6 +83,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_DoesNotEncodeMarkup()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", ">", "<span>Hello world!</span>", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -86,10 +91,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.AddMarkupContent(1, "<span>Hello world!</span>");
                 rtb.CloseElement();
             })).BuildServiceProvider();
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -100,6 +105,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderWithAttributes()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "lead", "\"", ">", "Hello world!", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -109,10 +115,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -122,6 +128,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_HtmlEncodesAttributeValues()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "&lt;lead", "\"", ">", "Hello world!", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -131,10 +138,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -144,6 +151,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderBooleanAttributes()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "input", " ", "disabled", " />" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -152,10 +160,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -165,6 +173,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_DoesNotRenderBooleanAttributesWhenValueIsFalse()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "input", " />" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -173,10 +182,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -186,6 +195,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderWithChildren()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
             {
@@ -196,10 +206,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -209,6 +219,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderWithMultipleChildren()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] { "<", "p", ">",
                 "<", "span", ">", "Hello world!", "</", "span", ">",
                 "<", "span", ">", "Bye Bye world!", "</", "span", ">",
@@ -226,10 +237,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -239,6 +250,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderComponentWithChildrenComponents()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
                 "<", "span", ">", "Child content!", "</", "span", ">"
@@ -255,10 +267,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseComponent();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -268,6 +280,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_ComponentReferenceNoops()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">",
                 "<", "span", ">", "Child content!", "</", "span", ">"
@@ -285,10 +298,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseComponent();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -298,6 +311,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanPassParameters()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "</", "p", ">" };
 
@@ -315,16 +329,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 .AddSingleton(new Func<ParameterCollection, RenderFragment>(Content))
                 .BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
             Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
 
             // Act
-            var result = htmlRenderer.RenderComponent<ComponentWithParameters>(
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<ComponentWithParameters>(
                 new ParameterCollection(new[] {
                     RenderTreeFrame.Element(0,string.Empty),
                     RenderTreeFrame.Attribute(1,"update",change),
                     RenderTreeFrame.Attribute(2,"value",5)
-                }, 0));
+                }, 0))));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -334,6 +348,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_CanRenderComponentWithRenderFragmentContent()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
@@ -347,10 +362,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -360,6 +375,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public void RenderComponent_ElementRefsNoops()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "<", "span", ">", "Hello world!", "</", "span", ">", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
@@ -374,15 +390,29 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 rtb.CloseElement();
             })).BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty);
+            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
+        private IEnumerable<string> GetResult(Task<IEnumerable<string>> task)
+        {
+            Assert.True(task.IsCompleted);
+            if (task.IsCompletedSuccessfully)
+            {
+                return task.Result;
+            }
+            else
+            {
+                ExceptionDispatchInfo.Capture(task.Exception).Throw();
+                throw new InvalidOperationException("We will never hit this line");
+            }
+        }
+
         private class ComponentWithParameters : IComponent
         {
             public RenderHandle RenderHandle { get; private set; }
@@ -406,17 +436,18 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public async Task CanRender_AsyncComponent()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "20", "</", "p", ">" };
             var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = await htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
+            var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 ["Value"] = 10
-            }));
+            })));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -426,6 +457,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         public async Task CanRender_NestedAsyncComponents()
         {
             // Arrange
+            var dispatcher = Renderer.CreateDefaultDispatcher();
             var expectedHtml = new[] {
                 "<", "p", ">", "20", "</", "p", ">",
                 "<", "p", ">", "80", "</", "p", ">"
@@ -433,14 +465,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
 
             var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
 
-            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = await htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
+            var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
             {
                 ["Nested"] = false,
                 ["Value"] = 10
-            }));
+            })));
 
             // Assert
             Assert.Equal(expectedHtml, result);

+ 27 - 29
src/Components/Server/test/Circuits/CircuitSynchronizationContextTest.cs → src/Components/Components/test/Rendering/RendererSynchronizationContextTests.cs

@@ -1,17 +1,15 @@
-// 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.Diagnostics;
 using System.Globalization;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Server.Circuits;
 using Xunit;
 
-namespace Microsoft.AspNetCore.Components.Server
+namespace Microsoft.AspNetCore.Components.Rendering
 {
-    public class CircuitSynchronizationContextTest
+    public class RendererSynchronizationContextTest
     {
         // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
         // failures.
@@ -21,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public void Post_CanRunSynchronously_WhenNotBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -39,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public void Post_CanRunSynchronously_WhenNotBusy_Exception()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // Act & Assert
             Assert.Throws<InvalidTimeZoneException>(() => context.Post((_) =>
@@ -52,7 +50,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Post_CanRunAsynchronously_WhenBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -92,7 +90,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Post_CanRunAsynchronously_CaptureExecutionContext()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // CultureInfo uses the execution context.
             CultureInfo.CurrentCulture = new CultureInfo("en-GB");
@@ -147,7 +145,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Post_CanRunAsynchronously_WhenBusy_Exception()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             Exception exception = null;
             context.UnhandledException += (sender, e) =>
@@ -189,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Post_BackgroundWorkItem_CanProcessMoreItemsInline()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             Thread capturedThread = null;
 
             var e1 = new ManualResetEventSlim();
@@ -251,7 +249,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public void Post_CapturesContext()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             var e1 = new ManualResetEventSlim();
 
@@ -281,7 +279,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public void Send_CanRunSynchronously()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -299,7 +297,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public void Send_CanRunSynchronously_Exception()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // Act & Assert
             Assert.Throws<InvalidTimeZoneException>(() => context.Send((_) =>
@@ -312,7 +310,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Send_BlocksWhenOtherWorkRunning()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             var e1 = new ManualResetEventSlim();
             var e2 = new ManualResetEventSlim();
@@ -359,7 +357,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public void Send_CapturesContext()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             var e1 = new ManualResetEventSlim();
 
@@ -390,7 +388,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Invoke_Void_CanRunSynchronously_WhenNotBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -409,7 +407,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Invoke_Void_CanRunAsynchronously_WhenBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -449,7 +447,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Invoke_Void_CanRethrowExceptions()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // Act
             var task = context.Invoke(() =>
@@ -465,7 +463,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Invoke_T_CanRunSynchronously_WhenNotBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
 
             // Act
@@ -482,7 +480,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Invoke_T_CanRunAsynchronously_WhenBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
 
             var e1 = new ManualResetEventSlim();
@@ -520,7 +518,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task Invoke_T_CanRethrowExceptions()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // Act
             var task = context.Invoke<string>(() =>
@@ -536,7 +534,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task InvokeAsync_Void_CanRunSynchronously_WhenNotBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -556,7 +554,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task InvokeAsync_Void_CanRunAsynchronously_WhenBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
             Thread capturedThread = null;
 
@@ -597,7 +595,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task InvokeAsync_Void_CanRethrowExceptions()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // Act
             var task = context.InvokeAsync(() =>
@@ -613,7 +611,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task InvokeAsync_T_CanRunSynchronously_WhenNotBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
 
             // Act
@@ -630,7 +628,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task InvokeAsync_T_CanRunAsynchronously_WhenBusy()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
             var thread = Thread.CurrentThread;
 
             var e1 = new ManualResetEventSlim();
@@ -668,7 +666,7 @@ namespace Microsoft.AspNetCore.Components.Server
         public async Task InvokeAsync_T_CanRethrowExceptions()
         {
             // Arrange
-            var context = new CircuitSynchronizationContext();
+            var context = new RendererSynchronizationContext();
 
             // Act
             var task = context.InvokeAsync<string>(() =>

+ 4 - 9
src/Components/Server/src/Circuits/CircuitHost.cs

@@ -55,7 +55,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             RemoteRenderer renderer,
             Action<IComponentsApplicationBuilder> configure,
             IJSRuntime jsRuntime,
-            CircuitSynchronizationContext synchronizationContext,
             CircuitHandler[] circuitHandlers)
         {
             _scope = scope ?? throw new ArgumentNullException(nameof(scope));
@@ -64,7 +63,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
             _configure = configure ?? throw new ArgumentNullException(nameof(configure));
             JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
-            SynchronizationContext = synchronizationContext ?? throw new ArgumentNullException(nameof(synchronizationContext));
 
             Services = scope.ServiceProvider;
 
@@ -72,7 +70,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             _circuitHandlers = circuitHandlers;
 
             Renderer.UnhandledException += Renderer_UnhandledException;
-            SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException;
+            Renderer.UnhandledSynchronizationException += SynchronizationContext_UnhandledException;
         }
 
         public string CircuitId { get; } = Guid.NewGuid().ToString();
@@ -89,11 +87,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
         public IServiceProvider Services { get; }
 
-        public CircuitSynchronizationContext SynchronizationContext { get; }
-
         public async Task InitializeAsync(CancellationToken cancellationToken)
         {
-            await SynchronizationContext.InvokeAsync(async () =>
+            await Renderer.InvokeAsync(async () =>
             {
                 SetCurrentCircuitHost(this);
 
@@ -127,10 +123,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
             try
             {
-                await SynchronizationContext.Invoke(() =>
+                await Renderer.Invoke(() =>
                 {
                     SetCurrentCircuitHost(this);
-
                     DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
                 });
             }
@@ -142,7 +137,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
         public async ValueTask DisposeAsync()
         {
-            await SynchronizationContext.InvokeAsync(async () =>
+            await Renderer.InvokeAsync(async () =>
             {
                 for (var i = 0; i < _circuitHandlers.Length; i++)
                 {

+ 8 - 3
src/Components/Server/src/Circuits/DefaultCircuitFactory.cs

@@ -5,6 +5,7 @@ using System;
 using System.Linq;
 using Microsoft.AspNetCore.Components.Browser;
 using Microsoft.AspNetCore.Components.Browser.Rendering;
+using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.SignalR;
 using Microsoft.Extensions.DependencyInjection;
@@ -41,8 +42,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             var scope = _scopeFactory.CreateScope();
             var jsRuntime = new RemoteJSRuntime(client);
             var rendererRegistry = new RendererRegistry();
-            var synchronizationContext = new CircuitSynchronizationContext();
-            var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);
+            var dispatcher = Renderer.CreateDefaultDispatcher();
+            var renderer = new RemoteRenderer(
+                scope.ServiceProvider,
+                rendererRegistry,
+                jsRuntime,
+                client,
+                dispatcher);
 
             var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
                 .OrderBy(h => h.Order)
@@ -55,7 +61,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 renderer,
                 config,
                 jsRuntime,
-                synchronizationContext,
                 circuitHandlers);
 
             // Initialize per-circuit data that services need

+ 3 - 52
src/Components/Server/src/Circuits/RemoteRenderer.cs

@@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
         private readonly IClientProxy _client;
         private readonly IJSRuntime _jsRuntime;
         private readonly RendererRegistry _rendererRegistry;
-        private readonly SynchronizationContext _syncContext;
         private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
             = new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
         private long _nextRenderId = 1;
@@ -46,13 +45,12 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
             RendererRegistry rendererRegistry,
             IJSRuntime jsRuntime,
             IClientProxy client,
-            SynchronizationContext syncContext)
-            : base(serviceProvider)
+            IDispatcher dispatcher)
+            : base(serviceProvider, dispatcher)
         {
             _rendererRegistry = rendererRegistry;
             _jsRuntime = jsRuntime;
             _client = client;
-            _syncContext = syncContext ?? throw new ArgumentNullException(nameof(syncContext));
 
             _id = _rendererRegistry.Add(this);
         }
@@ -64,7 +62,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
         /// <typeparam name="TComponent">The type of the component.</typeparam>
         /// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
         public void AddComponent<TComponent>(string domElementSelector)
-            where TComponent: IComponent
+            where TComponent : IComponent
         {
             AddComponent(typeof(TComponent), domElementSelector);
         }
@@ -90,36 +88,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
             RenderRootComponent(componentId);
         }
 
-        /// <inheritdoc />
-        public override Task Invoke(Action workItem)
-        {
-            if (SynchronizationContext.Current == _syncContext)
-            {
-                // No need to dispatch. Avoid deadlock by invoking directly.
-                return base.Invoke(workItem);
-            }
-            else
-            {
-                var syncContext = (CircuitSynchronizationContext)_syncContext;
-                return syncContext.Invoke(workItem);
-            }
-        }
-
-        /// <inheritdoc />
-        public override Task InvokeAsync(Func<Task> workItem)
-        {
-            if (SynchronizationContext.Current == _syncContext)
-            {
-                // No need to dispatch. Avoid deadlock by invoking directly.
-                return base.InvokeAsync(workItem);
-            }
-            else
-            {
-                var syncContext = (CircuitSynchronizationContext)_syncContext;
-                return syncContext.InvokeAsync(workItem);
-            }
-        }
-
         /// <inheritdoc />
         protected override void Dispose(bool disposing)
         {
@@ -127,23 +95,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
             _rendererRegistry.TryRemove(_id);
         }
 
-        protected override void AddToRenderQueue(int componentId, RenderFragment renderFragment)
-        {
-            // Render operations are not thread-safe, so they need to be serialized.
-            // Plus, any other logic that mutates state accessed during rendering also
-            // needs not to run concurrently with rendering so should be dispatched to
-            // the renderer's sync context.
-            if (SynchronizationContext.Current != _syncContext)
-            {
-                throw new RemoteRendererException(
-                    "The current thread is not associated with the renderer's synchronization context. " +
-                    "Use Invoke() or InvokeAsync() to switch execution to the renderer's synchronization " +
-                    "context when triggering rendering or modifying any state accessed during rendering.");
-            }
-
-            base.AddToRenderQueue(componentId, renderFragment);
-        }
-
         /// <inheritdoc />
         protected override Task UpdateDisplayAsync(in RenderBatch batch)
         {

+ 3 - 7
src/Components/Server/test/Circuits/CircuitHostTest.cs

@@ -129,7 +129,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             var clientProxy = Mock.Of<IClientProxy>();
             var renderRegistry = new RendererRegistry();
             var jsRuntime = Mock.Of<IJSRuntime>();
-            var syncContext = new CircuitSynchronizationContext();
 
             remoteRenderer = remoteRenderer ?? GetRemoteRenderer();
             handlers = handlers ?? Array.Empty<CircuitHandler>();
@@ -141,8 +140,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 remoteRenderer,
                 configure: _ => { },
                 jsRuntime: jsRuntime,
-                synchronizationContext:
-                syncContext,
                 handlers);
         }
 
@@ -152,14 +149,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 Mock.Of<IServiceProvider>(),
                 new RendererRegistry(),
                 Mock.Of<IJSRuntime>(),
-                Mock.Of<IClientProxy>(),
-                new CircuitSynchronizationContext());
+                Mock.Of<IClientProxy>());
         }
 
         private class TestRemoteRenderer : RemoteRenderer
         {
-            public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client, SynchronizationContext syncContext)
-                : base(serviceProvider, rendererRegistry, jsRuntime, client, syncContext)
+            public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
+                : base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher())
             {
             }
 

+ 1 - 1
src/Components/Server/test/Circuits/RenderBatchWriterTest.cs

@@ -373,7 +373,7 @@ namespace Microsoft.AspNetCore.Components.Server
         class FakeRenderer : Renderer
         {
             public FakeRenderer()
-                : base(new ServiceCollection().BuildServiceProvider())
+                : base(new ServiceCollection().BuildServiceProvider(), new RendererSynchronizationContext())
             {
             }
 

+ 18 - 1
src/Components/Shared/test/AutoRenderComponent.cs

@@ -1,9 +1,12 @@
 // 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.Runtime.ExceptionServices;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
+using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test.Helpers
 {
@@ -23,8 +26,22 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
             return Task.CompletedTask;
         }
 
+        // We do it this way so that we don't have to be doing renderer.Invoke on each and every test.
         public void TriggerRender()
-            => _renderHandle.Render(BuildRenderTree);
+        {
+            var t = _renderHandle.Invoke(() => _renderHandle.Render(BuildRenderTree));
+            // This should always be run synchronously
+            Assert.True(t.IsCompleted);
+            if (t.IsFaulted)
+            {
+                var exception = t.Exception.Flatten().InnerException;
+                while (exception is AggregateException e)
+                {
+                    exception = e.InnerException;
+                }
+                ExceptionDispatchInfo.Capture(exception).Throw();
+            }
+        }
 
         protected abstract void BuildRenderTree(RenderTreeBuilder builder);
     }

+ 25 - 6
src/Components/Shared/test/TestRenderer.cs

@@ -4,19 +4,25 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.ExceptionServices;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Rendering;
+using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test.Helpers
 {
     public class TestRenderer : Renderer
     {
-        public TestRenderer(): this(new TestServiceProvider())
+        public TestRenderer() : this(new TestServiceProvider())
         {
         }
 
-        public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider)
+        public TestRenderer(IDispatcher dispatcher) : base(new TestServiceProvider(), dispatcher)
+        {
+        }
+
+        public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, new RendererSynchronizationContext())
         {
         }
 
@@ -29,16 +35,29 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
             => base.AssignRootComponentId(component);
 
         public new void RenderRootComponent(int componentId)
-            => base.RenderRootComponent(componentId);
+            => Invoke(() => base.RenderRootComponent(componentId));
 
         public new Task RenderRootComponentAsync(int componentId)
-            => base.RenderRootComponentAsync(componentId);
+            => InvokeAsync(() => base.RenderRootComponentAsync(componentId));
 
         public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
-            => base.RenderRootComponentAsync(componentId, parameters);
+            => InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
 
         public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
-            => base.DispatchEvent(componentId, eventHandlerId, args);
+        {
+            var t = Invoke(() => base.DispatchEvent(componentId, eventHandlerId, args));
+            // This should always be run synchronously
+            Assert.True(t.IsCompleted);
+            if (t.IsFaulted)
+            {
+                var exception = t.Exception.Flatten().InnerException;
+                while (exception is AggregateException e)
+                {
+                    exception = e.InnerException;
+                }
+                ExceptionDispatchInfo.Capture(exception).Throw();
+            }
+        }
 
         public T InstantiateComponent<T>() where T : IComponent
             => (T)InstantiateComponent(typeof(T));

+ 2 - 1
src/Components/test/E2ETest/ServerExecutionTests/ServerComponentRenderingTest.cs

@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
 using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
 using Microsoft.AspNetCore.Components.E2ETest.Tests;
 using OpenQA.Selenium;
+using System;
 using System.Threading.Tasks;
 using Xunit;
 using Xunit.Abstractions;
@@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
             appElement.FindElement(By.Id("run-without-dispatch")).Click();
 
             WaitAssert.Contains(
-                $"{typeof(RemoteRendererException).FullName}: The current thread is not associated with the renderer's synchronization context",
+                $"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the renderer's synchronization context",
                 () => result.Text);
         }
     }

+ 4 - 3
src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/HtmlHelperComponentExtensions.cs

@@ -51,12 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
 
             var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices;
             var encoder = serviceProvider.GetRequiredService<HtmlEncoder>();
-            using (var htmlRenderer = new HtmlRenderer(serviceProvider, encoder.Encode))
+            var dispatcher = Renderer.CreateDefaultDispatcher();
+            using (var htmlRenderer = new HtmlRenderer(serviceProvider, encoder.Encode, dispatcher))
             {
-                var result = await htmlRenderer.RenderComponentAsync<TComponent>(
+                var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TComponent>(
                     parameters == null ?
                         ParameterCollection.Empty :
-                        ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)));
+                        ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))));
 
                 return new ComponentHtmlContent(result);
             }

Some files were not shown because too many files changed in this diff