Fix RendererSyncContext.Post() (#10451)

* Fix RendererSyncContext.Post()

Fixes: #9683 - SignalR connection breaks on large DOM

The root cause here is a misbehaving sync context. It's not legal for a
Post() implementation to run a callback synchronously. We want that
behavior for most of the functionality Blazor calls directly, but Post()
should always go async or else various threading primitives are broken.

* Fix incorrect tests

These tests have the assumption that setting the result of a TCS will
execution continuations synchronously. This was a bug in our
SyncContext, and these tests needed updating to be more resiliant.

* Remove a delegate allocation
This commit is contained in:
Ryan Nowak 2019-05-23 10:39:53 -07:00 committed by GitHub
parent a6dce7b33c
commit 2948c81aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 196 additions and 50 deletions

View File

@ -39,12 +39,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
public Task Invoke(Action action) public Task Invoke(Action action)
{ {
var completion = new TaskCompletionSource<object>(); var completion = new RendererSynchronizationTaskCompletionSource<Action, object>(action);
Post(_ => ExecuteSynchronouslyIfPossible((state) =>
{ {
var completion = (RendererSynchronizationTaskCompletionSource<Action, object>)state;
try try
{ {
action(); completion.Callback();
completion.SetResult(null); completion.SetResult(null);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -55,19 +56,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
{ {
completion.SetException(exception); completion.SetException(exception);
} }
}, null); }, completion);
return completion.Task; return completion.Task;
} }
public Task InvokeAsync(Func<Task> asyncAction) public Task InvokeAsync(Func<Task> asyncAction)
{ {
var completion = new TaskCompletionSource<object>(); var completion = new RendererSynchronizationTaskCompletionSource<Func<Task>, object>(asyncAction);
Post(async (_) => ExecuteSynchronouslyIfPossible(async (state) =>
{ {
var completion = (RendererSynchronizationTaskCompletionSource<Func<Task>, object>)state;
try try
{ {
await asyncAction(); await completion.Callback();
completion.SetResult(null); completion.SetResult(null);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -78,19 +80,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
{ {
completion.SetException(exception); completion.SetException(exception);
} }
}, null); }, completion);
return completion.Task; return completion.Task;
} }
public Task<TResult> Invoke<TResult>(Func<TResult> function) public Task<TResult> Invoke<TResult>(Func<TResult> function)
{ {
var completion = new TaskCompletionSource<TResult>(); var completion = new RendererSynchronizationTaskCompletionSource<Func<TResult>, TResult>(function);
Post(_ => ExecuteSynchronouslyIfPossible((state) =>
{ {
var completion = (RendererSynchronizationTaskCompletionSource<Func<TResult>, TResult>)state;
try try
{ {
var result = function(); var result = completion.Callback();
completion.SetResult(result); completion.SetResult(result);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -101,19 +104,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
{ {
completion.SetException(exception); completion.SetException(exception);
} }
}, null); }, completion);
return completion.Task; return completion.Task;
} }
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> asyncFunction) public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> asyncFunction)
{ {
var completion = new TaskCompletionSource<TResult>(); var completion = new RendererSynchronizationTaskCompletionSource<Func<Task<TResult>>, TResult>(asyncFunction);
Post(async (_) => ExecuteSynchronouslyIfPossible(async (state) =>
{ {
var completion = (RendererSynchronizationTaskCompletionSource<Func<Task<TResult>>, TResult>)state;
try try
{ {
var result = await asyncFunction(); var result = await completion.Callback();
completion.SetResult(result); completion.SetResult(result);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -124,30 +128,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
{ {
completion.SetException(exception); completion.SetException(exception);
} }
}, null); }, completion);
return completion.Task; return completion.Task;
} }
// asynchronously runs the callback // asynchronously runs the callback
//
// NOTE: this must always run async. It's not legal here to execute the work item synchronously.
public override void Post(SendOrPostCallback d, object state) public override void Post(SendOrPostCallback d, object state)
{ {
TaskCompletionSource<object> completion;
lock (_state.Lock) lock (_state.Lock)
{ {
if (!_state.Task.IsCompleted) _state.Task = Enqueue(_state.Task, d, state, forceAsync: true);
{
_state.Task = Enqueue(_state.Task, d, state);
return;
}
// We can execute this synchronously because nothing is currently running
// or queued.
completion = new TaskCompletionSource<object>();
_state.Task = completion.Task;
} }
ExecuteSynchronously(completion, d, state);
} }
// synchronously runs the callback // synchronously runs the callback
@ -177,10 +171,33 @@ namespace Microsoft.AspNetCore.Components.Rendering
return new RendererSynchronizationContext(_state); return new RendererSynchronizationContext(_state);
} }
private Task Enqueue(Task antecedant, SendOrPostCallback d, object state) // Similar to Post, but it can runs the work item synchronously if the context is not busy.
//
// This is the main code path used by components, we want to be able to run async work but only dispatch
// if necessary.
private void ExecuteSynchronouslyIfPossible(SendOrPostCallback d, object state)
{ {
// If we get here is means that a callback is being queued while something is currently executing TaskCompletionSource<object> completion;
// in this context. Let's instead add it to the queue and yield. lock (_state.Lock)
{
if (!_state.Task.IsCompleted)
{
_state.Task = Enqueue(_state.Task, d, state);
return;
}
// We can execute this synchronously because nothing is currently running
// or queued.
completion = new TaskCompletionSource<object>();
_state.Task = completion.Task;
}
ExecuteSynchronously(completion, d, state);
}
private Task Enqueue(Task antecedant, SendOrPostCallback d, object state, bool forceAsync = false)
{
// If we get here is means that a callback is being explicitly queued. Let's instead add it to the queue and yield.
// //
// We use our own queue here to maintain the execution order of the callbacks scheduled here. Also // We use our own queue here to maintain the execution order of the callbacks scheduled here. Also
// we need a queue rather than just scheduling an item in the thread pool - those items would immediately // we need a queue rather than just scheduling an item in the thread pool - those items would immediately
@ -194,13 +211,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
executionContext = ExecutionContext.Capture(); executionContext = ExecutionContext.Capture();
} }
var flags = forceAsync ? TaskContinuationOptions.RunContinuationsAsynchronously : TaskContinuationOptions.None;
return antecedant.ContinueWith(BackgroundWorkThunk, new WorkItem() return antecedant.ContinueWith(BackgroundWorkThunk, new WorkItem()
{ {
SynchronizationContext = this, SynchronizationContext = this,
ExecutionContext = executionContext, ExecutionContext = executionContext,
Callback = d, Callback = d,
State = state, State = state,
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Current); }, CancellationToken.None, flags, TaskScheduler.Current);
} }
private void ExecuteSynchronously( private void ExecuteSynchronously(
@ -280,5 +298,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
public SendOrPostCallback Callback; public SendOrPostCallback Callback;
public object State; public object State;
} }
private class RendererSynchronizationTaskCompletionSource<TCallback, TResult> : TaskCompletionSource<TResult>
{
public RendererSynchronizationTaskCompletionSource(TCallback callback)
{
Callback = callback;
}
public TCallback Callback { get; }
}
} }
} }

