Browse Source

Improve Components error handling (#7165)

* Improve Components error handling

* Change event handlers IHandleEvent, IHandleAfterEvent to be async.
* Return faulted tasks to Renderer instead of handling exceptions in ComponentBase
* Use ILogger in RemoteRenderer, and log to console in WebAssemblyRenderer
* Cleaning up touched files

Fixes https://github.com/aspnet/AspNetCore/issues/4964
Pranav K 7 years ago
parent
commit
cddbc2e888
27 changed files with 1087 additions and 497 deletions
  1. 4 3
      src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs
  2. 6 4
      src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs
  3. 34 11
      src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs
  4. 5 0
      src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
  5. 5 14
      src/Components/Browser.JS/src/package-lock.json
  6. 5 3
      src/Components/Browser/src/RendererRegistryEventDispatcher.cs
  7. 5 0
      src/Components/Components/perf/RenderTreeDiffBuilderBenchmark.cs
  8. 61 138
      src/Components/Components/src/ComponentBase.cs
  9. 5 2
      src/Components/Components/src/IHandleAfterRender.cs
  10. 5 2
      src/Components/Components/src/IHandleEvent.cs
  11. 32 29
      src/Components/Components/src/Rendering/ComponentState.cs
  12. 10 51
      src/Components/Components/src/Rendering/HtmlRenderer.cs
  13. 87 104
      src/Components/Components/src/Rendering/Renderer.cs
  14. 126 22
      src/Components/Components/test/ComponentBaseTest.cs
  15. 3 0
      src/Components/Components/test/RenderTreeBuilderTest.cs
  16. 6 4
      src/Components/Components/test/RenderTreeDiffBuilderTest.cs
  17. 555 49
      src/Components/Components/test/RendererTest.cs
  18. 30 30
      src/Components/Components/test/Rendering/HtmlRendererTests.cs
  19. 1 1
      src/Components/Server/src/Circuits/CircuitHost.cs
  20. 7 2
      src/Components/Server/src/Circuits/DefaultCircuitFactory.cs
  21. 24 15
      src/Components/Server/src/Circuits/RemoteRenderer.cs
  22. 29 0
      src/Components/Server/src/LoggerExtensions.cs
  23. 2 1
      src/Components/Server/test/Circuits/CircuitHostTest.cs
  24. 5 0
      src/Components/Server/test/Circuits/RenderBatchWriterTest.cs
  25. 32 8
      src/Components/Shared/test/TestRenderer.cs
  26. 2 3
      src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs
  27. 1 1
      src/Servers/Kestrel/test/Interop.FunctionalTests/H2SpecTests.cs

+ 4 - 3
src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Blazor.Rendering;
 using Microsoft.AspNetCore.Components.Builder;
 
@@ -35,13 +36,13 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
             Entries.Add((componentType, domElementSelector));
         }
 
-        public WebAssemblyRenderer CreateRenderer()
+        public async Task<WebAssemblyRenderer> CreateRendererAsync()
         {
             var renderer = new WebAssemblyRenderer(Services);
             for (var i = 0; i < Entries.Count; i++)
             {
-                var entry = Entries[i];
-                renderer.AddComponent(entry.componentType, entry.domElementSelector);
+                var (componentType, domElementSelector) = Entries[i];
+                await renderer.AddComponentAsync(componentType, domElementSelector);
             }
 
             return renderer;

+ 6 - 4
src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs

@@ -42,6 +42,11 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
             JSRuntime.SetCurrentJSRuntime(_runtime);
             SetBrowserHttpMessageHandlerAsDefault();
 
+            return StartAsyncAwaited();
+        }
+
+        private async Task StartAsyncAwaited()
+        {
             var scopeFactory = Services.GetRequiredService<IServiceScopeFactory>();
             _scope = scopeFactory.CreateScope();
 
@@ -61,7 +66,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
                 var builder = new WebAssemblyBlazorApplicationBuilder(_scope.ServiceProvider);
                 startup.Configure(builder, _scope.ServiceProvider);
 
-                _renderer = builder.CreateRenderer();
+                _renderer = await builder.CreateRendererAsync();
             }
             catch
             {
@@ -76,9 +81,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
 
                 throw;
             }
-
-
-            return Task.CompletedTask;
         }
 
         public Task StopAsync(CancellationToken cancellationToken = default)

+ 34 - 11
src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs

@@ -1,14 +1,13 @@
 // 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;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Browser;
 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
 {
@@ -24,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
         /// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
         /// </summary>
         /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use when initializing components.</param>
-        public WebAssemblyRenderer(IServiceProvider serviceProvider): base(serviceProvider)
+        public WebAssemblyRenderer(IServiceProvider serviceProvider) : base(serviceProvider)
         {
             // The browser renderer registers and unregisters itself with the static
             // registry. This works well with the WebAssembly runtime, and is simple for the
@@ -38,11 +37,13 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
         /// </summary>
         /// <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
-        {
-            AddComponent(typeof(TComponent), domElementSelector);
-        }
+        /// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
+        /// <remarks>
+        /// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
+        /// want to await the rendering of the added component.
+        /// </remarks>
+        public Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : IComponent
+            => AddComponentAsync(typeof(TComponent), domElementSelector);
 
         /// <summary>
         /// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
@@ -50,7 +51,12 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
         /// </summary>
         /// <param name="componentType">The type of the component.</param>
         /// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
-        public void AddComponent(Type componentType, string domElementSelector)
+        /// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
+        /// <remarks>
+        /// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
+        /// want to await the rendering of the added component.
+        /// </remarks>
+        public Task AddComponentAsync(Type componentType, string domElementSelector)
         {
             var component = InstantiateComponent(componentType);
             var componentId = AssignRootComponentId(component);
@@ -66,7 +72,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
                 domElementSelector,
                 componentId);
 
-            RenderRootComponent(componentId);
+            return RenderRootComponentAsync(componentId);
         }
 
         /// <inheritdoc />
@@ -93,5 +99,22 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
                 throw new NotImplementedException($"{nameof(WebAssemblyRenderer)} is supported only with in-process JS runtimes.");
             }
         }
+
+        /// <inheritdoc />
+        protected override void HandleException(Exception exception)
+        {
+            Console.Error.WriteLine($"Unhandled exception rendering component:");
+            if (exception is AggregateException aggregateException)
+            {
+                foreach (var innerException in aggregateException.Flatten().InnerExceptions)
+                {
+                    Console.Error.WriteLine(innerException);
+                }
+            }
+            else
+            {
+                Console.Error.WriteLine(exception);
+            }
+        }
     }
 }

+ 5 - 0
src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs

@@ -443,6 +443,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
             public void AttachComponent(IComponent component)
                 => AssignRootComponentId(component);
 
+            protected override void HandleException(Exception exception)
+            {
+                throw new NotImplementedException();
+            }
+
             protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
             {
                 LatestBatchReferenceFrames = renderBatch.ReferenceFrames.ToArray();

+ 5 - 14
src/Components/Browser.JS/src/package-lock.json

@@ -1546,14 +1546,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -1573,14 +1571,12 @@
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -1697,8 +1693,7 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -1724,7 +1719,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -1732,14 +1726,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -1758,7 +1750,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }

+ 5 - 3
src/Components/Browser/src/RendererRegistryEventDispatcher.cs

@@ -1,9 +1,10 @@
 // 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;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.JSInterop;
