[Components] [Fixes #6953, #7226]

* Moves the Synchronization context from the remote renderer to the base renderer.
* Removes all the locking from the base renderer.
This commit is contained in:
Javier Calvarro Nelson 2019-02-07 07:03:52 -08:00 committed by GitHub
parent 2d73f7ad2b
commit f456e3d153
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1113 additions and 988 deletions

View File

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

View File

@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{ {
var renderer = new TestRenderer(); var renderer = new TestRenderer();
renderer.AttachComponent(component); 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. // 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)); Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
if (task.IsFaulted) if (task.IsFaulted)
@ -434,7 +434,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
private class TestRenderer : Renderer private class TestRenderer : Renderer
{ {
public TestRenderer() : base(new TestServiceProvider()) public TestRenderer() : base(new TestServiceProvider(), CreateDefaultDispatcher())
{ {
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
@ -49,7 +49,13 @@ namespace Microsoft.AspNetCore.Components
/// </summary> /// </summary>
/// <param name="workItem">The work item to execute.</param> /// <param name="workItem">The work item to execute.</param>
public Task Invoke(Action workItem) 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> /// <summary>
/// Executes the supplied work item on the renderer's /// Executes the supplied work item on the renderer's
@ -57,6 +63,12 @@ namespace Microsoft.AspNetCore.Components
/// </summary> /// </summary>
/// <param name="workItem">The work item to execute.</param> /// <param name="workItem">The work item to execute.</param>
public Task InvokeAsync(Func<Task> workItem) 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);
}
} }
} }

View File

@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary> /// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param> /// <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> /// <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; _htmlEncoder = htmlEncoder;
} }

View File