View File

@ -2,9 +2,11 @@
// 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;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Principal; using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.AspNetCore.Components.Test.Helpers;
@ -14,6 +16,10 @@ namespace Microsoft.AspNetCore.Components
{ {
public class AuthorizeViewTest public class AuthorizeViewTest
{ {
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
// failures.
private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
[Fact] [Fact]
public void RendersNothingIfNotAuthorized() public void RendersNothingIfNotAuthorized()
{ {
@ -180,7 +186,11 @@ namespace Microsoft.AspNetCore.Components
public void RendersNothingUntilAuthorizationCompleted() public void RendersNothingUntilAuthorizationCompleted()
{ {
// Arrange // Arrange
var renderer = new TestRenderer(); var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
OnUpdateDisplayComplete = () => { @event.Set(); },
};
var rootComponent = WrapInAuthorizeView( var rootComponent = WrapInAuthorizeView(
notAuthorizedContent: builder => builder.AddContent(0, "You are not authorized")); notAuthorizedContent: builder => builder.AddContent(0, "You are not authorized"));
var authTcs = new TaskCompletionSource<AuthenticationState>(); var authTcs = new TaskCompletionSource<AuthenticationState>();
@ -195,7 +205,12 @@ namespace Microsoft.AspNetCore.Components
Assert.Empty(diff1.Edits); Assert.Empty(diff1.Edits);
// Act/Assert 2: Auth process completes asynchronously // Act/Assert 2: Auth process completes asynchronously
@event.Reset();
authTcs.SetResult(new AuthenticationState(new ClaimsPrincipal())); authTcs.SetResult(new AuthenticationState(new ClaimsPrincipal()));
// We need to wait here because the continuations of SetResult will be scheduled to run asynchronously.
@event.Wait(Timeout);
Assert.Equal(2, renderer.Batches.Count); Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1]; var batch2 = renderer.Batches[1];
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single(); var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
@ -212,7 +227,11 @@ namespace Microsoft.AspNetCore.Components
public void RendersAuthorizingContentUntilAuthorizationCompleted() public void RendersAuthorizingContentUntilAuthorizationCompleted()
{ {
// Arrange // Arrange
var renderer = new TestRenderer(); var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
OnUpdateDisplayComplete = () => { @event.Set(); },
};
var rootComponent = WrapInAuthorizeView( var rootComponent = WrapInAuthorizeView(
authorizingContent: builder => builder.AddContent(0, "Auth pending..."), authorizingContent: builder => builder.AddContent(0, "Auth pending..."),
authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!")); authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
@ -234,7 +253,12 @@ namespace Microsoft.AspNetCore.Components
}); });
// Act/Assert 2: Auth process completes asynchronously // Act/Assert 2: Auth process completes asynchronously
@event.Reset();
authTcs.SetResult(CreateAuthenticationState("Monsieur").Result); authTcs.SetResult(CreateAuthenticationState("Monsieur").Result);
// We need to wait here because the continuations of SetResult will be scheduled to run asynchronously.
@event.Wait(Timeout);
Assert.Equal(2, renderer.Batches.Count); Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1]; var batch2 = renderer.Batches[1];
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single(); var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();

View File

@ -2,6 +2,7 @@
// 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;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
@ -12,6 +13,10 @@ namespace Microsoft.AspNetCore.Components.Test
{ {
public class ComponentBaseTest public class ComponentBaseTest
{ {
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
// failures.
private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
[Fact] [Fact]
public void RunsOnInitWhenRendered() public void RunsOnInitWhenRendered()
{ {
@ -178,7 +183,12 @@ namespace Microsoft.AspNetCore.Components.Test
public async Task RendersAfterParametersSetAndInitAsyncTasksAreCompleted() public async Task RendersAfterParametersSetAndInitAsyncTasksAreCompleted()
{ {
// Arrange // Arrange
var renderer = new TestRenderer(); var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
OnUpdateDisplayComplete = () => { @event.Set(); },
};
var component = new TestComponent(); var component = new TestComponent();
component.Counter = 1; component.Counter = 1;
@ -197,10 +207,16 @@ namespace Microsoft.AspNetCore.Components.Test
// A rendering should have happened after the synchronous execution of Init // A rendering should have happened after the synchronous execution of Init
Assert.Single(renderer.Batches); Assert.Single(renderer.Batches);
@event.Reset();
// Completes task started by OnInitAsync // Completes task started by OnInitAsync
component.Counter = 2; component.Counter = 2;
initTask.SetResult(true); initTask.SetResult(true);
// We need to wait here, because the continuation from SetResult needs to be scheduled.
@event.Wait(Timeout);
@event.Reset();
// Component should be rendered once, after set parameters // Component should be rendered once, after set parameters
Assert.Equal(2, renderer.Batches.Count); Assert.Equal(2, renderer.Batches.Count);
@ -209,6 +225,7 @@ namespace Microsoft.AspNetCore.Components.Test
parametersSetTask.SetResult(false); parametersSetTask.SetResult(false);
await renderTask; await renderTask;
Assert.True(@event.IsSet);
// Component should be rendered again // Component should be rendered again
// after the async part of onparameterssetasync completes // after the async part of onparameterssetasync completes

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
@ -19,6 +20,10 @@ namespace Microsoft.AspNetCore.Components.Test
{ {
public class RendererTest public class RendererTest
{ {
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
// failures.
private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
private const string EventActionsName = nameof(NestedAsyncComponent.EventActions); private const string EventActionsName = nameof(NestedAsyncComponent.EventActions);
private const string WhatToRenderName = nameof(NestedAsyncComponent.WhatToRender); private const string WhatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
private const string LogName = nameof(NestedAsyncComponent.Log); private const string LogName = nameof(NestedAsyncComponent.Log);
@ -2458,13 +2463,17 @@ namespace Microsoft.AspNetCore.Components.Test
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingAsynchronously() public void CallsAfterRenderAfterTheUIHasFinishedUpdatingAsynchronously()
{ {
// Arrange // Arrange
var @event = new ManualResetEventSlim();
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
var afterRenderTcs = new TaskCompletionSource<object>(); var afterRenderTcs = new TaskCompletionSource<object>();
var onAfterRenderCallCountLog = new List<int>(); var onAfterRenderCallCountLog = new List<int>();
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task); var component = new AsyncAfterRenderComponent(afterRenderTcs.Task)
{
OnAfterRenderComplete = () => @event.Set(),
};
var renderer = new AsyncUpdateTestRenderer() var renderer = new AsyncUpdateTestRenderer()
{ {
OnUpdateDisplayAsync = _ => tcs.Task OnUpdateDisplayAsync = _ => tcs.Task,
}; };
renderer.AssignRootComponentId(component); renderer.AssignRootComponentId(component);
@ -2473,6 +2482,9 @@ namespace Microsoft.AspNetCore.Components.Test
tcs.SetResult(null); tcs.SetResult(null);
afterRenderTcs.SetResult(null); afterRenderTcs.SetResult(null);
// We need to wait here because the completions from SetResult will be scheduled.
@event.Wait(Timeout);
// Assert // Assert
Assert.True(component.Called); Assert.True(component.Called);
} }
@ -2481,9 +2493,13 @@ namespace Microsoft.AspNetCore.Components.Test
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingSynchronously() public void CallsAfterRenderAfterTheUIHasFinishedUpdatingSynchronously()
{ {
// Arrange // Arrange
var @event = new ManualResetEventSlim();
var afterRenderTcs = new TaskCompletionSource<object>(); var afterRenderTcs = new TaskCompletionSource<object>();
var onAfterRenderCallCountLog = new List<int>(); var onAfterRenderCallCountLog = new List<int>();
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task); var component = new AsyncAfterRenderComponent(afterRenderTcs.Task)
{
OnAfterRenderComplete = () => @event.Set(),
};
var renderer = new AsyncUpdateTestRenderer() var renderer = new AsyncUpdateTestRenderer()
{ {
OnUpdateDisplayAsync = _ => Task.CompletedTask OnUpdateDisplayAsync = _ => Task.CompletedTask
@ -2494,6 +2510,9 @@ namespace Microsoft.AspNetCore.Components.Test
component.TriggerRender(); component.TriggerRender();
afterRenderTcs.SetResult(null); afterRenderTcs.SetResult(null);
// We need to wait here because the completions from SetResult will be scheduled.
@event.Wait(Timeout);
// Assert // Assert
Assert.True(component.Called); Assert.True(component.Called);
} }
@ -2798,7 +2817,12 @@ namespace Microsoft.AspNetCore.Components.Test
// code paths are special cased for the first render because of prerendering. // code paths are special cased for the first render because of prerendering.
// Arrange // Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true }; var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
ShouldHandleExceptions = true,
OnExceptionHandled = () => { @event.Set(); },
};
var taskToAwait = Task.CompletedTask; var taskToAwait = Task.CompletedTask;
var component = new TestComponent(builder => var component = new TestComponent(builder =>
{ {
@ -2815,8 +2839,13 @@ namespace Microsoft.AspNetCore.Components.Test
// Act // Act
var exception = new InvalidOperationException(); var exception = new InvalidOperationException();
@event.Reset();
asyncExceptionTcs.SetException(exception); asyncExceptionTcs.SetException(exception);
// We need to wait here because the continuations of SetException will be scheduled to run asynchronously.
@event.Wait(Timeout);
// Assert // Assert
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException()); Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
} }
@ -3829,10 +3858,14 @@ namespace Microsoft.AspNetCore.Components.Test
public bool Called { get; private set; } public bool Called { get; private set; }
public Action OnAfterRenderComplete { get; set; }
public async Task OnAfterRenderAsync() public async Task OnAfterRenderAsync()
{ {
await _task; await _task;
Called = true; Called = true;
OnAfterRenderComplete?.Invoke();
} }
protected override void BuildRenderTree(RenderTreeBuilder builder) protected override void BuildRenderTree(RenderTreeBuilder builder)

View File

@ -1,8 +1,9 @@
// 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 Xunit; using Xunit;
@ -16,34 +17,51 @@ namespace Microsoft.AspNetCore.Components.Rendering
public TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); public TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
[Fact] [Fact]
public void Post_CanRunSynchronously_WhenNotBusy() public void Post_RunsAsynchronously_WhenNotBusy()
{ {
// Arrange // Arrange
var context = new RendererSynchronizationContext(); var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread; var thread = Thread.CurrentThread;
Thread capturedThread = null; Thread capturedThread = null;
var e = new ManualResetEventSlim();
// Act // Act
context.Post((_) => context.Post((_) =>
{ {
capturedThread = Thread.CurrentThread; capturedThread = Thread.CurrentThread;
e.Set();
}, null); }, null);
// Assert // Assert
Assert.Same(thread, capturedThread); Assert.True(e.Wait(Timeout), "timeout");
Assert.NotSame(thread, capturedThread);
} }
[Fact] [Fact]
public void Post_CanRunSynchronously_WhenNotBusy_Exception() public void Post_RunsAynchronously_WhenNotBusy_Exception()
{ {
// Arrange // Arrange
var context = new RendererSynchronizationContext(); var context = new RendererSynchronizationContext();
// Act & Assert Exception exception = null;
Assert.Throws<InvalidTimeZoneException>(() => context.Post((_) => context.UnhandledException += (sender, e) =>
{
exception = (InvalidTimeZoneException)e.ExceptionObject;
};
// Act
context.Post((_) =>
{ {
throw new InvalidTimeZoneException(); throw new InvalidTimeZoneException();
}, null)); }, null);
// Assert
//
// Use another item to 'push through' the throwing one
context.Send((_) => { }, null);
Assert.NotNull(exception);
} }
[Fact] [Fact]
@ -573,7 +591,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
Thread capturedThread = null; Thread capturedThread = null;
// Act // Act
var task = context.Invoke(() => var task = context.InvokeAsync(() =>
{ {
capturedThread = Thread.CurrentThread; capturedThread = Thread.CurrentThread;
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -19,6 +20,10 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
{ {
public class RemoteRendererTest : HtmlRendererTestBase public class RemoteRendererTest : HtmlRendererTestBase
{ {
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
// failures.
private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider) protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider)
{ {
return GetRemoteRenderer(serviceProvider, new CircuitClientProxy()); return GetRemoteRenderer(serviceProvider, new CircuitClientProxy());
@ -50,6 +55,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
public async Task ProcessBufferedRenderBatches_WritesRenders() public async Task ProcessBufferedRenderBatches_WritesRenders()
{ {
// Arrange // Arrange
var @event = new ManualResetEventSlim();
var serviceProvider = new ServiceCollection().BuildServiceProvider(); var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderIds = new List<long>(); var renderIds = new List<long>();
@ -78,8 +84,13 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender(); component.TriggerRender();
renderer.OnRenderCompleted(2, null); renderer.OnRenderCompleted(2, null);
@event.Reset();
firstBatchTCS.SetResult(null); firstBatchTCS.SetResult(null);
// Waiting is required here because the continuations of SetResult will not execute synchronously.
@event.Wait(Timeout);
circuitClient.SetDisconnected(); circuitClient.SetDisconnected();
component.TriggerRender(); component.TriggerRender();
component.TriggerRender(); component.TriggerRender();
@ -261,7 +272,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
NullLogger.Instance); NullLogger.Instance);
} }
private class TestComponent : IComponent private class TestComponent : IComponent, IHandleAfterRender
{ {
private RenderHandle _renderHandle; private RenderHandle _renderHandle;
private RenderFragment _renderFragment = (builder) => private RenderFragment _renderFragment = (builder) =>
@ -280,11 +291,19 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
_renderFragment = renderFragment; _renderFragment = renderFragment;
} }
public Action OnAfterRenderComplete { get; set; }
public void Configure(RenderHandle renderHandle) public void Configure(RenderHandle renderHandle)
{ {
_renderHandle = renderHandle; _renderHandle = renderHandle;
} }
public Task OnAfterRenderAsync()
{
OnAfterRenderComplete?.Invoke();
return Task.CompletedTask;
}
public Task SetParametersAsync(ParameterCollection parameters) public Task SetParametersAsync(ParameterCollection parameters)
{ {
TriggerRender(); TriggerRender();

View File

@ -25,8 +25,12 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
{ {
} }
public Action OnExceptionHandled { get; set; }
public Action<RenderBatch> OnUpdateDisplay { get; set; } public Action<RenderBatch> OnUpdateDisplay { get; set; }
public Action OnUpdateDisplayComplete { get; set; }
public List<CapturedBatch> Batches { get; } public List<CapturedBatch> Batches { get; }
= new List<CapturedBatch>(); = new List<CapturedBatch>();
@ -81,6 +85,7 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
} }
HandledExceptions.Add(exception); HandledExceptions.Add(exception);
OnExceptionHandled?.Invoke();
} }
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
@ -102,6 +107,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
// This renderer updates the UI synchronously, like the WebAssembly one. // This renderer updates the UI synchronously, like the WebAssembly one.
// To test async UI updates, subclass TestRenderer and override UpdateDisplayAsync. // To test async UI updates, subclass TestRenderer and override UpdateDisplayAsync.
OnUpdateDisplayComplete?.Invoke();
return Task.CompletedTask; return Task.CompletedTask;
} }
} }