-using System;
 
 namespace Microsoft.AspNetCore.Components.Browser
 {
@@ -16,12 +17,13 @@ namespace Microsoft.AspNetCore.Components.Browser
         /// For framework use only.
         /// </summary>
         [JSInvokable(nameof(DispatchEvent))]
-        public static void DispatchEvent(
+        public static Task DispatchEvent(
             BrowserEventDescriptor eventDescriptor, string eventArgsJson)
         {
             var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
             var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
-            renderer.DispatchEvent(
+
+            return renderer.DispatchEventAsync(
                 eventDescriptor.ComponentId,
                 eventDescriptor.EventHandlerId,
                 eventArgs);

+ 5 - 0
src/Components/Components/perf/RenderTreeDiffBuilderBenchmark.cs

@@ -91,6 +91,11 @@ namespace Microsoft.AspNetCore.Components.Performance
             {
             }
 
+            protected override void HandleException(Exception exception)
+            {
+                throw new NotImplementedException();
+            }
+
             protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
                 => Task.CompletedTask;
         }

+ 61 - 138
src/Components/Components/src/ComponentBase.cs

@@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components
 
         private readonly RenderFragment _renderFragment;
         private RenderHandle _renderHandle;
-        private bool _hasCalledInit;
+        private bool _initialized;
         private bool _hasNeverRendered = true;
         private bool _hasPendingQueuedRender;
 
@@ -177,188 +177,111 @@ namespace Microsoft.AspNetCore.Components
         public virtual Task SetParametersAsync(ParameterCollection parameters)
         {
             parameters.SetParameterProperties(this);
-            if (!_hasCalledInit)
+            if (!_initialized)
             {
-                return RunInitAndSetParameters();
+                _initialized = true;
+
+                return RunInitAndSetParametersAsync();
             }
             else
             {
-                OnParametersSet();
-                // If you override OnInitAsync or OnParametersSetAsync and return a noncompleted task,
-                // then by default we automatically re-render once each of those tasks completes.
-                var isAsync = false;
-                Task parametersTask = null;
-                (isAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
-                StateHasChanged();
-                // We call StateHasChanged here so that we render after OnParametersSet and after the
-                // synchronous part of OnParametersSetAsync has run, and in case there is async work
-                // we trigger another render.
-                if (isAsync)
-                {
-                    return parametersTask;
-                }
-
-                return Task.CompletedTask;
+                return CallOnParametersSetAsync();
             }
         }
 
-        private async Task RunInitAndSetParameters()
+        private async Task RunInitAndSetParametersAsync()
         {
-            _hasCalledInit = true;
-            var initIsAsync = false;
-
             OnInit();
-            Task initTask = null;
-            (initIsAsync, initTask) = ProcessLifeCycletask(OnInitAsync());
-            if (initIsAsync)
+            var task = OnInitAsync();
+
+            if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
             {
                 // Call state has changed here so that we render after the sync part of OnInitAsync has run
                 // and wait for it to finish before we continue. If no async work has been done yet, we want
                 // to defer calling StateHasChanged up until the first bit of async code happens or until
-                // the end.
+                // the end. Additionally, we want to avoid calling StateHasChanged if no
+                // async work is to be performed.
                 StateHasChanged();
-                await initTask;
-            }
 
-            OnParametersSet();
-            Task parametersTask = null;
-            var setParametersIsAsync = false;
-            (setParametersIsAsync, parametersTask) = ProcessLifeCycletask(OnParametersSetAsync());
-            // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
-            // the synchronous part of OnParametersSetAsync has run, triggering another re-render in case there
-            // is additional async work.
-            StateHasChanged();
-            if (setParametersIsAsync)
-            {
-                await parametersTask;
+                try
+                {
+                    await task;
+                }
+                catch when (task.IsCanceled)
+                {
+                    // Ignore exceptions from task cancelletions.
+                    // Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of 
+                    // CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled). 
+                    // It's much easier to check the state of the Task (i.e. Task.IsCanceled) rather than catch two distinct exceptions.
+                }
+
+                // Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us.
             }
+
+            await CallOnParametersSetAsync();
         }
 
-        private (bool isAsync, Task asyncTask) ProcessLifeCycletask(Task task)
+        private Task CallOnParametersSetAsync()
         {
-            if (task == null)
-            {
-                throw new ArgumentNullException(nameof(task));
-            }
+            OnParametersSet();
+            var task = OnParametersSetAsync();
+            // If no async work is to be performed, i.e. the task has already ran to completion
+            // or was canceled by the time we got to inspect it, avoid going async and re-invoking
+            // StateHasChanged at the culmination of the async work.
+            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
+                task.Status != TaskStatus.Canceled;
 
-            switch (task.Status)
-            {
-                // If it's already completed synchronously, no need to await and no
-                // need to issue a further render (we already rerender synchronously).
-                // Just need to make sure we propagate any errors.
-                case TaskStatus.RanToCompletion:
-                case TaskStatus.Canceled:
-                    return (false, null);
-                case TaskStatus.Faulted:
-                    HandleException(task.Exception);
-                    return (false, null);
-                // For incomplete tasks, automatically re-render on successful completion
-                default:
-                    return (true, ReRenderAsyncTask(task));
-            }
+            // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
+            // the synchronous part of OnParametersSetAsync has run.
+            StateHasChanged();
+
+            return shouldAwaitTask ?
+                CallStateHasChangedOnAsyncCompletion(task) :
+                Task.CompletedTask;
         }
 
-        private async Task ReRenderAsyncTask(Task task)
+        private async Task CallStateHasChangedOnAsyncCompletion(Task task)
         {
             try
             {
                 await task;
-                StateHasChanged();
             }
-            catch (Exception ex)
-            {
-                // Either the task failed, or it was cancelled, or StateHasChanged threw.
-                // We want to report task failure or StateHasChanged exceptions only.
-                if (!task.IsCanceled)
-                {
-                    HandleException(ex);
-                }
-            }
-        }
-
-        private async void ContinueAfterLifecycleTask(Task task)
-        {
-            switch (task == null ? TaskStatus.RanToCompletion : task.Status)
-            {
-                // If it's already completed synchronously, no need to await and no
-                // need to issue a further render (we already rerender synchronously).
-                // Just need to make sure we propagate any errors.
-                case TaskStatus.RanToCompletion:
-                case TaskStatus.Canceled:
-                    break;
-                case TaskStatus.Faulted:
-                    HandleException(task.Exception);
-                    break;
-
-                // For incomplete tasks, automatically re-render on successful completion
-                default:
-                    try
-                    {
-                        await task;
-                        StateHasChanged();
-                    }
-                    catch (Exception ex)
-                    {
-                        // Either the task failed, or it was cancelled, or StateHasChanged threw.
-                        // We want to report task failure or StateHasChanged exceptions only.
-                        if (!task.IsCanceled)
-                        {
-                            HandleException(ex);
-                        }
-                    }
-
-                    break;
-            }
-        }
-
-        private static void HandleException(Exception ex)
-        {
-            if (ex is AggregateException && ex.InnerException != null)
+            catch when (task.IsCanceled)
             {
-                ex = ex.InnerException; // It's more useful
+                // Ignore exceptions from task cancelletions, but don't bother issuing a state change.
+                return;
             }
 
-            // TODO: Need better global exception handling
-            Console.Error.WriteLine($"[{ex.GetType().FullName}] {ex.Message}\n{ex.StackTrace}");
+            StateHasChanged();
         }
 
-        void IHandleEvent.HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
+        Task IHandleEvent.HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
         {
             var task = binding.Invoke(args);
-            ContinueAfterLifecycleTask(task);
+            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
+                task.Status != TaskStatus.Canceled;
 
             // After each event, we synchronously re-render (unless !ShouldRender())
             // This just saves the developer the trouble of putting "StateHasChanged();"
             // at the end of every event callback.
             StateHasChanged();
+
+            return shouldAwaitTask ?
+                CallStateHasChangedOnAsyncCompletion(task) :
+                Task.CompletedTask;
         }
 
-        void IHandleAfterRender.OnAfterRender()
+        Task IHandleAfterRender.OnAfterRenderAsync()
         {
             OnAfterRender();
 
-            var onAfterRenderTask = OnAfterRenderAsync();
-            if (onAfterRenderTask != null && onAfterRenderTask.Status != TaskStatus.RanToCompletion)
-            {
-                // Note that we don't call StateHasChanged to trigger a render after
-                // handling this, because that would be an infinite loop. The only
-                // reason we have OnAfterRenderAsync is so that the developer doesn't
-                // have to use "async void" and do their own exception handling in
-                // the case where they want to start an async task.
-                var taskWithHandledException = HandleAfterRenderException(onAfterRenderTask);
-            }
-        }
+            return OnAfterRenderAsync();
 
-        private async Task HandleAfterRenderException(Task parentTask)
-        {
-            try
-            {
-                await parentTask;
-            }
-            catch (Exception e)
-            {
-                HandleException(e);
-            }
+            // Note that we don't call StateHasChanged to trigger a render after
+            // handling this, because that would be an infinite loop. The only
+            // reason we have OnAfterRenderAsync is so that the developer doesn't
+            // have to use "async void" and do their own exception handling in
+            // the case where they want to start an async task.
         }
     }
 }

+ 5 - 2
src/Components/Components/src/IHandleAfterRender.cs

@@ -1,6 +1,8 @@
-// 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 System.Threading.Tasks;
+
 namespace Microsoft.AspNetCore.Components
 {
     /// <summary>
@@ -11,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
         /// <summary>
         /// Notifies the component that it has been rendered.
         /// </summary>
-        void OnAfterRender();
+        /// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
+        Task OnAfterRenderAsync();
     }
 }

+ 5 - 2
src/Components/Components/src/IHandleEvent.cs

@@ -1,6 +1,8 @@
-// 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 System.Threading.Tasks;
+
 namespace Microsoft.AspNetCore.Components
 {
     /// <summary>
@@ -13,6 +15,7 @@ namespace Microsoft.AspNetCore.Components
         /// </summary>
         /// <param name="binding">The event binding.</param>
         /// <param name="args">Arguments for the event handler.</param>
-        void HandleEvent(EventHandlerInvoker binding, UIEventArgs args);
+        /// <returns>A <see cref="Task"/> that represents the asynchronous event handling operation.</returns>
+        Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args);
     }
 }

+ 32 - 29
src/Components/Components/src/Rendering/ComponentState.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 
 namespace Microsoft.AspNetCore.Components.Rendering