@ -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);
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree; 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 Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder(); private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
private readonly Dictionary<int, EventHandlerInvoker> _eventBindings = new Dictionary<int, EventHandlerInvoker>(); 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 int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
private bool _isBatchInProgress; private bool _isBatchInProgress;
private int _lastEventHandlerId = 0; private int _lastEventHandlerId = 0;
private List<Task> _pendingTasks; private List<Task> _pendingTasks;
// We need to introduce locking as we don't know if we are executing /// <summary>
// under a synchronization context that limits the ammount of concurrency /// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
// that can happen when async callbacks are executed. /// </summary>
// As a result, we have to protect the _pendingTask list and the public event UnhandledExceptionEventHandler UnhandledSynchronizationException
// _batchBuilder render queue from concurrent modifications. {
private object _asyncWorkLock = new object(); add
{
if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
{
return;
}
rendererSynchronizationContext.UnhandledException += value;
}
remove
{
if (!(_dispatcher is RendererSynchronizationContext rendererSynchronizationContext))
{
return;
}
rendererSynchronizationContext.UnhandledException -= value;
}
}
/// <summary> /// <summary>
/// Constructs an instance of <see cref="Renderer"/>. /// Constructs an instance of <see cref="Renderer"/>.
/// </summary> /// </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) public Renderer(IServiceProvider serviceProvider)
{ {
_componentFactory = new ComponentFactory(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> /// <summary>
/// Constructs a new component of the specified type. /// Constructs a new component of the specified type.
/// </summary> /// </summary>
@ -198,14 +232,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
while (_pendingTasks.Count > 0) while (_pendingTasks.Count > 0)
{ {
Task pendingWork; 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. // Clear all pending work.
_pendingTasks.Clear(); _pendingTasks.Clear();
}
// new work might be added before we check again as a result of waiting for all // new work might be added before we check again as a result of waiting for all
// the child components to finish executing SetParametersAsync // 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> /// <param name="eventArgs">Arguments to be passed to the event handler.</param>
public void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs) public void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
{ {
EnsureSynchronizationContext();
if (_eventBindings.TryGetValue(eventHandlerId, out var binding)) if (_eventBindings.TryGetValue(eventHandlerId, out var binding))
{ {
// The event handler might request multiple renders in sequence. Capture them // 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> /// <param name="workItem">The work item to execute.</param>
public virtual Task Invoke(Action workItem) public virtual Task Invoke(Action workItem)
{ {
// Base renderer has nothing to dispatch to, so execute directly // This is for example when we run on a system with a single thread, like WebAssembly.
workItem(); if (_dispatcher == null)
return Task.CompletedTask; {
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> /// <summary>
@ -278,8 +326,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="workItem">The work item to execute.</param> /// <param name="workItem">The work item to execute.</param>
public virtual Task InvokeAsync(Func<Task> workItem) public virtual Task InvokeAsync(Func<Task> workItem)
{ {
// Base renderer has nothing to dispatch to, so execute directly // This is for example when we run on a system with a single thread, like WebAssembly.
return workItem(); 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) internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
@ -323,10 +386,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
{ {
return; return;
} }
lock (_asyncWorkLock) _pendingTasks.Add(task);
{
_pendingTasks.Add(task);
}
break; 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> /// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment) protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment)
{ {
EnsureSynchronizationContext();
var componentState = GetOptionalComponentState(componentId); var componentState = GetOptionalComponentState(componentId);
if (componentState == null) if (componentState == null)
{ {
@ -359,11 +421,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
return; return;
} }
lock (_asyncWorkLock) _batchBuilder.ComponentRenderQueue.Enqueue(
{ new RenderQueueEntry(componentState, renderFragment));
_batchBuilder.ComponentRenderQueue.Enqueue(
new RenderQueueEntry(componentState, renderFragment));
}
if (!_isBatchInProgress) 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) private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState) => _componentStateById.TryGetValue(componentId, out var componentState)
? componentState ? componentState
@ -389,8 +464,9 @@ namespace Microsoft.AspNetCore.Components.Rendering
try try
{ {
// Process render queue until empty // Process render queue until empty
while (TryDequeueRenderQueueEntry(out var nextToRender)) while (_batchBuilder.ComponentRenderQueue.Count > 0)
{ {
var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue();
RenderInExistingBatch(nextToRender); 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) private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
{ {
var array = updatedComponents.Array; var array = updatedComponents.Array;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
@ -15,6 +16,10 @@ namespace Microsoft.AspNetCore.Components.Test
{ {
public class RendererTest 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] [Fact]
public void CanRenderTopLevelComponents() public void CanRenderTopLevelComponents()
{ {
@ -171,7 +176,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act // Act
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
await renderer.RenderRootComponentAsync(componentId); await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
// Assert // Assert
Assert.Equal(5, renderer.Batches.Count); Assert.Equal(5, renderer.Batches.Count);
@ -221,9 +226,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert // Act/Assert
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>(); 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> [0] = new List<NestedAsyncComponent.ExecutionAction>
{ {
@ -240,13 +245,13 @@ namespace Microsoft.AspNetCore.Components.Test
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true), 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 }), [0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>()) [1] = CreateRenderFactory(Array.Empty<int>())
}, },
[nameof(NestedAsyncComponent.Log)] = log [LogName] = log
})); })));
var logForParent = log.Where(l => l.id == 0).ToArray(); var logForParent = log.Where(l => l.id == 0).ToArray();
var logForChild = log.Where(l => l.id == 1).ToArray(); var logForChild = log.Where(l => l.id == 1).ToArray();
@ -265,9 +270,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert // Act/Assert
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>(); 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> [0] = new List<NestedAsyncComponent.ExecutionAction>
{ {
@ -284,13 +289,13 @@ namespace Microsoft.AspNetCore.Components.Test
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync), 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 }), [0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>()) [1] = CreateRenderFactory(Array.Empty<int>())
}, },
[nameof(NestedAsyncComponent.Log)] = log [LogName] = log
})); })));
var logForParent = log.Where(l => l.id == 0).ToArray(); var logForParent = log.Where(l => l.id == 0).ToArray();
var logForChild = log.Where(l => l.id == 1).ToArray(); var logForChild = log.Where(l => l.id == 1).ToArray();
@ -309,9 +314,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert // Act/Assert
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>(); 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> [0] = new List<NestedAsyncComponent.ExecutionAction>
{ {
@ -328,13 +333,13 @@ namespace Microsoft.AspNetCore.Components.Test
NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync), 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 }), [0] = CreateRenderFactory(new[] { 1 }),
[1] = CreateRenderFactory(Array.Empty<int>()) [1] = CreateRenderFactory(Array.Empty<int>())
}, },
[nameof(NestedAsyncComponent.Log)] = log [LogName] = log
})); })));
var logForParent = log.Where(l => l.id == 0).ToArray(); var logForParent = log.Where(l => l.id == 0).ToArray();
var logForChild = log.Where(l => l.id == 1).ToArray(); var logForChild = log.Where(l => l.id == 1).ToArray();
@ -353,9 +358,9 @@ namespace Microsoft.AspNetCore.Components.Test
// Act/Assert // Act/Assert
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>(); 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> [0] = new List<NestedAsyncComponent.ExecutionAction>
{ {
@ -386,15 +391,15 @@ namespace Microsoft.AspNetCore.Components.Test
NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true), 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 }), [0] = CreateRenderFactory(new[] { 1, 2 }),
[1] = CreateRenderFactory(new[] { 3 }), [1] = CreateRenderFactory(new[] { 3 }),
[2] = CreateRenderFactory(Array.Empty<int>()), [2] = CreateRenderFactory(Array.Empty<int>()),
[3] = 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 logForParent = log.Where(l => l.id == 0).ToArray();
var logForFirstChild = log.Where(l => l.id == 1).ToArray(); var logForFirstChild = log.Where(l => l.id == 1).ToArray();
@ -553,10 +558,7 @@ namespace Microsoft.AspNetCore.Components.Test
var eventArgs = new UIEventArgs(); var eventArgs = new UIEventArgs();
// Act/Assert // Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => var ex = Assert.Throws<InvalidOperationException>(() => renderer.DispatchEvent(componentId, eventHandlerId, eventArgs));
{
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
});
Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " + Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " +
$"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message); $"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message);
} }
@ -568,10 +570,7 @@ namespace Microsoft.AspNetCore.Components.Test
var renderer = new TestRenderer(); var renderer = new TestRenderer();
// Act/Assert // Act/Assert
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(123, 0, new UIEventArgs()));
{
renderer.DispatchEvent(123, 0, new UIEventArgs());
});
} }
[Fact] [Fact]
@ -766,8 +765,9 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs); Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs);
// Act/Assert: If a disposed component requests a render, it's a no-op // Act/Assert: If a disposed component requests a render, it's a no-op
((FakeComponent)childComponent3).RenderHandle.Render(builder var renderHandle = ((FakeComponent)childComponent3).RenderHandle;
=> throw new NotImplementedException("Should not be invoked")); renderHandle.Invoke(() => renderHandle.Render(builder
=> throw new NotImplementedException("Should not be invoked")));
Assert.Equal(2, renderer.Batches.Count); Assert.Equal(2, renderer.Batches.Count);
} }
@ -798,10 +798,7 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender(); component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event, but can fire the new event // Act/Assert 2: Can no longer fire the original event, but can fire the new event
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
{
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount); Assert.Equal(1, eventCount);
Assert.Equal(0, newEventCount); Assert.Equal(0, newEventCount);
renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null); renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null);
@ -834,10 +831,7 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender(); component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event // Act/Assert 2: Can no longer fire the original event
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
{
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount); Assert.Equal(1, eventCount);
} }
@ -883,10 +877,7 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender(); component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event // Act/Assert 2: Can no longer fire the original event
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null));
{
renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null);
});
Assert.Equal(1, eventCount); Assert.Equal(1, eventCount);
} }
@ -916,10 +907,7 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender(); component.TriggerRender();
// Act/Assert 2: Can no longer fire the original event // Act/Assert 2: Can no longer fire the original event
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(componentId, origEventHandlerId, args: null));
{
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
});
Assert.Equal(1, eventCount); Assert.Equal(1, eventCount);
} }
@ -1009,10 +997,7 @@ namespace Microsoft.AspNetCore.Components.Test
var component = new TestComponent(builder => { }); var component = new TestComponent(builder => { });
// Act/Assert // Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
{
component.TriggerRender();
});
Assert.Equal("The render handle is not yet assigned.", ex.Message); Assert.Equal("The render handle is not yet assigned.", ex.Message);
} }
@ -1317,7 +1302,7 @@ namespace Microsoft.AspNetCore.Components.Test
} }
[Fact] [Fact]
public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatch() public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync()
{ {
// This represents the scenario where the same event handler is being triggered // 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 // 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 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() 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; } public bool Disposed { get; private set; }
@ -1674,10 +1672,10 @@ namespace Microsoft.AspNetCore.Components.Test
{ {
foreach (var renderHandle in _renderHandles) foreach (var renderHandle in _renderHandles)
{ {
renderHandle.Render(builder => renderHandle.Invoke(() => renderHandle.Render(builder =>
{ {
builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}"); builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}");
}); }));
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
private readonly IClientProxy _client; private readonly IClientProxy _client;
private readonly IJSRuntime _jsRuntime; private readonly IJSRuntime _jsRuntime;
private readonly RendererRegistry _rendererRegistry; private readonly RendererRegistry _rendererRegistry;
private readonly SynchronizationContext _syncContext;
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>(); = new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
private long _nextRenderId = 1; private long _nextRenderId = 1;
@ -46,13 +45,12 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
RendererRegistry rendererRegistry, RendererRegistry rendererRegistry,
IJSRuntime jsRuntime, IJSRuntime jsRuntime,
IClientProxy client, IClientProxy client,
SynchronizationContext syncContext) IDispatcher dispatcher)
: base(serviceProvider) : base(serviceProvider, dispatcher)
{ {
_rendererRegistry = rendererRegistry; _rendererRegistry = rendererRegistry;
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
_client = client; _client = client;
_syncContext = syncContext ?? throw new ArgumentNullException(nameof(syncContext));
_id = _rendererRegistry.Add(this); _id = _rendererRegistry.Add(this);
} }
@ -64,7 +62,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
/// <typeparam name="TComponent">The type of the component.</typeparam> /// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param> /// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public void AddComponent<TComponent>(string domElementSelector) public void AddComponent<TComponent>(string domElementSelector)
where TComponent: IComponent where TComponent : IComponent
{ {
AddComponent(typeof(TComponent), domElementSelector); AddComponent(typeof(TComponent), domElementSelector);
} }
@ -90,36 +88,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
RenderRootComponent(componentId); 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 /> /// <inheritdoc />
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
@ -127,23 +95,6 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
_rendererRegistry.TryRemove(_id); _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 /> /// <inheritdoc />
protected override Task UpdateDisplayAsync(in RenderBatch batch) protected override Task UpdateDisplayAsync(in RenderBatch batch)
{ {

View File

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

View File

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

View File

@ -1,9 +1,12 @@
// 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. // 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test.Helpers namespace Microsoft.AspNetCore.Components.Test.Helpers
{ {
@ -23,8 +26,22 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
return Task.CompletedTask; 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() 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); protected abstract void BuildRenderTree(RenderTreeBuilder builder);
} }

View File

@ -4,19 +4,25 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test.Helpers namespace Microsoft.AspNetCore.Components.Test.Helpers
{ {
public class TestRenderer : Renderer 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); => base.AssignRootComponentId(component);
public new void RenderRootComponent(int componentId) public new void RenderRootComponent(int componentId)
=> base.RenderRootComponent(componentId); => Invoke(() => base.RenderRootComponent(componentId));
public new Task RenderRootComponentAsync(int componentId) public new Task RenderRootComponentAsync(int componentId)
=> base.RenderRootComponentAsync(componentId); => InvokeAsync(() => base.RenderRootComponentAsync(componentId));
public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters) 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) 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 public T InstantiateComponent<T>() where T : IComponent
=> (T)InstantiateComponent(typeof(T)); => (T)InstantiateComponent(typeof(T));

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.E2ETest.Tests; using Microsoft.AspNetCore.Components.E2ETest.Tests;
using OpenQA.Selenium; using OpenQA.Selenium;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
appElement.FindElement(By.Id("run-without-dispatch")).Click(); appElement.FindElement(By.Id("run-without-dispatch")).Click();
WaitAssert.Contains( 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); () => result.Text);
} }
} }

View File

@ -51,12 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices; var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices;
var encoder = serviceProvider.GetRequiredService<HtmlEncoder>(); 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 ? parameters == null ?
ParameterCollection.Empty : ParameterCollection.Empty :
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))); ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))));
return new ComponentHtmlContent(result); return new ComponentHtmlContent(result);
} }