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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Components.Performance
private class FakeRenderer : Renderer
{
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.
using Microsoft.AspNetCore.Components.Rendering;
@ -49,7 +49,13 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
/// <param name="workItem">The work item to execute.</param>
public Task Invoke(Action workItem)
=> _renderer.Invoke(workItem);
{
if (_renderer == null)
{
throw new InvalidOperationException("The render handle is not yet assigned.");
}
return _renderer.Invoke(workItem);
}
/// <summary>
/// Executes the supplied work item on the renderer's
@ -57,6 +63,12 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
/// <param name="workItem">The work item to execute.</param>
public Task InvokeAsync(Func<Task> workItem)
=> _renderer.InvokeAsync(workItem);
{
if (_renderer == null)
{
throw new InvalidOperationException("The render handle is not yet assigned.");
}
return _renderer.InvokeAsync(workItem);
}
}
}

View File

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

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -373,7 +373,7 @@ namespace Microsoft.AspNetCore.Components.Server
class FakeRenderer : Renderer
{
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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Xunit;
namespace Microsoft.AspNetCore.Components.Test.Helpers
{
@ -23,8 +26,22 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
return Task.CompletedTask;
}
// We do it this way so that we don't have to be doing renderer.Invoke on each and every test.
public void TriggerRender()
=> _renderHandle.Render(BuildRenderTree);
{
var t = _renderHandle.Invoke(() => _renderHandle.Render(BuildRenderTree));
// This should always be run synchronously
Assert.True(t.IsCompleted);
if (t.IsFaulted)
{
var exception = t.Exception.Flatten().InnerException;
while (exception is AggregateException e)
{
exception = e.InnerException;
}
ExceptionDispatchInfo.Capture(exception).Throw();
}
}
protected abstract void BuildRenderTree(RenderTreeBuilder builder);
}

View File

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

View File

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

View File

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