@@ -16,22 +15,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
     /// </summary>
     internal class ComponentState
     {
-        private readonly int _componentId; // TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls
-        private readonly ComponentState _parentComponentState;
-        private readonly IComponent _component;
         private readonly Renderer _renderer;
         private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
         private readonly bool _hasAnyCascadingParameterSubscriptions;
-        private RenderTreeBuilder _renderTreeBuilderCurrent;
         private RenderTreeBuilder _renderTreeBuilderPrevious;
         private ArrayBuilder<RenderTreeFrame> _latestDirectParametersSnapshot; // Lazily instantiated
         private bool _componentWasDisposed;
 
-        public int ComponentId => _componentId;
-        public IComponent Component => _component;
-        public ComponentState ParentComponentState => _parentComponentState;
-        public RenderTreeBuilder CurrrentRenderTree => _renderTreeBuilderCurrent;
-
         /// <summary>
         /// Constructs an instance of <see cref="ComponentState"/>.
         /// </summary>
@@ -41,12 +31,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="parentComponentState">The <see cref="ComponentState"/> for the parent component, or null if this is a root component.</param>
         public ComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState)
         {
-            _componentId = componentId;
-            _parentComponentState = parentComponentState;
-            _component = component ?? throw new ArgumentNullException(nameof(component));
+            ComponentId = componentId;
+            ParentComponentState = parentComponentState;
+            Component = component ?? throw new ArgumentNullException(nameof(component));
             _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
             _cascadingParameters = CascadingParameterState.FindCascadingParameters(this);
-            _renderTreeBuilderCurrent = new RenderTreeBuilder(renderer);
+            CurrrentRenderTree = new RenderTreeBuilder(renderer);
             _renderTreeBuilderPrevious = new RenderTreeBuilder(renderer);
 
             if (_cascadingParameters != null)
@@ -55,6 +45,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
+        // TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls
+        public int ComponentId { get; }
+        public IComponent Component { get; }
+        public ComponentState ParentComponentState { get; }
+        public RenderTreeBuilder CurrrentRenderTree { get; private set; }
+
         public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
         {
             // A component might be in the render queue already before getting disposed by an
@@ -65,31 +61,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
 
             // Swap the old and new tree builders
-            (_renderTreeBuilderCurrent, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, _renderTreeBuilderCurrent);
+            (CurrrentRenderTree, _renderTreeBuilderPrevious) = (_renderTreeBuilderPrevious, CurrrentRenderTree);
 
-            _renderTreeBuilderCurrent.Clear();
-            renderFragment(_renderTreeBuilderCurrent);
+            CurrrentRenderTree.Clear();
+            renderFragment(CurrrentRenderTree);
 
             var diff = RenderTreeDiffBuilder.ComputeDiff(
                 _renderer,
                 batchBuilder,
-                _componentId,
+                ComponentId,
                 _renderTreeBuilderPrevious.GetFrames(),
-                _renderTreeBuilderCurrent.GetFrames());
+                CurrrentRenderTree.GetFrames());
             batchBuilder.UpdatedComponentDiffs.Append(diff);
         }
 
         public void DisposeInBatch(RenderBatchBuilder batchBuilder)
         {
             _componentWasDisposed = true;
- 
+
             // TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.
-            if (_component is IDisposable disposable)
+            if (Component is IDisposable disposable)
             {
                 disposable.Dispose();
             }
 
-            RenderTreeDiffBuilder.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames());
+            RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrrentRenderTree.GetFrames());
 
             if (_hasAnyCascadingParameterSubscriptions)
             {
@@ -97,22 +93,29 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
-        public void DispatchEvent(EventHandlerInvoker binding, UIEventArgs eventArgs)
+        public Task DispatchEventAsync(EventHandlerInvoker binding, UIEventArgs eventArgs)
         {
-            if (_component is IHandleEvent handleEventComponent)
+            if (Component is IHandleEvent handleEventComponent)
             {
-                handleEventComponent.HandleEvent(binding, eventArgs);
+                return handleEventComponent.HandleEventAsync(binding, eventArgs);
             }
             else
             {
                 throw new InvalidOperationException(
-                    $"The component of type {_component.GetType().FullName} cannot receive " +
+                    $"The component of type {Component.GetType().FullName} cannot receive " +
                     $"events because it does not implement {typeof(IHandleEvent).FullName}.");
             }
         }
 
-        public void NotifyRenderCompleted()
-            => (_component as IHandleAfterRender)?.OnAfterRender();
+        public Task NotifyRenderCompletedAsync()
+        {
+            if (Component is IHandleAfterRender handlerAfterRender)
+            {
+                return handlerAfterRender.OnAfterRenderAsync();
+            }
+
+            return Task.CompletedTask;
+        }
 
         public void SetDirectParameters(ParameterCollection parameters)
         {
@@ -167,7 +170,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     hasSubscription = true;
                 }
             }
-            
+
             return hasSubscription;
         }
 

+ 10 - 51
src/Components/Components/src/Rendering/HtmlRenderer.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Runtime.ExceptionServices;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
 
@@ -39,49 +40,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
             return Task.CompletedTask;
         }
 
-        /// <summary>
-        /// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
-        /// of the HTML produced by the component.
-        /// </summary>
-        /// <typeparam name="T">The type of the <see cref="IComponent"/>.</typeparam>
-        /// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
-        /// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
-        public IEnumerable<string> RenderComponent<T>(ParameterCollection initialParameters) where T : IComponent
-        {
-            return RenderComponent(typeof(T), initialParameters);
-        }
-
         /// <summary>
         /// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
         /// of the HTML produced by the component.
         /// </summary>
         /// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
         /// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
-        /// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
-        private IEnumerable<string> RenderComponent(Type componentType, ParameterCollection initialParameters)
-        {
-            var frames = CreateInitialRender(componentType, initialParameters);
-
-            if (frames.Count == 0)
-            {
-                return Array.Empty<string>();
-            }
-            else
-            {
-                var result = new List<string>();
-                var newPosition = RenderFrames(result, frames, 0, frames.Count);
-                Debug.Assert(newPosition == frames.Count);
-                return result;
-            }
-        }
-
-        /// <summary>
-        /// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
-        /// of the HTML produced by the component.
-        /// </summary>
-        /// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
-        /// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
-        /// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
+        /// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
         public async Task<IEnumerable<string>> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
         {
             var frames = await CreateInitialRenderAsync(componentType, initialParameters);
@@ -103,14 +68,18 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
         /// of the HTML produced by the component.
         /// </summary>
-        /// <typeparam name="T">The type of the <see cref="IComponent"/>.</typeparam>
+        /// <typeparam name="TComponent">The type of the <see cref="IComponent"/>.</typeparam>
         /// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
-        /// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
-        public Task<IEnumerable<string>> RenderComponentAsync<T>(ParameterCollection initialParameters) where T : IComponent
+        /// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
+        public Task<IEnumerable<string>> RenderComponentAsync<TComponent>(ParameterCollection initialParameters) where TComponent : IComponent
         {
-            return RenderComponentAsync(typeof(T), initialParameters);
+            return RenderComponentAsync(typeof(TComponent), initialParameters);
         }
 
+        /// <inheritdoc />
+        protected override void HandleException(Exception exception)
+            => ExceptionDispatchInfo.Capture(exception).Throw();
+
         private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
         {
             var nextPosition = position;
@@ -258,16 +227,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
             return position + maxElements;
         }
 
-        private ArrayRange<RenderTreeFrame> CreateInitialRender(Type componentType, ParameterCollection initialParameters)
-        {
-            var component = InstantiateComponent(componentType);
-            var componentId = AssignRootComponentId(component);
-
-            RenderRootComponent(componentId, initialParameters);
-
-            return GetCurrentRenderTreeFrames(componentId);
-        }
-
         private async Task<ArrayRange<RenderTreeFrame>> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
         {
             var component = InstantiateComponent(componentType);

+ 87 - 104
src/Components/Components/src/Rendering/Renderer.cs

@@ -4,7 +4,6 @@
 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;
@@ -100,80 +99,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
         private protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrrentRenderTree.GetFrames();
 
-        /// <summary>
-        /// Performs the first render for a root component. After this, the root component
-        /// makes its own decisions about when to re-render, so there is no need to call
-        /// this more than once.
-        /// </summary>
-        /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
-        protected void RenderRootComponent(int componentId)
-        {
-            RenderRootComponent(componentId, ParameterCollection.Empty);
-        }
-
-        /// <summary>
-        /// Performs the first render for a root component. After this, the root component
-        /// makes its own decisions about when to re-render, so there is no need to call
-        /// this more than once.
-        /// </summary>
-        /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
-        /// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
-        protected void RenderRootComponent(int componentId, ParameterCollection initialParameters)
-        {
-            ReportAsyncExceptions(RenderRootComponentAsync(componentId, initialParameters));
-        }
-
-        private async void ReportAsyncExceptions(Task task)
-        {
-            switch (task.Status)
-            {
-                // If it's already completed synchronously, no need to await and no
-                // need to issue a further render (we already rerender synchronously).
-                // Just need to make sure we propagate any errors.
-                case TaskStatus.RanToCompletion:
-                case TaskStatus.Canceled:
-                    _pendingTasks = null;
-                    break;
-                case TaskStatus.Faulted:
-                    _pendingTasks = null;
-                    HandleException(task.Exception);
-                    break;
-
-                default:
-                    try
-                    {
-                        await task;
-                    }
-                    catch (Exception ex)
-                    {
-                        // Either the task failed, or it was cancelled.
-                        // We want to report task failure exceptions only.
-                        if (!task.IsCanceled)
-                        {
-                            HandleException(ex);
-                        }
-                    }
-                    finally
-                    {
-                        // Clear the list after we are done rendering the root component or an async exception has ocurred.
-                        _pendingTasks = null;
-                    }
-
-                    break;
-            }
-        }
-
-        private static void HandleException(Exception ex)
-        {
-            if (ex is AggregateException && ex.InnerException != null)
-            {
-                ex = ex.InnerException; // It's more useful
-            }
-
-            // TODO: Need better global exception handling
-            Console.Error.WriteLine($"[{ex.GetType().FullName}] {ex.Message}\n{ex.StackTrace}");
-        }
-
         /// <summary>
         /// Performs the first render for a root component, waiting for this component and all
         /// children components to finish rendering in case there is any asynchronous work being
@@ -182,6 +107,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// this more than once.
         /// </summary>
         /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
+        /// <remarks>
+        /// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
+        /// start, but not wait for the entire render to complete.
+        /// </remarks>
         protected Task RenderRootComponentAsync(int componentId)
         {
             return RenderRootComponentAsync(componentId, ParameterCollection.Empty);
@@ -196,13 +125,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// </summary>
         /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
         /// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
+        /// <remarks>
+        /// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
+        /// start, but not wait for the entire render to complete.
+        /// </remarks>
         protected async Task RenderRootComponentAsync(int componentId, ParameterCollection initialParameters)
         {
-            if (_pendingTasks != null)
+            if (Interlocked.CompareExchange(ref _pendingTasks, new List<Task>(), null) != null)
             {
                 throw new InvalidOperationException("There is an ongoing rendering in progress.");
             }
-            _pendingTasks = new List<Task>();
+
             // During the rendering process we keep a list of components performing work in _pendingTasks.
             // _renderer.AddToPendingTasks will be called by ComponentState.SetDirectParameters to add the
             // the Task produced by Component.SetParametersAsync to _pendingTasks in order to track the
@@ -211,8 +144,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
             // work to finish as it will simply trigger new renders that will be handled afterwards.
             // During the asynchronous rendering process we want to wait up untill al components have
             // finished rendering so that we can produce the complete output.
-            GetRequiredComponentState(componentId)
-                .SetDirectParameters(initialParameters);
+            var componentState = GetRequiredComponentState(componentId);
+            componentState.SetDirectParameters(initialParameters);
 
             try
             {
@@ -225,15 +158,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
+        /// <summary>
+        /// Allows derived types to handle exceptions during rendering. Defaults to rethrowing the original exception.
+        /// </summary>
+        /// <param name="exception">The <see cref="Exception"/>.</param>
+        protected abstract void HandleException(Exception exception);
+
         private async Task ProcessAsynchronousWork()
         {
             // Child components SetParametersAsync are stored in the queue of pending tasks,
             // which might trigger further renders.
             while (_pendingTasks.Count > 0)
             {
-                Task pendingWork;
                 // Create a Task that represents the remaining ongoing work for the rendering process
-                pendingWork = Task.WhenAll(_pendingTasks);
+                var pendingWork = Task.WhenAll(_pendingTasks);
 
                 // Clear all pending work.
                 _pendingTasks.Clear();
@@ -241,7 +179,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 // new work might be added before we check again as a result of waiting for all
                 // the child components to finish executing SetParametersAsync
                 await pendingWork;
-            };
+            }
         }
 
         private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
@@ -267,7 +205,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="componentId">The unique identifier for the component within the scope of this <see cref="Renderer"/>.</param>
         /// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
         /// <param name="eventArgs">Arguments to be passed to the event handler.</param>
-        public void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs)
+        /// <returns>A <see cref="Task"/> representing the asynchronous execution operation.</returns>
+        public Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs eventArgs)
         {
             EnsureSynchronizationContext();
 
@@ -275,16 +214,20 @@ namespace Microsoft.AspNetCore.Components.Rendering
             {
                 // The event handler might request multiple renders in sequence. Capture them
                 // all in a single batch.
+                var componentState = GetRequiredComponentState(componentId);
+                Task task = null;
                 try
                 {
                     _isBatchInProgress = true;
-                    GetRequiredComponentState(componentId).DispatchEvent(binding, eventArgs);
+                    task = componentState.DispatchEventAsync(binding, eventArgs);
                 }
                 finally
                 {
                     _isBatchInProgress = false;
                     ProcessRenderQueue();
                 }
+
+                return GetErrorHandledTask(task);
             }
             else
             {
@@ -329,8 +272,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
             // This is for example when we run on a system with a single thread, like WebAssembly.
             if (_dispatcher == null)
             {
-                workItem();
-                return Task.CompletedTask;
+                return workItem();
             }
 
             if (SynchronizationContext.Current == _dispatcher)
@@ -373,12 +315,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 case TaskStatus.Canceled:
                     break;
                 case TaskStatus.Faulted:
-                    // We want to throw immediately if the task failed synchronously instead of
+                    // We want to immediately handle exceptions if the task failed synchronously instead of
                     // waiting for it to throw later. This can happen if the task is produced by
                     // an 'async' state machine (the ones generated using async/await) where even
                     // the synchronous exceptions will get captured and converted into a faulted
                     // task.
-                    ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
+                    HandleException(task.Exception.GetBaseException());
                     break;
                 default:
                     // We are not in rendering the root component.
@@ -386,7 +328,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     {
                         return;
                     }
-                    _pendingTasks.Add(task);
+
+                    _pendingTasks.Add(GetErrorHandledTask(task));
                     break;
             }
         }
@@ -472,7 +415,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
 
                 var batch = _batchBuilder.ToBatch();
                 updateDisplayTask = UpdateDisplayAsync(batch);
-                InvokeRenderCompletedCalls(batch.UpdatedComponents);
+
+                // Fire off the execution of OnAfterRenderAsync, but don't wait for it
+                // if there is async work to be done.
+                _ = InvokeRenderCompletedCalls(batch.UpdatedComponents);
             }
             finally
             {
@@ -482,15 +428,45 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
-        private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
+        private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
         {
+            List<Task> batch = null;
             var array = updatedComponents.Array;
             for (var i = 0; i < updatedComponents.Count; i++)
             {
-                // The component might be rendered and disposed in the same batch (if its parent
-                // was rendered later in the batch, and removed the child from the tree).
-                GetOptionalComponentState(array[i].ComponentId)?.NotifyRenderCompleted();
+                var componentState = GetOptionalComponentState(array[i].ComponentId);
+                if (componentState != null)
+                {
+                    // The component might be rendered and disposed in the same batch (if its parent
+                    // was rendered later in the batch, and removed the child from the tree).
+                    var task = componentState.NotifyRenderCompletedAsync();
+
+                    // We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
+                    // unless we absolutely have to.
+                    if (task.IsCompleted)
+                    {
+                        if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
+                        {
+                            // Nothing to do here.
+                            continue;
+                        }
+                        else if (task.Status == TaskStatus.Faulted)
+                        {
+                            HandleException(task.Exception);
+                            continue;
+                        }
+                    }
+
+                    // The Task is incomplete.
+                    // Queue up the task and we can inspect it later.
+                    batch = batch ?? new List<Task>();
+                    batch.Add(GetErrorHandledTask(task));
+                }
             }
+
+            return batch != null ?
+                Task.WhenAll(batch) :
+                Task.CompletedTask;
         }
 
         private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
@@ -536,14 +512,28 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
+        private async Task GetErrorHandledTask(Task taskToHandle)
+        {
+            try
+            {
+                await taskToHandle;
+            }
+            catch (Exception ex)
+            {
+                if (!taskToHandle.IsCanceled)
+                {
+                    // Ignore errors due to task cancellations.
+                    HandleException(ex);
+                }
+            }
+        }
+
         /// <summary>
         /// Releases all resources currently used by this <see cref="Renderer"/> instance.
         /// </summary>
         /// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
         protected virtual void Dispose(bool disposing)
         {
-            List<Exception> exceptions = null;
-
             foreach (var componentState in _componentStateById.Values)
             {
                 if (componentState.Component is IDisposable disposable)
@@ -554,17 +544,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
                     }
                     catch (Exception exception)
                     {
-                        // Capture exceptions thrown by individual components and rethrow as an aggregate.
-                        exceptions = exceptions ?? new List<Exception>();
-                        exceptions.Add(exception);
+                        HandleException(exception);
                     }
                 }
             }
-
-            if (exceptions != null)
-            {
-                throw new AggregateException(exceptions);
-            }
         }
 
         /// <summary>

+ 126 - 22
src/Components/Components/test/ComponentBaseTest.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
             var component = new TestComponent();
 
-            int onInitRuns = 0;
+            var onInitRuns = 0;
             component.OnInitLogic = c => onInitRuns++;
 
             // Act
@@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
             var component = new TestComponent();
 
-            int onInitAsyncRuns = 0;
+            var onInitAsyncRuns = 0;
             component.RunsBaseOnInitAsync = false;
             component.OnInitAsyncLogic = c =>
             {
@@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
             var component = new TestComponent();
 
-            int onInitAsyncRuns = 0;
+            var onInitAsyncRuns = 0;
             component.RunsBaseOnInitAsync = true;
             component.OnInitAsyncLogic = c =>
             {
@@ -84,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
             var component = new TestComponent();
 
-            int onParametersSetRuns = 0;
+            var onParametersSetRuns = 0;
             component.OnParametersSetLogic = c => onParametersSetRuns++;
 
             // Act
@@ -103,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
             var component = new TestComponent();
 
-            int onParametersSetAsyncRuns = 0;
+            var onParametersSetAsyncRuns = 0;
             component.RunsBaseOnParametersSetAsync = false;
             component.OnParametersSetAsyncLogic = c =>
             {
@@ -127,7 +128,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
             var component = new TestComponent();
 
-            int onParametersSetAsyncRuns = 0;
+            var onParametersSetAsyncRuns = 0;
             component.RunsBaseOnParametersSetAsync = true;
             component.OnParametersSetAsyncLogic = c =>
             {
@@ -145,7 +146,7 @@ namespace Microsoft.AspNetCore.Components.Test
         }
 
         [Fact]
-        public void RendersAfterParametersSetAsyncTaskIsCompleted()
+        public async Task RendersAfterParametersSetAsyncTaskIsCompleted()
         {
             // Arrange
             var renderer = new TestRenderer();
@@ -158,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act
             var componentId = renderer.AssignRootComponentId(component);
-            renderer.RenderRootComponent(componentId);
+            var renderTask = renderer.RenderRootComponentAsync(componentId);
 
             // Assert
             Assert.Single(renderer.Batches);
@@ -167,12 +168,14 @@ namespace Microsoft.AspNetCore.Components.Test
             component.Counter = 2;
             parametersSetTask.SetResult(true);
 
+            await renderTask;
+
             // Component should be rendered again
             Assert.Equal(2, renderer.Batches.Count);
         }
 
         [Fact]
-        public void RendersAfterParametersSetAndInitAsyncTasksAreCompleted()
+        public async Task RendersAfterParametersSetAndInitAsyncTasksAreCompleted()
         {
             // Arrange
             var renderer = new TestRenderer();
@@ -188,31 +191,32 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act
             var componentId = renderer.AssignRootComponentId(component);
-            renderer.RenderRootComponent(componentId);
+            var renderTask = renderer.RenderRootComponentAsync(componentId);
 
             // Assert
+            // A rendering should have happened after the synchronous execution of Init
             Assert.Single(renderer.Batches);
 
             // Completes task started by OnInitAsync
             component.Counter = 2;
             initTask.SetResult(true);
 
-            // Component should be rendered again 2 times
-            // after on init async
-            // after set parameters
-            Assert.Equal(3, renderer.Batches.Count);
+            // Component should be rendered once, after set parameters
+            Assert.Equal(2, renderer.Batches.Count);
 
             // Completes task started by OnParametersSetAsync
             component.Counter = 3;
             parametersSetTask.SetResult(false);
 
+            await renderTask;
+
             // Component should be rendered again
             // after the async part of onparameterssetasync completes
-            Assert.Equal(4, renderer.Batches.Count);
+            Assert.Equal(3, renderer.Batches.Count);
         }
 
         [Fact]
-        public void DoesNotRenderAfterOnInitAsyncTaskIsCancelled()
+        public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelled()
         {
             // Arrange
             var renderer = new TestRenderer();
@@ -222,22 +226,49 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act
             var componentId = renderer.AssignRootComponentId(component);
-            renderer.RenderRootComponent(componentId);
+            var renderTask = renderer.RenderRootComponentAsync(componentId);
 
             // Assert
+            Assert.False(renderTask.IsCompleted);
             Assert.Single(renderer.Batches);
 
             // Cancel task started by OnInitAsync
             component.Counter = 2;
             initTask.SetCanceled();
 
+            await renderTask;
+
             // Component should only be rendered again due to
             // the call to StateHasChanged after SetParametersAsync
-            Assert.Equal(2,renderer.Batches.Count);
+            Assert.Equal(2, renderer.Batches.Count);
+        }
+
+        [Fact]
+        public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationToken()
+        {
+            // Arrange
+            var renderer = new TestRenderer();
+            var component = new TestComponent() { Counter = 1 };
+
+            var cts = new CancellationTokenSource();
+            cts.Cancel();
+            component.OnInitAsyncLogic = async _ =>
+            {
+                await Task.Yield();
+                cts.Token.ThrowIfCancellationRequested();
+            };
+
+            // Act
+            var componentId = renderer.AssignRootComponentId(component);
+            await renderer.RenderRootComponentAsync(componentId);
+
+            // Assert
+            // At least one call to StateHasChanged depending on how OnInitAsyncLogic gets scheduled.
+            Assert.NotEmpty(renderer.Batches);
         }
 
         [Fact]
-        public void DoesNotRenderAfterOnParametersSetAsyncTaskIsCancelled()
+        public async Task DoesNotRenderAfterOnParametersSetAsyncTaskIsCanceled()
         {
             // Arrange
             var renderer = new TestRenderer();
@@ -247,7 +278,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act
             var componentId = renderer.AssignRootComponentId(component);
-            renderer.RenderRootComponent(componentId);
+            var renderTask = renderer.RenderRootComponentAsync(componentId);
 
             // Assert
             Assert.Single(renderer.Batches);
@@ -256,8 +287,75 @@ namespace Microsoft.AspNetCore.Components.Test
             component.Counter = 2;
             onParametersSetTask.SetCanceled();
 
+            await renderTask;
+
             // Component should not be rendered again
             Assert.Single(renderer.Batches);
+
+        }
+
+        [Fact]
+        public async Task RenderRootComponentAsync_ReportsErrorDuringOnInit()
+        {
+            // Arrange
+            var expected = new TimeZoneNotFoundException();
+            var renderer = new TestRenderer();
+            var component = new TestComponent { OnInitLogic = _ => throw expected };
+
+            // Act & Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
+
+            // Assert
+            Assert.Same(expected, actual);
+        }
+
+        [Fact]
+        public async Task RenderRootComponentAsync_ReportsErrorDuringOnInitAsync()
+        {
+            // Arrange
+            var expected = new TimeZoneNotFoundException();
+            var renderer = new TestRenderer();
+            var component = new TestComponent { OnInitAsyncLogic = _ => Task.FromException(expected) };
+
+            // Act & Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
+
+            // Assert
+            Assert.Same(expected, actual);
+        }
+
+        [Fact]
+        public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSet()
+        {
+            // Arrange
+            var expected = new TimeZoneNotFoundException();
+            var renderer = new TestRenderer();
+            var component = new TestComponent { OnParametersSetLogic = _ => throw expected };
+
+            // Act & Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
+
+            // Assert
+            Assert.Same(expected, actual);
+        }
+
+        [Fact]
+        public async Task RenderRootComponentAsync_ReportsErrorDuringOnParameterSetAsync()
+        {
+            // Arrange
+            var expected = new TimeZoneNotFoundException();
+            var renderer = new TestRenderer();
+            var component = new TestComponent { OnParametersSetAsyncLogic = _ => Task.FromException(expected) };
+
+            // Act & Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var actual = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => renderer.RenderRootComponentAsync(componentId));
+
+            // Assert
+            Assert.Same(expected, actual);
         }
 
         private class TestComponent : ComponentBase
@@ -305,7 +403,10 @@ namespace Microsoft.AspNetCore.Components.Test
                     await base.OnInitAsync();
                 }
 
-                await OnInitAsyncLogic?.Invoke(this);
+                if (OnInitAsyncLogic != null)
+                {
+                    await OnInitAsyncLogic.Invoke(this);
+                }
             }
 
             protected override void OnParametersSet()
@@ -325,7 +426,10 @@ namespace Microsoft.AspNetCore.Components.Test
                     await base.OnParametersSetAsync();
                 }
 
-                await OnParametersSetAsyncLogic?.Invoke(this);
+                if (OnParametersSetAsyncLogic != null)
+                {
+                    await OnParametersSetAsyncLogic(this);
+                }
             }
         }
     }

+ 3 - 0
src/Components/Components/test/RenderTreeBuilderTest.cs

@@ -1061,6 +1061,9 @@ namespace Microsoft.AspNetCore.Components.Test
             {
             }
 
+            protected override void HandleException(Exception exception)
+                => throw new NotImplementedException();
+
             protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
                 => throw new NotImplementedException();
         }

+ 6 - 4
src/Components/Components/test/RenderTreeDiffBuilderTest.cs

@@ -1,14 +1,13 @@
 // 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.Rendering;
-using Microsoft.AspNetCore.Components.RenderTree;
-using Microsoft.AspNetCore.Components.Test.Helpers;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Test.Helpers;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test
@@ -1531,6 +1530,9 @@ namespace Microsoft.AspNetCore.Components.Test
             {
             }
 
+            protected override void HandleException(Exception exception)
+                => throw new NotImplementedException();
+
             protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
                 => Task.CompletedTask;
         }

+ 555 - 49
src/Components/Components/test/RendererTest.cs

@@ -6,10 +6,12 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.ExceptionServices;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Test.Helpers;
+using Microsoft.AspNetCore.Testing;
 using Microsoft.AspNetCore.Testing.xunit;
 using Xunit;
 
@@ -173,13 +175,17 @@ namespace Microsoft.AspNetCore.Components.Test
         {
             // Arrange
             var renderer = new TestRenderer();
-            var component = new AsyncComponent(5); // Triggers n renders, the first one creating <p>n</p> and the n-1 renders asynchronously update the value.
+            var tcs = new TaskCompletionSource<int>();
+            var component = new AsyncComponent(tcs.Task, 5); // Triggers n renders, the first one creating <p>n</p> and the n-1 renders asynchronously update the value.
 
             // Act
             var componentId = renderer.AssignRootComponentId(component);
-            await renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
+            var renderTask = renderer.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId));
 
             // Assert
+            Assert.False(renderTask.IsCompleted);
+            tcs.SetResult(0);
+            await renderTask;
             Assert.Equal(5, renderer.Batches.Count);
 
             // First render
@@ -195,7 +201,7 @@ namespace Microsoft.AspNetCore.Components.Test
             AssertFrame.Text(create.ReferenceFrames[1], "5");
 
             // Second render
-            for (int i = 1; i < 5; i++)
+            for (var i = 1; i < 5; i++)
             {
 
                 var update = renderer.Batches[i];
@@ -413,6 +419,36 @@ namespace Microsoft.AspNetCore.Components.Test
             AssertStream(3, logForThirdChild);
         }
 
+        [Fact]
+        public void DispatchingEventsWithoutAsyncWorkShouldCompleteSynchronously()
+        {
+            // Arrange: Render a component with an event handler
+            var renderer = new TestRenderer();
+            UIEventArgs receivedArgs = null;
+
+            var component = new EventComponent
+            {
+                OnTest = args => { receivedArgs = args; }
+            };
+            var componentId = renderer.AssignRootComponentId(component);
+            component.TriggerRender();
+
+            var eventHandlerId = renderer.Batches.Single()
+                .ReferenceFrames
+                .First(frame => frame.AttributeValue != null)
+                .AttributeEventHandlerId;
+
+            // Assert: Event not yet fired
+            Assert.Null(receivedArgs);
+
+            // Act/Assert: Event can be fired
+            var eventArgs = new UIEventArgs();
+            var task = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
+
+            // This should always be run synchronously
+            Assert.True(task.IsCompletedSuccessfully);
+        }
+
         [Fact]
         public void CanDispatchEventsToTopLevelComponents()
         {
@@ -437,7 +473,9 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert: Event can be fired
             var eventArgs = new UIEventArgs();
-            renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
+            var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Same(eventArgs, receivedArgs);
         }
 
@@ -465,7 +503,9 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert: Event can be fired
             var eventArgs = new UIMouseEventArgs();
-            renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
+            var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Same(eventArgs, receivedArgs);
         }
 
@@ -493,7 +533,9 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert: Event can be fired
             var eventArgs = new UIMouseEventArgs();
-            renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
+            var renderTask = renderer.DispatchEventAsync(componentId, eventHandlerId, eventArgs);
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.NotNull(receivedArgs);
         }
 
@@ -532,7 +574,9 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert: Event can be fired
             var eventArgs = new UIEventArgs();
-            renderer.DispatchEvent(nestedComponentId, eventHandlerId, eventArgs);
+            var renderTask = renderer.DispatchEventAsync(nestedComponentId, eventHandlerId, eventArgs);
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Same(eventArgs, receivedArgs);
         }
 
@@ -559,7 +603,11 @@ 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>(() =>
+            {
+                // Verifies that the exception is thrown synchronously.
+                _ = renderer.DispatchEventAsync(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);
         }
@@ -571,7 +619,11 @@ namespace Microsoft.AspNetCore.Components.Test
             var renderer = new TestRenderer();
 
             // Act/Assert
-            Assert.Throws<ArgumentException>(() => renderer.DispatchEvent(123, 0, new UIEventArgs()));
+            Assert.Throws<ArgumentException>(() =>
+            {
+                // Intentionally written this way to verify that the exception is thrown synchronously.
+                _ = renderer.DispatchEventAsync(123, 0, new UIEventArgs());
+            });
         }
 
         [Fact]
@@ -790,7 +842,8 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert 1: Event handler fires when we trigger it
             Assert.Equal(0, eventCount);
-            renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
+            var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Equal(1, eventCount);
 
             // Now change the attribute value
@@ -799,10 +852,16 @@ 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>(() =>
+            {
+                // Verifies that the exception is thrown synchronously.
+                _ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
+            });
+
             Assert.Equal(1, eventCount);
             Assert.Equal(0, newEventCount);
-            renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null);
+            renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId + 1, args: null);
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Equal(1, newEventCount);
         }
 
@@ -824,7 +883,8 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert 1: Event handler fires when we trigger it
             Assert.Equal(0, eventCount);
-            renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
+            var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Equal(1, eventCount);
 
             // Now remove the event attribute
@@ -832,7 +892,11 @@ 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>(() =>
+            {
+                // Verifies that the exception is thrown synchronously.
+                _ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
+            });
             Assert.Equal(1, eventCount);
         }
 
@@ -870,7 +934,8 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert 1: Event handler fires when we trigger it
             Assert.Equal(0, eventCount);
-            renderer.DispatchEvent(childComponentId, eventHandlerId, args: null);
+            var renderTask = renderer.DispatchEventAsync(childComponentId, eventHandlerId, args: null);
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Equal(1, eventCount);
 
             // Now remove the EventComponent
@@ -878,7 +943,11 @@ 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>(() =>
+            {
+                // Verifies that the exception is thrown synchronously.
+                _ = renderer.DispatchEventAsync(eventHandlerId, eventHandlerId, args: null);
+            });
             Assert.Equal(1, eventCount);
         }
 
@@ -900,7 +969,8 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act/Assert 1: Event handler fires when we trigger it
             Assert.Equal(0, eventCount);
-            renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
+            var renderTask = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Equal(1, eventCount);
 
             // Now remove the ancestor element
@@ -908,7 +978,11 @@ 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>(() =>
+            {
+                // Verifies that the exception is thrown synchronously.
+                _ = renderer.DispatchEventAsync(componentId, origEventHandlerId, args: null);
+            });
             Assert.Equal(1, eventCount);
         }
 
@@ -947,9 +1021,10 @@ namespace Microsoft.AspNetCore.Components.Test
             Assert.Single(renderer.Batches);
 
             // Act
-            renderer.DispatchEvent(childComponentId, origEventHandlerId, args: null);
+            var renderTask = renderer.DispatchEventAsync(childComponentId, origEventHandlerId, args: null);
 
             // Assert
+            Assert.True(renderTask.IsCompletedSuccessfully);
             Assert.Equal(2, renderer.Batches.Count);
             var batch = renderer.Batches.Last();
             Assert.Collection(batch.DiffsInOrder,
@@ -1137,9 +1212,10 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act
             // The fact that there's no error here is the main thing we're testing
-            renderer.DispatchEvent(childComponentId, origEventHandlerId, args: null);
+            var renderTask = renderer.DispatchEventAsync(childComponentId, origEventHandlerId, args: null);
 
             // Assert: correct render result
+            Assert.True(renderTask.IsCompletedSuccessfully);
             var newBatch = renderer.Batches.Skip(1).Single();
             Assert.Equal(1, newBatch.DisposedComponentIDs.Count);
             Assert.Equal(1, newBatch.DiffsByComponentId.Count);
@@ -1168,7 +1244,9 @@ namespace Microsoft.AspNetCore.Components.Test
 
             // Act: Toggle the checkbox
             var eventArgs = new UIChangeEventArgs { Value = true };
-            renderer.DispatchEvent(componentId, checkboxChangeEventHandlerId, eventArgs);
+            var renderTask =  renderer.DispatchEventAsync(componentId, checkboxChangeEventHandlerId, eventArgs);
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
             var latestBatch = renderer.Batches.Last();
             var latestDiff = latestBatch.DiffsInOrder.Single();
             var referenceFrames = latestBatch.ReferenceFrames;
@@ -1349,28 +1427,444 @@ namespace Microsoft.AspNetCore.Components.Test
             // Act/Assert 1: Event can be fired for the first time
             var render1TCS = new TaskCompletionSource<object>();
             renderer.NextUpdateDisplayReturnTask = render1TCS.Task;
-            renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
+            await renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs());
             Assert.Equal(1, numEventsFired);
 
             // Act/Assert 2: *Same* event handler ID can be reused prior to completion of
             // preceding UI update
             var render2TCS = new TaskCompletionSource<object>();
             renderer.NextUpdateDisplayReturnTask = render2TCS.Task;
-            renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
+            await renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs());
             Assert.Equal(2, numEventsFired);
 
             // Act/Assert 3: After we complete the first UI update in which a given
             // event handler ID is disposed, we can no longer reuse that event handler ID
             render1TCS.SetResult(null);
             await Task.Delay(500); // From here we can't see when the async disposal is completed. Just give it plenty of time (Task.Yield isn't enough).
-            var ex = Assert.Throws<ArgumentException>(() =>
-            {
-                renderer.DispatchEvent(componentId, eventHandlerId, new UIEventArgs());
-            });
+            var ex = await Assert.ThrowsAsync<ArgumentException>(() =>
+                renderer.DispatchEventAsync(componentId, eventHandlerId, new UIEventArgs()));
             Assert.Equal($"There is no event handler with ID {eventHandlerId}", ex.Message);
             Assert.Equal(2, numEventsFired);
         }
 
+        [Fact]
+        public void ExceptionsThrownSynchronouslyCanBeHandledSynchronously()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var exception = new InvalidTimeZoneException();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var task = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = () => throw exception,
+                        },
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.True(task.IsCompletedSuccessfully);
+            Assert.Equal(new[] { exception }, renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public void ExceptionsThrownSynchronouslyCanBeHandled()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var exception = new InvalidTimeZoneException();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = () => throw exception,
+                        },
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
+            Assert.Equal(new[] { exception }, renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public void ExceptionsReturnedUsingTaskFromExceptionCanBeHandled()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var exception = new InvalidTimeZoneException();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = () => Task.FromException<(int, NestedAsyncComponent.EventType)>(exception),
+                        },
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
+            Assert.Equal(new[] { exception }, renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public async Task ExceptionsThrownAsynchronouslyCanBeHandled()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var tcs = new TaskCompletionSource<int>();
+            var exception = new InvalidTimeZoneException();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = async () =>
+                            {
+                                await tcs.Task;
+                                throw exception;
+                            }
+                        },
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.False(renderTask.IsCompleted);
+            tcs.SetResult(0);
+            await renderTask;
+            Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
+        }
+
+        [Fact]
+        public async Task ExceptionsThrownAsynchronouslyFromMultipleComponentsCanBeHandled()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var exception1 = new InvalidTimeZoneException();
+            var exception2 = new UriFormatException();
+            var tcs = new TaskCompletionSource<int>();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = Array.Empty<NestedAsyncComponent.ExecutionAction>(),
+                    [1] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = async () =>
+                            {
+                                await tcs.Task;
+                                throw exception1;
+                            }
+                        },
+                    },
+                    [2] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = async () =>
+                            {
+                                await tcs.Task;
+                                throw exception2;
+                            }
+                        },
+                    },
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1, 2, }),
+                    [1] = CreateRenderFactory(Array.Empty<int>()),
+                    [2] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.False(renderTask.IsCompleted);
+            tcs.SetResult(0);
+
+            await renderTask;
+            Assert.Equal(2, renderer.HandledExceptions.Count);
+            Assert.Contains(exception1, renderer.HandledExceptions);
+            Assert.Contains(exception2, renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public void ExceptionsThrownSynchronouslyFromMultipleComponentsCanBeHandled()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var exception1 = new InvalidTimeZoneException();
+            var exception2 = new UriFormatException();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = Array.Empty<NestedAsyncComponent.ExecutionAction>(),
+                    [1] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = () =>
+                            {
+                                throw exception1;
+                            }
+                        },
+                    },
+                    [2] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnInitAsyncAsync,
+                            EventAction = () =>
+                            {
+                                throw exception2;
+                            }
+                        },
+                    },
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1, 2, }),
+                    [1] = CreateRenderFactory(Array.Empty<int>()),
+                    [2] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
+
+            Assert.Equal(2, renderer.HandledExceptions.Count);
+            Assert.Contains(exception1, renderer.HandledExceptions);
+            Assert.Contains(exception2, renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public async Task ExceptionsThrownFromHandleAfterRender_AreHandled()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var exception = new InvalidTimeZoneException();
+
+            var taskCompletionSource = new TaskCompletionSource<int>();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
+                            EventAction = () =>
+                            {
+                                throw exception;
+                            },
+                        }
+                    },
+                    [1] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
+                            EventAction = async () =>
+                            {
+                                await Task.Yield();
+                                taskCompletionSource.TrySetResult(0);
+                                return (1, NestedAsyncComponent.EventType.OnAfterRenderAsync);
+                            },
+                        }
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1 }),
+                    [1] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            Assert.True(renderTask.IsCompletedSuccessfully);
+
+            // OnAfterRenderAsync happens in the background. Make it more predictable, by gating it until we're ready to capture exceptions.
+            await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
+            Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
+        }
+
+        [Fact]
+        public void SynchronousCancelledTasks_HandleAfterRender_Works()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var tcs = new TaskCompletionSource<(int, NestedAsyncComponent.EventType)>();
+            tcs.TrySetCanceled();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
+                            EventAction = () => tcs.Task,
+                        }
+                    },
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            // Rendering should finish synchronously
+            Assert.True(renderTask.IsCompletedSuccessfully);
+            Assert.Empty(renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public void AsynchronousCancelledTasks_HandleAfterRender_Works()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var tcs = new TaskCompletionSource<(int, NestedAsyncComponent.EventType)>();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
+                            EventAction = () => tcs.Task,
+                        }
+                    },
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            // Rendering should be complete.
+            Assert.True(renderTask.IsCompletedSuccessfully);
+            tcs.TrySetCanceled();
+            Assert.Empty(renderer.HandledExceptions);
+        }
+
+        [Fact]
+        public async Task CanceledTasksInHandleAfterRender_AreIgnored()
+        {
+            // Arrange
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
+            var component = new NestedAsyncComponent();
+            var taskCompletionSource = new TaskCompletionSource<int>();
+            var cancellationTokenSource = new CancellationTokenSource();
+            cancellationTokenSource.Cancel();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new[]
+                    {
+                        new NestedAsyncComponent.ExecutionAction
+                        {
+                            Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
+                            EventAction = () =>
+                            {
+                                taskCompletionSource.TrySetResult(0);
+                                cancellationTokenSource.Token.ThrowIfCancellationRequested();
+                                return default;
+                            },
+                        }
+                    },
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(Array.Empty<int>()),
+                },
+            }));
+
+            await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
+
+            Assert.Empty(renderer.HandledExceptions);
+        }
+
         [Fact]
         public void DisposingRenderer_DisposesTopLevelComponents()
         {
@@ -1416,7 +1910,7 @@ namespace Microsoft.AspNetCore.Components.Test
         public void DisposingRenderer_CapturesExceptionsFromAllRegisteredComponents()
         {
             // Arrange
-            var renderer = new TestRenderer();
+            var renderer = new TestRenderer { ShouldHandleExceptions = true };
             var exception1 = new Exception();
             var exception2 = new Exception();
             var component = new TestComponent(builder =>
@@ -1434,13 +1928,13 @@ namespace Microsoft.AspNetCore.Components.Test
             component.TriggerRender();
 
             // Act &A Assert
-            var aggregate = Assert.Throws<AggregateException>(renderer.Dispose);
+            renderer.Dispose();
 
             // All components must be disposed even if some throw as part of being diposed.
             Assert.True(component.Disposed);
-            Assert.Equal(2, aggregate.InnerExceptions.Count);
-            Assert.Contains(exception1, aggregate.InnerExceptions);
-            Assert.Contains(exception2, aggregate.InnerExceptions);
+            Assert.Equal(2, renderer.HandledExceptions.Count);
+            Assert.Contains(exception1, renderer.HandledExceptions);
+            Assert.Contains(exception2, renderer.HandledExceptions);
         }
 
         private class NoOpRenderer : Renderer
@@ -1452,6 +1946,9 @@ namespace Microsoft.AspNetCore.Components.Test
             public new int AssignRootComponentId(IComponent component)
                 => base.AssignRootComponentId(component);
 
+            protected override void HandleException(Exception exception)
+                => throw new NotImplementedException();
+
             protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
                 => Task.CompletedTask;
         }
@@ -1572,9 +2069,10 @@ namespace Microsoft.AspNetCore.Components.Test
                 builder.AddContent(6, $"Render count: {++renderCount}");
             }
 
-            public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
+            public Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
             {
                 binding.Invoke(args);
+                return Task.CompletedTask;
             }
         }
 
@@ -1642,9 +2140,9 @@ namespace Microsoft.AspNetCore.Components.Test
                 return Task.CompletedTask;
             }
 
-            public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
+            public async Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
             {
-                var task = binding.Invoke(args);
+                await binding.Invoke(args);
                 Render();
             }
 
@@ -1687,10 +2185,11 @@ namespace Microsoft.AspNetCore.Components.Test
             public bool CheckboxEnabled;
             public string SomeStringProperty;
 
-            public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
+            public Task HandleEventAsync(EventHandlerInvoker binding, UIEventArgs args)
             {
                 binding.Invoke(args);
                 TriggerRender();
+                return Task.CompletedTask;
             }
 
             protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -1712,9 +2211,10 @@ namespace Microsoft.AspNetCore.Components.Test
         {
             public int OnAfterRenderCallCount { get; private set; }
 
-            public void OnAfterRender()
+            public Task OnAfterRenderAsync()
             {
                 OnAfterRenderCallCount++;
+                return Task.CompletedTask;
             }
 
             Task IComponent.SetParametersAsync(ParameterCollection parameters)
@@ -1761,11 +2261,14 @@ namespace Microsoft.AspNetCore.Components.Test
         {
             private RenderHandle _renderHandler;
 
-            public AsyncComponent(int number)
+            public AsyncComponent(Task taskToAwait, int number)
             {
+                _taskToAwait = taskToAwait;
                 Number = number;
             }
 
+            private readonly Task _taskToAwait;
+
             public int Number { get; set; }
 
             public void Configure(RenderHandle renderHandle)
@@ -1781,7 +2284,7 @@ namespace Microsoft.AspNetCore.Components.Test
                     n = Number;
                     _renderHandler.Render(CreateFragment);
                     Number--;
-                    await Task.Yield();
+                    await _taskToAwait;
                 };
 
                 // Cheap closure
@@ -1833,7 +2336,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
             return component => builder =>
             {
-                int s = 0;
+                var s = 0;
                 builder.OpenElement(s++, "div");
                 builder.AddContent(s++, $"Id: {component.TestId} BuildRenderTree, {Guid.NewGuid()}");
                 foreach (var child in childrenToRender)
@@ -1852,13 +2355,6 @@ namespace Microsoft.AspNetCore.Components.Test
 
         private class NestedAsyncComponent : ComponentBase
         {
-            private RenderHandle _renderHandle;
-
-            public void Configure(RenderHandle renderHandle)
-            {
-                _renderHandle = renderHandle;
-            }
-
             [Parameter] public IDictionary<int, IList<ExecutionAction>> EventActions { get; set; }
 
             [Parameter] public IDictionary<int, Func<NestedAsyncComponent, RenderFragment>> WhatToRender { get; set; }
@@ -1924,6 +2420,15 @@ namespace Microsoft.AspNetCore.Components.Test
                 renderFactory(this)(builder);
             }
 
+            protected override async Task OnAfterRenderAsync()
+            {
+                if (TryGetEntry(EventType.OnAfterRenderAsync, out var entry))
+                {
+                    var result = await entry.EventAction();
+                    LogResult(result);
+                }
+            }
+
             private bool TryGetEntry(EventType eventType, out ExecutionAction entry)
             {
                 var entries = EventActions[TestId];
@@ -1937,7 +2442,7 @@ namespace Microsoft.AspNetCore.Components.Test
 
             private void LogResult((int, EventType) entry)
             {
-                Log.Enqueue(entry);
+                Log?.Enqueue(entry);
             }
 
             public class ExecutionAction
@@ -1977,7 +2482,8 @@ namespace Microsoft.AspNetCore.Components.Test
                 OnInitAsyncAsync,
                 OnParametersSet,
                 OnParametersSetAsyncSync,
-                OnParametersSetAsyncAsync
+                OnParametersSetAsyncAsync,
+                OnAfterRenderAsync,
             }
         }
     }

+ 30 - 30
src/Components/Components/test/Rendering/HtmlRendererTests.cs

@@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         private static readonly Func<string, string> _encoder = (string t) => HtmlEncoder.Default.Encode(t);
 
         [Fact]
-        public void RenderComponent_CanRenderEmptyElement()
+        public void RenderComponentAsync_CanRenderEmptyElement()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -30,14 +30,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_CanRenderSimpleComponent()
+        public void RenderComponentAsync_CanRenderSimpleComponent()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -51,14 +51,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_HtmlEncodesContent()
+        public void RenderComponentAsync_HtmlEncodesContent()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
 
 
         [Fact]
-        public void RenderComponent_DoesNotEncodeMarkup()
+        public void RenderComponentAsync_DoesNotEncodeMarkup()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
@@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
 
 
         [Fact]
-        public void RenderComponent_CanRenderWithAttributes()
+        public void RenderComponentAsync_CanRenderWithAttributes()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -118,14 +118,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_HtmlEncodesAttributeValues()
+        public void RenderComponentAsync_HtmlEncodesAttributeValues()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -141,14 +141,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_CanRenderBooleanAttributes()
+        public void RenderComponentAsync_CanRenderBooleanAttributes()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -163,14 +163,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_DoesNotRenderBooleanAttributesWhenValueIsFalse()
+        public void RenderComponentAsync_DoesNotRenderBooleanAttributesWhenValueIsFalse()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -185,14 +185,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_CanRenderWithChildren()
+        public void RenderComponentAsync_CanRenderWithChildren()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -209,14 +209,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_CanRenderWithMultipleChildren()
+        public void RenderComponentAsync_CanRenderWithMultipleChildren()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -240,14 +240,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_CanRenderComponentWithChildrenComponents()
+        public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -270,14 +270,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_ComponentReferenceNoops()
+        public void RenderComponentAsync_ComponentReferenceNoops()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -301,14 +301,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_CanPassParameters()
+        public void RenderComponentAsync_CanPassParameters()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
             Action<UIChangeEventArgs> change = (UIChangeEventArgs changeArgs) => throw new InvalidOperationException();
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<ComponentWithParameters>(
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<ComponentWithParameters>(
                 new ParameterCollection(new[] {
                     RenderTreeFrame.Element(0,string.Empty),
                     RenderTreeFrame.Attribute(1,"update",change),
@@ -345,7 +345,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         }
 
         [Fact]
-        public void RenderComponent_CanRenderComponentWithRenderFragmentContent()
+        public void RenderComponentAsync_CanRenderComponentAsyncWithRenderFragmentContent()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -365,14 +365,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);
         }
 
         [Fact]
-        public void RenderComponent_ElementRefsNoops()
+        public void RenderComponentAsync_ElementRefsNoops()
         {
             // Arrange
             var dispatcher = Renderer.CreateDefaultDispatcher();
@@ -393,7 +393,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder, dispatcher);
 
             // Act
-            var result = GetResult(dispatcher.Invoke(() => htmlRenderer.RenderComponent<TestComponent>(ParameterCollection.Empty)));
+            var result = GetResult(dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty)));
 
             // Assert
             Assert.Equal(expectedHtml, result);

+ 1 - 1
src/Components/Server/src/Circuits/CircuitHost.cs

@@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 for (var i = 0; i < builder.Entries.Count; i++)
                 {
                     var (componentType, domElementSelector) = builder.Entries[i];
-                    Renderer.AddComponent(componentType, domElementSelector);
+                    await Renderer.AddComponentAsync(componentType, domElementSelector);
                 }
 
                 for (var i = 0; i < _circuitHandlers.Length; i++)

+ 7 - 2
src/Components/Server/src/Circuits/DefaultCircuitFactory.cs

@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.SignalR;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 
 namespace Microsoft.AspNetCore.Components.Server.Circuits
@@ -17,10 +18,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
     {
         private readonly IServiceScopeFactory _scopeFactory;
         private readonly DefaultCircuitFactoryOptions _options;
+        private readonly ILoggerFactory _loggerFactory;
 
         public DefaultCircuitFactory(
             IServiceScopeFactory scopeFactory,
-            IOptions<DefaultCircuitFactoryOptions> options)
+            IOptions<DefaultCircuitFactoryOptions> options,
+            ILoggerFactory loggerFactory)
         {
             if (options == null)
             {
@@ -29,6 +32,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
             _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
             _options = options.Value;
+            _loggerFactory = loggerFactory;
         }
 
         public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientProxy client)
@@ -48,7 +52,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 rendererRegistry,
                 jsRuntime,
                 client,
-                dispatcher);
+                dispatcher,
+                _loggerFactory.CreateLogger<RemoteRenderer>());
 
             var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
                 .OrderBy(h => h.Order)

+ 24 - 15
src/Components/Server/src/Circuits/RemoteRenderer.cs

@@ -7,8 +7,10 @@ using System.Threading;
 using System.Threading.Tasks;
 using MessagePack;
 using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.Server;
 using Microsoft.AspNetCore.Components.Server.Circuits;
 using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Logging;
 using Microsoft.JSInterop;
 
 namespace Microsoft.AspNetCore.Components.Browser.Rendering
@@ -25,6 +27,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
         private readonly RendererRegistry _rendererRegistry;
         private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
             = new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
+        private readonly ILogger _logger;
         private long _nextRenderId = 1;
 
         /// <summary>
@@ -45,7 +48,8 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
             RendererRegistry rendererRegistry,
             IJSRuntime jsRuntime,
             IClientProxy client,
-            IDispatcher dispatcher)
+            IDispatcher dispatcher,
+            ILogger logger)
             : base(serviceProvider, dispatcher)
         {
             _rendererRegistry = rendererRegistry;
@@ -53,18 +57,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
             _client = client;
 
             _id = _rendererRegistry.Add(this);
-        }
-
-        /// <summary>
-        /// Attaches a new root component to the renderer,
-        /// causing it to be displayed in the specified DOM element.
-        /// </summary>
-        /// <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
-        {
-            AddComponent(typeof(TComponent), domElementSelector);
+            _logger = logger;
         }
 
         /// <summary>
@@ -73,7 +66,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
         /// </summary>
         /// <param name="componentType">The type of the component.</param>
         /// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
-        public void AddComponent(Type componentType, string domElementSelector)
+        public Task AddComponentAsync(Type componentType, string domElementSelector)
         {
             var component = InstantiateComponent(componentType);
             var componentId = AssignRootComponentId(component);
@@ -85,7 +78,23 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
                 componentId);
             CaptureAsyncExceptions(attachComponentTask);
 
-            RenderRootComponent(componentId);
+            return RenderRootComponentAsync(componentId);
+        }
+
+        /// <inheritdoc />
+        protected override void HandleException(Exception exception)
+        {
+            if (exception is AggregateException aggregateException)
+            {
+                foreach (var innerException in aggregateException.Flatten().InnerExceptions)
+                {
+                    _logger.UnhandledExceptionRenderingComponent(innerException);
+                }
+            }
+            else
+            {
+                _logger.UnhandledExceptionRenderingComponent(exception);
+            }
         }
 
         /// <inheritdoc />

+ 29 - 0
src/Components/Server/src/LoggerExtensions.cs

@@ -0,0 +1,29 @@
+// 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 Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Components.Server
+{
+    internal static class LoggerExtensions
+    {
+        private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
+
+        static LoggerExtensions()
+        {
+            _unhandledExceptionRenderingComponent = LoggerMessage.Define<string>(
+                LogLevel.Warning,
+                new EventId(1, "ExceptionRenderingComponent"),
+                "Unhandled exception rendering component: {Message}");
+        }
+
+        public static void UnhandledExceptionRenderingComponent(this ILogger logger, Exception exception)
+        {
+            _unhandledExceptionRenderingComponent(
+                logger,
+                exception.Message,
+                exception);
+        }
+    }
+}

+ 2 - 1
src/Components/Server/test/Circuits/CircuitHostTest.cs

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Components.Browser;
 using Microsoft.AspNetCore.Components.Browser.Rendering;
 using Microsoft.AspNetCore.SignalR;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.JSInterop;
 using Moq;
 using Xunit;
@@ -155,7 +156,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
         private class TestRemoteRenderer : RemoteRenderer
         {
             public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
-                : base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher())
+                : base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher(), NullLogger.Instance)
             {
             }
 

+ 5 - 0
src/Components/Server/test/Circuits/RenderBatchWriterTest.cs

@@ -377,6 +377,11 @@ namespace Microsoft.AspNetCore.Components.Server
             {
             }
 
+            protected override void HandleException(Exception exception)
+            {
+                throw new NotImplementedException();
+            }
+
             protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
                 => throw new NotImplementedException();
         }

+ 32 - 8
src/Components/Shared/test/TestRenderer.cs

@@ -6,7 +6,6 @@ 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;
 
@@ -31,11 +30,18 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
         public List<CapturedBatch> Batches { get; }
             = new List<CapturedBatch>();
 
+        public List<Exception> HandledExceptions { get; } = new List<Exception>();
+
+        public bool ShouldHandleExceptions { get; set; }
+
         public new int AssignRootComponentId(IComponent component)
             => base.AssignRootComponentId(component);
 
-        public new void RenderRootComponent(int componentId)
-            => Invoke(() => base.RenderRootComponent(componentId));
+        public void RenderRootComponent(int componentId, ParameterCollection? parameters = default)
+        {
+            var task = InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterCollection.Empty));
+            UnwrapTask(task);
+        }
 
         public new Task RenderRootComponentAsync(int componentId)
             => InvokeAsync(() => base.RenderRootComponentAsync(componentId));
@@ -43,25 +49,43 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
         public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
             => InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
 
-        public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
+        public new Task DispatchEventAsync(int componentId, int eventHandlerId, UIEventArgs args)
+        {
+            var task = InvokeAsync(() => base.DispatchEventAsync(componentId, eventHandlerId, args));
+            return UnwrapTask(task);
+        }
+
+        private static Task UnwrapTask(Task task)
         {
-            var t = Invoke(() => base.DispatchEvent(componentId, eventHandlerId, args));
             // This should always be run synchronously
-            Assert.True(t.IsCompleted);
-            if (t.IsFaulted)
+            Assert.True(task.IsCompleted);
+            if (task.IsFaulted)
             {
-                var exception = t.Exception.Flatten().InnerException;
+                var exception = task.Exception.Flatten().InnerException;
                 while (exception is AggregateException e)
                 {
                     exception = e.InnerException;
                 }
+
                 ExceptionDispatchInfo.Capture(exception).Throw();
             }
+
+            return task;
         }
 
         public T InstantiateComponent<T>() where T : IComponent
             => (T)InstantiateComponent(typeof(T));
 
+        protected override void HandleException(Exception exception)
+        {
+            if (!ShouldHandleExceptions)
+            {
+                throw exception;
+            }
+
+            HandledExceptions.Add(exception);
+        }
+
         protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
         {
             OnUpdateDisplay?.Invoke(renderBatch);

+ 2 - 3
src/Components/test/E2ETest/Infrastructure/SeleniumStandaloneServer.cs

@@ -107,14 +107,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
                     {
 
                     }
-                    await Task.Delay(1000);
-
+                    await Task.Delay(1000); 
                 }
             });
 
             try
             {
-                waitForStart.TimeoutAfter(Timeout).Wait();
+                waitForStart.TimeoutAfter(Timeout).Wait(1000);
             }
             catch (Exception ex)
             {

+ 1 - 1
src/Servers/Kestrel/test/Interop.FunctionalTests/H2SpecTests.cs

@@ -23,7 +23,7 @@ namespace Interop.FunctionalTests
         SkipReason = "Missing Windows ALPN support: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation#Support")]
     public class H2SpecTests : LoggedTest
     {
-        [ConditionalTheory]
+        [ConditionalTheory(Skip = "https://github.com/aspnet/AspNetCore/issues/6691")]
         [MemberData(nameof(H2SpecTestCases))]
         [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7299
         public async Task RunIndividualTestCase(H2SpecTestCase testCase)