Browse Source

[Components] Support for prerrendering asynchronous components.
* Updates the IComponent interface to rename Init into Configure
* Updates the IComponent interface to change SetParameters for
SetParametersAsync and make it return a Task that represents when the
component is done applying the parameters and potentially triggering
one or more renders.
* Updates ComponentBase SetParametersAsync to ensure that OnInit(Async)
runs before OnParametersSet(Async).
* Introduces ParameterCollection.FromDictionary to generate a parameter
collection from a dictionary of key value pairs.
* Introduces RenderComponentAsync on HtmlRenderer to support
prerrendering of async components.
* Introduces RenderRootComponentAsync on the renderer to allow for
asynchronous prerrendering of the root component.

Javier Calvarro Nelson 7 years ago
parent
commit
19b543e45f
28 changed files with 1106 additions and 118 deletions
  1. 6 2
      src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs
  2. 3 1
      src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs
  3. 4 2
      src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs
  4. 8 1
      src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs
  5. 7 4
      src/Components/Components/src/CascadingValue.cs
  6. 104 19
      src/Components/Components/src/ComponentBase.cs
  7. 6 3
      src/Components/Components/src/IComponent.cs
  8. 4 2
      src/Components/Components/src/Layouts/LayoutDisplay.cs
  9. 21 0
      src/Components/Components/src/ParameterCollection.cs
  10. 4 2
      src/Components/Components/src/Rendering/ComponentState.cs
  11. 46 0
      src/Components/Components/src/Rendering/HtmlRenderer.cs
  12. 191 8
      src/Components/Components/src/Rendering/Renderer.cs
  13. 4 2
      src/Components/Components/src/Routing/NavLink.cs
  14. 4 2
      src/Components/Components/src/Routing/Router.cs
  15. 4 3
      src/Components/Components/test/CascadingParameterStateTest.cs
  16. 4 3
      src/Components/Components/test/CascadingParameterTest.cs
  17. 13 13
      src/Components/Components/test/ComponentBaseTest.cs
  18. 4 3
      src/Components/Components/test/DependencyInjectionTest.cs
  19. 3 2
      src/Components/Components/test/ParameterCollectionAssignmentExtensionsTest.cs
  20. 36 5
      src/Components/Components/test/ParameterCollectionTest.cs
  21. 2 2
      src/Components/Components/test/RenderTreeBuilderTest.cs
  22. 12 12
      src/Components/Components/test/RenderTreeDiffBuilderTest.cs
  23. 492 13
      src/Components/Components/test/RendererTest.cs
  24. 107 6
      src/Components/Components/test/Rendering/HtmlRendererTests.cs
  25. 2 2
      src/Components/Server/test/Circuits/RenderBatchWriterTest.cs
  26. 4 2
      src/Components/Shared/test/AutoRenderComponent.cs
  27. 5 4
      src/Components/Shared/test/IComponentExtensions.cs
  28. 6 0
      src/Components/Shared/test/TestRenderer.cs

+ 6 - 2
src/Components/Blazor/Build/test/BindRazorIntegrationTest.cs

@@ -62,14 +62,16 @@ namespace Test
             // Arrange
             AdditionalSyntaxTrees.Add(Parse(@"
 using System;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 
 namespace Test
 {
     public class MyComponent : ComponentBase, IComponent
     {
-        void IComponent.SetParameters(ParameterCollection parameters)
+        Task IComponent.SetParametersAsync(ParameterCollection parameters)
         {
+            return Task.CompletedTask;
         }
     }
 }"));
@@ -136,14 +138,16 @@ namespace Test
             // Arrange
             AdditionalSyntaxTrees.Add(Parse(@"
 using System;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 
 namespace Test
 {
     public class MyComponent : ComponentBase, IComponent
     {
-        void IComponent.SetParameters(ParameterCollection parameters)
+        Task IComponent.SetParametersAsync(ParameterCollection parameters)
         {
+            return Task.CompletedTask;
         }
     }
 }"));

+ 3 - 1
src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs

@@ -162,14 +162,16 @@ namespace Test
         {
             // Arrange
             AdditionalSyntaxTrees.Add(Parse(@"
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 
 namespace Test
 {
     public class MyComponent : ComponentBase, IComponent
     {
-        void IComponent.SetParameters(ParameterCollection parameters)
+        Task IComponent.SetParametersAsync(ParameterCollection parameters)
         {
+            return Task.CompletedTask;
         }
     }
 }

+ 4 - 2
src/Components/Blazor/Build/test/DirectiveRazorIntegrationTest.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Linq;
 using System.Reflection;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Layouts;
 using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -149,12 +150,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
             [Parameter]
             RenderFragment Body { get; set; }
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
             }
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
+                return Task.CompletedTask;
             }
         }
 

+ 8 - 1
src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Runtime.ExceptionServices;
 using System.Runtime.InteropServices;
 using System.Text;
 using System.Threading;
@@ -376,7 +377,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
         {
             var renderer = new TestRenderer();
             renderer.AttachComponent(component);
-            component.SetParameters(ParameterCollection.Empty);
+            var task = component.SetParametersAsync(ParameterCollection.Empty);
+            // we will have to change this method if we add a test that does actual async work.
+            Assert.True(task.Status.HasFlag(TaskStatus.RanToCompletion) || task.Status.HasFlag(TaskStatus.Faulted));
+            if (task.IsFaulted)
+            {
+                ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
+            }
             return renderer.LatestBatchReferenceFrames;
         }
 

+ 7 - 4
src/Components/Components/src/CascadingValue.cs

@@ -1,10 +1,11 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-using Microsoft.AspNetCore.Components.Rendering;
-using Microsoft.AspNetCore.Components.RenderTree;
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.RenderTree;
 
 namespace Microsoft.AspNetCore.Components
 {
@@ -49,13 +50,13 @@ namespace Microsoft.AspNetCore.Components
         bool ICascadingValueComponent.CurrentValueIsFixed => IsFixed;
 
         /// <inheritdoc />
-        public void Init(RenderHandle renderHandle)
+        public void Configure(RenderHandle renderHandle)
         {
             _renderHandle = renderHandle;
         }
 
         /// <inheritdoc />
-        public void SetParameters(ParameterCollection parameters)
+        public Task SetParametersAsync(ParameterCollection parameters)
         {
             // Implementing the parameter binding manually, instead of just calling
             // parameters.SetParameterProperties(this), is just a very slight perf optimization
@@ -129,6 +130,8 @@ namespace Microsoft.AspNetCore.Components
             {
                 NotifySubscribers();
             }
+
+            return Task.CompletedTask;
         }
 
         bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName)

+ 104 - 19
src/Components/Components/src/ComponentBase.cs

@@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Components
         protected Task InvokeAsync(Func<Task> workItem)
             => _renderHandle.InvokeAsync(workItem);
 
-        void IComponent.Init(RenderHandle renderHandle)
+        void IComponent.Configure(RenderHandle renderHandle)
         {
             // This implicitly means a ComponentBase can only be associated with a single
             // renderer. That's the only use case we have right now. If there was ever a need,
@@ -174,26 +174,106 @@ namespace Microsoft.AspNetCore.Components
         /// Method invoked to apply initial or updated parameters to the component.
         /// </summary>
         /// <param name="parameters">The parameters to apply.</param>
-        public virtual void SetParameters(ParameterCollection parameters)
+        public virtual Task SetParametersAsync(ParameterCollection parameters)
         {
             parameters.SetParameterProperties(this);
-
             if (!_hasCalledInit)
             {
-                _hasCalledInit = true;
-                OnInit();
+                return RunInitAndSetParameters();
+            }
+            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;
+                }
 
-                // If you override OnInitAsync and return a noncompleted task, then by default
-                // we automatically re-render once that task completes.
-                var initTask = OnInitAsync();
-                ContinueAfterLifecycleTask(initTask);
+                return Task.CompletedTask;
             }
+        }
 
-            OnParametersSet();
-            var parametersTask = OnParametersSetAsync();
-            ContinueAfterLifecycleTask(parametersTask);
+        private async Task RunInitAndSetParameters()
+        {
+            _hasCalledInit = true;
+            var initIsAsync = false;
+
+            OnInit();
+            Task initTask = null;
+            (initIsAsync, initTask) = ProcessLifeCycletask(OnInitAsync());
+            if (initIsAsync)
+            {
+                // 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.
+                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;
+            }
+        }
+
+        private (bool isAsync, Task asyncTask) ProcessLifeCycletask(Task task)
+        {
+            if (task == null)
+            {
+                throw new ArgumentNullException(nameof(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:
+                    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));
+            }
+        }
+
+        private async Task ReRenderAsyncTask(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)
@@ -260,19 +340,24 @@ namespace Microsoft.AspNetCore.Components
             var onAfterRenderTask = OnAfterRenderAsync();
             if (onAfterRenderTask != null && onAfterRenderTask.Status != TaskStatus.RanToCompletion)
             {
-                onAfterRenderTask.ContinueWith(task =>
-                {
                 // 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);
+            }
+        }
 
-                if (task.Exception != null)
-                    {
-                        HandleException(task.Exception);
-                    }
-                });
+        private async Task HandleAfterRenderException(Task parentTask)
+        {
+            try
+            {
+                await parentTask;
+            }
+            catch (Exception e)
+            {
+                HandleException(e);
             }
         }
     }

+ 6 - 3
src/Components/Components/src/IComponent.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>
@@ -12,12 +14,13 @@ namespace Microsoft.AspNetCore.Components
         /// Initializes the component.
         /// </summary>
         /// <param name="renderHandle">A <see cref="RenderHandle"/> that allows the component to be rendered.</param>
-        void Init(RenderHandle renderHandle);
+        void Configure(RenderHandle renderHandle);
 
         /// <summary>
         /// Sets parameters supplied by the component's parent in the render tree.
         /// </summary>
         /// <param name="parameters">The parameters.</param>
-        void SetParameters(ParameterCollection parameters);
+        /// <returns>A <see cref="Task"/> that completes when the component has finished updating and rendering itself.</returns>
+        Task SetParametersAsync(ParameterCollection parameters);
     }
 }

+ 4 - 2
src/Components/Components/src/Layouts/LayoutDisplay.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 
@@ -34,16 +35,17 @@ namespace Microsoft.AspNetCore.Components.Layouts
         IDictionary<string, object> PageParameters { get; set; }
 
         /// <inheritdoc />
-        public void Init(RenderHandle renderHandle)
+        public void Configure(RenderHandle renderHandle)
         {
             _renderHandle = renderHandle;
         }
 
         /// <inheritdoc />
-        public void SetParameters(ParameterCollection parameters)
+        public Task SetParametersAsync(ParameterCollection parameters)
         {
             parameters.SetParameterProperties(this);
             Render();
+            return Task.CompletedTask;
         }
 
         private void Render()

+ 21 - 0
src/Components/Components/src/ParameterCollection.cs

@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Components
     /// </summary>
     public readonly struct ParameterCollection
     {
+        private const string GeneratedParameterCollectionElementName = "__ARTIFICIAL_PARAMETER_COLLECTION";
         private static readonly RenderTreeFrame[] _emptyCollectionFrames = new RenderTreeFrame[]
         {
             RenderTreeFrame.Element(0, string.Empty).WithComponentSubtreeLength(1)
@@ -196,5 +197,25 @@ namespace Microsoft.AspNetCore.Components
                 builder.Append(_frames, _ownerIndex + 1, numEntries);
             }
         }
+
+        /// <summary>
+        /// Creates a new <see cref="ParameterCollection"/> from the given <see cref="IDictionary{TKey, TValue}"/>.
+        /// </summary>
+        /// <param name="parameters">The <see cref="IDictionary{TKey, TValue}"/> with the parameters.</param>
+        /// <returns>A <see cref="ParameterCollection"/>.</returns>
+        public static ParameterCollection FromDictionary(IDictionary<string, object> parameters)
+        {
+            var frames = new RenderTreeFrame[parameters.Count + 1];
+            frames[0] = RenderTreeFrame.Element(0, GeneratedParameterCollectionElementName)
+                .WithElementSubtreeLength(frames.Length);
+
+            var i = 0;
+            foreach (var kvp in parameters)
+            {
+                frames[++i] = RenderTreeFrame.Attribute(i, kvp.Key, kvp.Value);
+            }
+
+            return new ParameterCollection(frames, 0);
+        }
     }
 }

+ 4 - 2
src/Components/Components/src/Rendering/ComponentState.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 
@@ -139,7 +140,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 parameters = parameters.WithCascadingParameters(_cascadingParameters);
             }
 
-            Component.SetParameters(parameters);
+            _renderer.AddToPendingTasks(Component.SetParametersAsync(parameters));
         }
 
         public void NotifyCascadingValueChanged()
@@ -148,7 +149,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 ? new ParameterCollection(_latestDirectParametersSnapshot.Buffer, 0)
                 : ParameterCollection.Empty;
             var allParams = directParams.WithCascadingParameters(_cascadingParameters);
-            Component.SetParameters(allParams);
+            var task = Component.SetParametersAsync(allParams);
+            _renderer.AddToPendingTasks(task);
         }
 
         private bool AddCascadingParameterSubscriptions()

+ 46 - 0
src/Components/Components/src/Rendering/HtmlRenderer.cs

@@ -73,6 +73,42 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
+        /// <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>
+        public async Task<IEnumerable<string>> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
+        {
+            var frames = await CreateInitialRenderAsync(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>
+        /// <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 Task<IEnumerable<string>> RenderComponentAsync<T>(ParameterCollection initialParameters) where T : IComponent
+        {
+            return RenderComponentAsync(typeof(T), initialParameters);
+        }
+
         private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
         {
             var nextPosition = position;
@@ -229,6 +265,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
 
             return GetCurrentRenderTreeFrames(componentId);
         }
+
+        private async Task<ArrayRange<RenderTreeFrame>> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
+        {
+            var component = InstantiateComponent(componentType);
+            var componentId = AssignRootComponentId(component);
+
+            await RenderRootComponentAsync(componentId, initialParameters);
+
+            return GetCurrentRenderTreeFrames(componentId);
+        }
     }
 }
 

+ 191 - 8
src/Components/Components/src/Rendering/Renderer.cs

@@ -1,10 +1,12 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-using Microsoft.AspNetCore.Components.RenderTree;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.ExceptionServices;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.RenderTree;
 
 namespace Microsoft.AspNetCore.Components.Rendering
 {
@@ -22,6 +24,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
         private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
         private bool _isBatchInProgress;
         private int _lastEventHandlerId = 0;
+        private List<Task> _pendingTasks;
+
+        // We need to introduce locking as we don't know if we are executing
+        // under a synchronization context that limits the ammount of concurrency
+        // that can happen when async callbacks are executed.
+        // As a result, we have to protect the _pendingTask list and the
+        // _batchBuilder render queue from concurrent modifications.
+        private object _asyncWorkLock = new object();
 
         /// <summary>
         /// Constructs an instance of <see cref="Renderer"/>.
@@ -64,8 +74,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
         protected void RenderRootComponent(int componentId)
         {
-            GetRequiredComponentState(componentId)
-                .SetDirectParameters(ParameterCollection.Empty);
+            RenderRootComponent(componentId, ParameterCollection.Empty);
         }
 
         /// <summary>
@@ -77,8 +86,131 @@ namespace Microsoft.AspNetCore.Components.Rendering
         /// <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
+        /// done by any of the components. 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 Task RenderRootComponentAsync(int componentId)
+        {
+            return RenderRootComponentAsync(componentId, ParameterCollection.Empty);
+        }
+
+        /// <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
+        /// done by any of the components. 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 async Task RenderRootComponentAsync(int componentId, ParameterCollection initialParameters)
+        {
+            if (_pendingTasks != 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
+            // remaining work.
+            // During the synchronous rendering process we don't wait for the pending asynchronous
+            // 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);
+
+            try
+            {
+                await ProcessAsynchronousWork();
+                Debug.Assert(_pendingTasks.Count == 0);
+            }
+            finally
+            {
+                _pendingTasks = null;
+            }
+        }
+
+        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;
+                lock (_asyncWorkLock)
+                {
+                    // Create a Task that represents the remaining ongoing work for the rendering process
+                    pendingWork = Task.WhenAll(_pendingTasks);
+
+                    // Clear all pending work.
+                    _pendingTasks.Clear();
+                }
+
+                // new work might be added before we check again as a result of waiting for all
+                // the child components to finish executing SetParametersAsync
+                await pendingWork;
+            };
         }
 
         private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
@@ -87,7 +219,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
             var parentComponentState = GetOptionalComponentState(parentComponentId);
             var componentState = new ComponentState(this, componentId, component, parentComponentState);
             _componentStateById.Add(componentId, componentState);
-            component.Init(new RenderHandle(this, componentId));
+            component.Configure(new RenderHandle(this, componentId));
             return componentState;
         }
 
@@ -167,6 +299,38 @@ namespace Microsoft.AspNetCore.Components.Rendering
             frame = frame.WithComponent(newComponentState);
         }
 
+        internal void AddToPendingTasks(Task task)
+        {
+            switch (task == null ? TaskStatus.RanToCompletion : task.Status)
+            {
+                // If it's already completed synchronously, no need to add it to the list of
+                // pending Tasks as no further render (we already rerender synchronously) will.
+                // happen.
+                case TaskStatus.RanToCompletion:
+                case TaskStatus.Canceled:
+                    break;
+                case TaskStatus.Faulted:
+                    // We want to throw immediately 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();
+                    break;
+                default:
+                    // We are not in rendering the root component.
+                    if (_pendingTasks == null)
+                    {
+                        return;
+                    }
+                    lock (_asyncWorkLock)
+                    {
+                        _pendingTasks.Add(task);
+                    }
+                    break;
+            }
+        }
+
         internal void AssignEventHandlerId(ref RenderTreeFrame frame)
         {
             var id = ++_lastEventHandlerId;
@@ -195,8 +359,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
                 return;
             }
 
-            _batchBuilder.ComponentRenderQueue.Enqueue(
-                new RenderQueueEntry(componentState, renderFragment));
+            lock (_asyncWorkLock)
+            {
+                _batchBuilder.ComponentRenderQueue.Enqueue(
+                    new RenderQueueEntry(componentState, renderFragment));
+            }
 
             if (!_isBatchInProgress)
             {
@@ -222,9 +389,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
             try
             {
                 // Process render queue until empty
-                while (_batchBuilder.ComponentRenderQueue.Count > 0)
+                while (TryDequeueRenderQueueEntry(out var nextToRender))
                 {
-                    var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue();
                     RenderInExistingBatch(nextToRender);
                 }
 
@@ -240,6 +406,23 @@ namespace Microsoft.AspNetCore.Components.Rendering
             }
         }
 
+        private bool TryDequeueRenderQueueEntry(out RenderQueueEntry entry)
+        {
+            lock (_asyncWorkLock)
+            {
+                if (_batchBuilder.ComponentRenderQueue.Count > 0)
+                {
+                    entry = _batchBuilder.ComponentRenderQueue.Dequeue();
+                    return true;
+                }
+                else
+                {
+                    entry = default;
+                    return false;
+                }
+            }
+        }
+
         private void InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
         {
             var array = updatedComponents.Array;

+ 4 - 2
src/Components/Components/src/Routing/NavLink.cs

@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Services;
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading.Tasks;
 
 namespace Microsoft.AspNetCore.Components.Routing
 {
@@ -50,7 +51,7 @@ namespace Microsoft.AspNetCore.Components.Routing
         [Inject] private IUriHelper UriHelper { get; set; }
 
         /// <inheritdoc />
-        public void Init(RenderHandle renderHandle)
+        public void Configure(RenderHandle renderHandle)
         {
             _renderHandle = renderHandle;
 
@@ -59,7 +60,7 @@ namespace Microsoft.AspNetCore.Components.Routing
         }
 
         /// <inheritdoc />
-        public void SetParameters(ParameterCollection parameters)
+        public Task SetParametersAsync(ParameterCollection parameters)
         {
             // Capture the parameters we want to do special things with, plus all as a dictionary
             parameters.TryGetValue(RenderTreeBuilder.ChildContent, out _childContent);
@@ -73,6 +74,7 @@ namespace Microsoft.AspNetCore.Components.Routing
             _hrefAbsolute = href == null ? null : UriHelper.ToAbsoluteUri(href).AbsoluteUri;
             _isActive = ShouldMatch(UriHelper.GetAbsoluteUri());
             _renderHandle.Render(Render);
+            return Task.CompletedTask;
         }
 
         /// <inheritdoc />

+ 4 - 2
src/Components/Components/src/Routing/Router.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.Layouts;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Services;
@@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.Components.Routing
         private RouteTable Routes { get; set; }
 
         /// <inheritdoc />
-        public void Init(RenderHandle renderHandle)
+        public void Configure(RenderHandle renderHandle)
         {
             _renderHandle = renderHandle;
             _baseUri = UriHelper.GetBaseUri();
@@ -47,12 +48,13 @@ namespace Microsoft.AspNetCore.Components.Routing
         }
 
         /// <inheritdoc />
-        public void SetParameters(ParameterCollection parameters)
+        public Task SetParametersAsync(ParameterCollection parameters)
         {
             parameters.SetParameterProperties(this);
             var types = ComponentResolver.ResolveComponents(AppAssembly);
             Routes = RouteTable.Create(types);
             Refresh();
+            return Task.CompletedTask;
         }
 
         /// <inheritdoc />

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

@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Test.Helpers;
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading.Tasks;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test
@@ -372,7 +373,7 @@ namespace Microsoft.AspNetCore.Components.Test
         static CascadingValue<T> CreateCascadingValueComponent<T>(T value, string name = null)
         {
             var supplier = new CascadingValue<T>();
-            supplier.Init(new RenderHandle(new TestRenderer(), 0));
+            supplier.Configure(new RenderHandle(new TestRenderer(), 0));
 
             var supplierParams = new Dictionary<string, object>
             {
@@ -422,10 +423,10 @@ namespace Microsoft.AspNetCore.Components.Test
 
         class TestComponentBase : IComponent
         {
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => throw new NotImplementedException();
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
                 => throw new NotImplementedException();
         }
 

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

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Test.Helpers;
 using System;
 using System.Linq;
+using System.Threading.Tasks;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test
@@ -76,7 +77,7 @@ namespace Microsoft.AspNetCore.Components.Test
             var firstBatch = renderer.Batches.Single();
             var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
             Assert.Equal(1, nestedComponent.NumRenders);
-            
+
             // Act 2: Render again with updated regular parameter
             regularParameterValue = "Changed value";
             component.TriggerRender();
@@ -374,10 +375,10 @@ namespace Microsoft.AspNetCore.Components.Test
             [CascadingParameter] T CascadingParameter { get; set; }
             [Parameter] string RegularParameter { get; set; }
 
-            public override void SetParameters(ParameterCollection parameters)
+            public override async Task SetParametersAsync(ParameterCollection parameters)
             {
                 NumSetParametersCalls++;
-                base.SetParameters(parameters);
+                await base.SetParametersAsync(parameters);
             }
 
             protected override void BuildRenderTree(RenderTreeBuilder builder)

+ 13 - 13
src/Components/Components/test/ComponentBaseTest.cs

@@ -193,23 +193,22 @@ namespace Microsoft.AspNetCore.Components.Test
             // Assert
             Assert.Single(renderer.Batches);
 
-            // Completes task started by OnParametersSetAsync
+            // Completes task started by OnInitAsync
             component.Counter = 2;
-            parametersSetTask.SetResult(false);
+            initTask.SetResult(true);
 
-            // Component should be rendered again
-            Assert.Equal(2, renderer.Batches.Count);
+            // Component should be rendered again 2 times
+            // after on init async
+            // after set parameters
+            Assert.Equal(3, renderer.Batches.Count);
 
-            // Completes task started by OnInitAsync
-            // NOTE: We will probably change this behavior. It would make more sense for the base class
-            // to wait until InitAsync is completed before proceeding with SetParametersAsync, rather
-            // that running the two lifecycle methods in parallel. This will come up as a requirement
-            // when implementing async server-side prerendering.
+            // Completes task started by OnParametersSetAsync
             component.Counter = 3;
-            initTask.SetResult(true);
+            parametersSetTask.SetResult(false);
 
             // Component should be rendered again
-            Assert.Equal(3, renderer.Batches.Count);
+            // after the async part of onparameterssetasync completes
+            Assert.Equal(4, renderer.Batches.Count);
         }
 
         [Fact]
@@ -232,8 +231,9 @@ namespace Microsoft.AspNetCore.Components.Test
             component.Counter = 2;
             initTask.SetCanceled();
 
-            // Component should not be rendered again
-            Assert.Single(renderer.Batches);
+            // Component should only be rendered again due to
+            // the call to StateHasChanged after SetParametersAsync
+            Assert.Equal(2,renderer.Batches.Count);
         }
 
         [Fact]

+ 4 - 3
src/Components/Components/test/DependencyInjectionTest.cs

@@ -1,9 +1,10 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Test.Helpers;
 using System;
+using System.Threading.Tasks;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test
@@ -193,10 +194,10 @@ namespace Microsoft.AspNetCore.Components.Test
             // not throw, then be sure also to add a test to verify that injection
             // occurs before lifecycle methods.
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => throw new NotImplementedException();
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
                 => throw new NotImplementedException();
         }
     }

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

@@ -4,6 +4,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -298,10 +299,10 @@ namespace Microsoft.AspNetCore.Components.Test
 
         class FakeComponent : IComponent
         {
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => throw new NotImplementedException();
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
                 => throw new NotImplementedException();
         }
     }

+ 36 - 5
src/Components/Components/test/ParameterCollectionTest.cs

@@ -1,11 +1,11 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Rendering;
-using Microsoft.AspNetCore.Components.RenderTree;
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.RenderTree;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components.Test
@@ -239,6 +239,37 @@ namespace Microsoft.AspNetCore.Components.Test
             });
         }
 
+        [Fact]
+        public void FromDictionary_CanBeInitializedWithEmptyDictionary()
+        {
+            // Arrange
+            var dictionary = new Dictionary<string, object>();
+
+            // Act
+            var collection = ParameterCollection.FromDictionary(dictionary);
+
+            // Assert
+            Assert.Empty(collection.ToDictionary());
+        }
+
+        [Fact]
+        public void FromDictionary_RoundTrips()
+        {
+            // Arrange
+            var dictionary = new Dictionary<string, object>
+            {
+                ["IntValue"] = 1,
+                ["StringValue"] = "String"
+            };
+
+            // Act
+            var collection = ParameterCollection.FromDictionary(dictionary);
+
+            // Assert
+            Assert.Equal(dictionary, collection.ToDictionary());
+        }
+
+
         [Fact]
         public void CanConvertToReadOnlyDictionary()
         {
@@ -311,10 +342,10 @@ namespace Microsoft.AspNetCore.Components.Test
 
         private class FakeComponent : IComponent
         {
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => throw new NotImplementedException();
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
                 => throw new NotImplementedException();
         }
 

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

@@ -1049,9 +1049,9 @@ namespace Microsoft.AspNetCore.Components.Test
 
         private class TestComponent : IComponent
         {
-            public void Init(RenderHandle renderHandle) { }
+            public void Configure(RenderHandle renderHandle) { }
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
                 => throw new NotImplementedException();
         }
 

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

@@ -1554,35 +1554,35 @@ namespace Microsoft.AspNetCore.Components.Test
 
             public string NonParameterProperty { get; set; }
 
-            public void Init(RenderHandle renderHandle) { }
-            public void SetParameters(ParameterCollection parameters)
+            public void Configure(RenderHandle renderHandle) { }
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
                 parameters.SetParameterProperties(this);
+                return Task.CompletedTask;
             }
         }
 
         private class FakeComponent2 : IComponent
         {
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
             }
 
-            public void SetParameters(ParameterCollection parameters)
-            {
-            }
+            public Task SetParametersAsync(ParameterCollection parameters) => Task.CompletedTask;
         }
 
         private class CaptureSetParametersComponent : IComponent
         {
             public int SetParametersCallCount { get; private set; }
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
             }
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
                 SetParametersCallCount++;
+                return Task.CompletedTask;
             }
         }
 
@@ -1591,16 +1591,16 @@ namespace Microsoft.AspNetCore.Components.Test
             public int DisposalCount { get; private set; }
             public void Dispose() => DisposalCount++;
 
-            public void Init(RenderHandle renderHandle) { }
+            public void Configure(RenderHandle renderHandle) { }
 
-            public void SetParameters(ParameterCollection parameters) { }
+            public Task SetParametersAsync(ParameterCollection parameters) => Task.CompletedTask;
         }
 
         private class NonDisposableComponent : IComponent
         {
-            public void Init(RenderHandle renderHandle) { }
+            public void Configure(RenderHandle renderHandle) { }
 
-            public void SetParameters(ParameterCollection parameters) { }
+            public Task SetParametersAsync(ParameterCollection parameters) => Task.CompletedTask;
         }
 
         private static void AssertEdit(

+ 492 - 13
src/Components/Components/test/RendererTest.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.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
@@ -161,6 +162,251 @@ namespace Microsoft.AspNetCore.Components.Test
                 });
         }
 
+        [Fact]
+        public async Task CanRenderAsyncTopLevelComponents()
+        {
+            // 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.
+
+            // Act
+            var componentId = renderer.AssignRootComponentId(component);
+            await renderer.RenderRootComponentAsync(componentId);
+
+            // Assert
+            Assert.Equal(5, renderer.Batches.Count);
+
+            // First render
+            var create = renderer.Batches[0];
+            var diff = create.DiffsByComponentId[componentId].Single();
+            Assert.Collection(diff.Edits,
+                edit =>
+                {
+                    Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
+                    Assert.Equal(0, edit.ReferenceFrameIndex);
+                });
+            AssertFrame.Element(create.ReferenceFrames[0], "p", 2);
+            AssertFrame.Text(create.ReferenceFrames[1], "5");
+
+            // Second render
+            for (int i = 1; i < 5; i++)
+            {
+
+                var update = renderer.Batches[i];
+                var updateDiff = update.DiffsByComponentId[componentId].Single();
+                Assert.Collection(updateDiff.Edits,
+                    edit =>
+                    {
+                        Assert.Equal(RenderTreeEditType.StepIn, edit.Type);
+                    },
+                    edit =>
+                    {
+                        Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
+                    },
+                    edit =>
+                    {
+                        Assert.Equal(RenderTreeEditType.StepOut, edit.Type);
+                    });
+                AssertFrame.Text(update.ReferenceFrames[0], (5 - i).ToString());
+            }
+        }
+
+        [Fact]
+        public async Task CanRenderAsyncNestedComponents()
+        {
+            // Arrange
+            var renderer = new TestRenderer();
+            var component = new NestedAsyncComponent();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
+            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
+                    },
+                    [1] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1 }),
+                    [1] = CreateRenderFactory(Array.Empty<int>())
+                },
+                [nameof(NestedAsyncComponent.Log)] = log
+            }));
+
+            var logForParent = log.Where(l => l.id == 0).ToArray();
+            var logForChild = log.Where(l => l.id == 1).ToArray();
+
+            AssertStream(0, logForParent);
+            AssertStream(1, logForChild);
+        }
+
+        [Fact]
+        public async Task CanRenderAsyncComponentsWithSyncChildComponents()
+        {
+            // Arrange
+            var renderer = new TestRenderer();
+            var component = new NestedAsyncComponent();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
+            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
+                    },
+                    [1] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1 }),
+                    [1] = CreateRenderFactory(Array.Empty<int>())
+                },
+                [nameof(NestedAsyncComponent.Log)] = log
+            }));
+
+            var logForParent = log.Where(l => l.id == 0).ToArray();
+            var logForChild = log.Where(l => l.id == 1).ToArray();
+
+            AssertStream(0, logForParent);
+            AssertStream(1, logForChild);
+        }
+
+        [Fact]
+        public async Task CanRenderAsyncComponentsWithAsyncChildInit()
+        {
+            // Arrange
+            var renderer = new TestRenderer();
+            var component = new NestedAsyncComponent();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
+            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
+                    },
+                    [1] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync),
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1 }),
+                    [1] = CreateRenderFactory(Array.Empty<int>())
+                },
+                [nameof(NestedAsyncComponent.Log)] = log
+            }));
+
+            var logForParent = log.Where(l => l.id == 0).ToArray();
+            var logForChild = log.Where(l => l.id == 1).ToArray();
+
+            AssertStream(0, logForParent);
+            AssertStream(1, logForChild);
+        }
+
+        [Fact]
+        public async Task CanRenderAsyncComponentsWithMultipleAsyncChildren()
+        {
+            // Arrange
+            var renderer = new TestRenderer();
+            var component = new NestedAsyncComponent();
+
+            // Act/Assert
+            var componentId = renderer.AssignRootComponentId(component);
+            var log = new ConcurrentQueue<(int id, NestedAsyncComponent.EventType @event)>();
+            await renderer.RenderRootComponentAsync(componentId, ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                [nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
+                {
+                    [0] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(0, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async: true),
+                    },
+                    [1] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(1, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
+                    },
+                    [2] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(2, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
+                    },
+                    [3] = new List<NestedAsyncComponent.ExecutionAction>
+                    {
+                        NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnInit),
+                        NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnInitAsyncAsync, async:true),
+                        NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSet),
+                        NestedAsyncComponent.ExecutionAction.On(3, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync, async:true),
+                    }
+                },
+                [nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
+                {
+                    [0] = CreateRenderFactory(new[] { 1, 2 }),
+                    [1] = CreateRenderFactory(new[] { 3 }),
+                    [2] = CreateRenderFactory(Array.Empty<int>()),
+                    [3] = CreateRenderFactory(Array.Empty<int>())
+                },
+                [nameof(NestedAsyncComponent.Log)] = log
+            }));
+
+            var logForParent = log.Where(l => l.id == 0).ToArray();
+            var logForFirstChild = log.Where(l => l.id == 1).ToArray();
+            var logForSecondChild = log.Where(l => l.id == 2).ToArray();
+            var logForThirdChild = log.Where(l => l.id == 3).ToArray();
+
+            AssertStream(0, logForParent);
+            AssertStream(1, logForFirstChild);
+            AssertStream(2, logForSecondChild);
+            AssertStream(3, logForThirdChild);
+        }
+
         [Fact]
         public void CanDispatchEventsToTopLevelComponents()
         {
@@ -1233,13 +1479,16 @@ namespace Microsoft.AspNetCore.Components.Test
                 _renderFragment = renderFragment;
             }
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
                 _renderHandle = renderHandle;
             }
 
-            public void SetParameters(ParameterCollection parameters)
-                => TriggerRender();
+            public Task SetParametersAsync(ParameterCollection parameters)
+            {
+                TriggerRender();
+                return Task.CompletedTask;
+            }
 
             public void TriggerRender()
                 => _renderHandle.Render(_renderFragment);
@@ -1273,11 +1522,14 @@ namespace Microsoft.AspNetCore.Components.Test
 
             public RenderHandle RenderHandle { get; private set; }
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => RenderHandle = renderHandle;
 
-            public void SetParameters(ParameterCollection parameters)
-                => parameters.SetParameterProperties(this);
+            public Task SetParametersAsync(ParameterCollection parameters)
+            {
+                parameters.SetParameterProperties(this);
+                return Task.CompletedTask;
+            }
         }
 
         private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
@@ -1337,7 +1589,7 @@ namespace Microsoft.AspNetCore.Components.Test
             protected override void BuildRenderTree(RenderTreeBuilder builder)
             {
                 builder.AddContent(0, "Parent here");
-                
+
                 if (IncludeChild)
                 {
                     builder.OpenComponent<T>(1);
@@ -1353,7 +1605,7 @@ namespace Microsoft.AspNetCore.Components.Test
                 }
             }
         }
-        
+
         private class ReRendersParentComponent : AutoRenderComponent
         {
             [Parameter]
@@ -1380,13 +1632,14 @@ namespace Microsoft.AspNetCore.Components.Test
 
             private RenderHandle _renderHandle;
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => _renderHandle = renderHandle;
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
                 parameters.SetParameterProperties(this);
                 Render();
+                return Task.CompletedTask;
             }
 
             public void HandleEvent(EventHandlerInvoker binding, UIEventArgs args)
@@ -1409,11 +1662,12 @@ namespace Microsoft.AspNetCore.Components.Test
             private readonly List<RenderHandle> _renderHandles
                 = new List<RenderHandle>();
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => _renderHandles.Add(renderHandle);
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
+                return Task.CompletedTask;
             }
 
             public void TriggerRender()
@@ -1463,9 +1717,10 @@ namespace Microsoft.AspNetCore.Components.Test
                 OnAfterRenderCallCount++;
             }
 
-            void IComponent.SetParameters(ParameterCollection parameters)
+            Task IComponent.SetParametersAsync(ParameterCollection parameters)
             {
                 TriggerRender();
+                return Task.CompletedTask;
             }
 
             protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -1501,5 +1756,229 @@ namespace Microsoft.AspNetCore.Components.Test
                 return NextUpdateDisplayReturnTask;
             }
         }
+
+        private class AsyncComponent : IComponent
+        {
+            private RenderHandle _renderHandler;
+
+            public AsyncComponent(int number)
+            {
+                Number = number;
+            }
+
+            public int Number { get; set; }
+
+            public void Configure(RenderHandle renderHandle)
+            {
+                _renderHandler = renderHandle;
+            }
+
+            public async Task SetParametersAsync(ParameterCollection parameters)
+            {
+                int n;
+                while (Number > 0)
+                {
+                    n = Number;
+                    _renderHandler.Render(CreateFragment);
+                    Number--;
+                    await Task.Yield();
+                };
+
+                // Cheap closure
+                void CreateFragment(RenderTreeBuilder builder)
+                {
+                    var s = 0;
+                    builder.OpenElement(s++, "p");
+                    builder.AddContent(s++, n);
+                    builder.CloseElement();
+                }
+            }
+        }
+
+        private void AssertStream(int expectedId, (int id, NestedAsyncComponent.EventType @event)[] logStream)
+        {
+            // OnInit runs first
+            Assert.Equal((expectedId, NestedAsyncComponent.EventType.OnInit), logStream[0]);
+
+            // OnInit async completes
+            Assert.Single(logStream.Skip(1),
+                e => e == (expectedId, NestedAsyncComponent.EventType.OnInitAsyncAsync) || e == (expectedId, NestedAsyncComponent.EventType.OnInitAsyncSync));
+
+            var parametersSetEvent = logStream.Where(le => le == (expectedId, NestedAsyncComponent.EventType.OnParametersSet)).ToArray();
+            // OnParametersSet gets called at least once
+            Assert.NotEmpty(parametersSetEvent);
+
+            var parametersSetAsyncEvent = logStream
+                .Where(le => le == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync) ||
+                       le == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncSync))
+                .ToArray();
+            // OnParametersSetAsync async gets called at least once
+            Assert.NotEmpty(parametersSetAsyncEvent);
+
+            // The same number of OnParametersSet and OnParametersSetAsync get produced
+            Assert.Equal(parametersSetEvent.Length, parametersSetAsyncEvent.Length);
+
+            // The log ends with an OnParametersSetAsync event
+            Assert.True(logStream.Last() == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncSync) ||
+                logStream.Last() == (expectedId, NestedAsyncComponent.EventType.OnParametersSetAsyncAsync));
+        }
+
+        private Func<NestedAsyncComponent, RenderFragment> CreateRenderFactory(int[] childrenToRender)
+        {
+            // For some reason nameof doesn't work inside a nested lambda, so capturing the value here.
+            var eventActionsName = nameof(NestedAsyncComponent.EventActions);
+            var whatToRenderName = nameof(NestedAsyncComponent.WhatToRender);
+            var testIdName = nameof(NestedAsyncComponent.TestId);
+            var logName = nameof(NestedAsyncComponent.Log);
+
+            return component => builder =>
+            {
+                int s = 0;
+                builder.OpenElement(s++, "div");
+                builder.AddContent(s++, $"Id: {component.TestId} BuildRenderTree, {Guid.NewGuid()}");
+                foreach (var child in childrenToRender)
+                {
+                    builder.OpenComponent<NestedAsyncComponent>(s++);
+                    builder.AddAttribute(s++, eventActionsName, component.EventActions);
+                    builder.AddAttribute(s++, whatToRenderName, component.WhatToRender);
+                    builder.AddAttribute(s++, testIdName, child);
+                    builder.AddAttribute(s++, logName, component.Log);
+                    builder.CloseComponent();
+                }
+
+                builder.CloseElement();
+            };
+        }
+
+        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; }
+
+            [Parameter] public int TestId { get; set; }
+
+            [Parameter] public ConcurrentQueue<(int testId, EventType @event)> Log { get; set; }
+
+            protected override void OnInit()
+            {
+                if (TryGetEntry(EventType.OnInit, out var entry))
+                {
+                    var result = entry.EventAction();
+                    LogResult(result.Result);
+                }
+                base.OnInit();
+            }
+
+            protected override async Task OnInitAsync()
+            {
+                if (TryGetEntry(EventType.OnInitAsyncSync, out var entrySync))
+                {
+                    var result = await entrySync.EventAction();
+                    LogResult(result);
+                }
+                else if (TryGetEntry(EventType.OnInitAsyncAsync, out var entryAsync))
+                {
+                    var result = await entryAsync.EventAction();
+                    LogResult(result);
+                }
+            }
+
+            protected override void OnParametersSet()
+            {
+                if (TryGetEntry(EventType.OnParametersSet, out var entry))
+                {
+                    var result = entry.EventAction();
+                    LogResult(result.Result);
+                }
+                base.OnParametersSet();
+            }
+
+            protected override async Task OnParametersSetAsync()
+            {
+                if (TryGetEntry(EventType.OnParametersSetAsyncSync, out var entrySync))
+                {
+                    var result = await entrySync.EventAction();
+                    LogResult(result);
+
+                    await entrySync.EventAction();
+                }
+                else if (TryGetEntry(EventType.OnParametersSetAsyncAsync, out var entryAsync))
+                {
+                    var result = await entryAsync.EventAction();
+                    LogResult(result);
+                }
+            }
+
+            protected override void BuildRenderTree(RenderTreeBuilder builder)
+            {
+                base.BuildRenderTree(builder);
+                var renderFactory = WhatToRender[TestId];
+                renderFactory(this)(builder);
+            }
+
+            private bool TryGetEntry(EventType eventType, out ExecutionAction entry)
+            {
+                var entries = EventActions[TestId];
+                if (entries == null)
+                {
+                    throw new InvalidOperationException("Failed to find entries for component with Id: " + TestId);
+                }
+                entry = entries.FirstOrDefault(e => e.Event == eventType);
+                return entry != null;
+            }
+
+            private void LogResult((int, EventType) entry)
+            {
+                Log.Enqueue(entry);
+            }
+
+            public class ExecutionAction
+            {
+                public EventType Event { get; set; }
+                public Func<Task<(int id, EventType @event)>> EventAction { get; set; }
+
+                public static ExecutionAction On(int id, EventType @event, bool async = false)
+                {
+                    if (!async)
+                    {
+                        return new ExecutionAction
+                        {
+                            Event = @event,
+                            EventAction = () => Task.FromResult((id, @event))
+                        };
+                    }
+                    else
+                    {
+                        return new ExecutionAction
+                        {
+                            Event = @event,
+                            EventAction = async () =>
+                            {
+                                await Task.Yield();
+                                return (id, @event);
+                            }
+                        };
+                    }
+                }
+            }
+
+            public enum EventType
+            {
+                OnInit,
+                OnInitAsyncSync,
+                OnInitAsyncAsync,
+                OnParametersSet,
+                OnParametersSetAsyncSync,
+                OnParametersSetAsyncAsync
+            }
+        }
     }
 }

+ 107 - 6
src/Components/Components/test/Rendering/HtmlRendererTests.cs

@@ -2,7 +2,9 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Collections.Generic;
 using System.Text.Encodings.Web;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.Extensions.DependencyInjection;
 using Xunit;
@@ -385,7 +387,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
         {
             public RenderHandle RenderHandle { get; private set; }
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
                 RenderHandle = renderHandle;
             }
@@ -393,9 +395,106 @@ namespace Microsoft.AspNetCore.Components.Rendering
             [Inject]
             Func<ParameterCollection, RenderFragment> CreateRenderFragment { get; set; }
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
                 RenderHandle.Render(CreateRenderFragment(parameters));
+                return Task.CompletedTask;
+            }
+        }
+
+        [Fact]
+        public async Task CanRender_AsyncComponent()
+        {
+            // Arrange
+            var expectedHtml = new[] {
+                "<", "p", ">", "20", "</", "p", ">" };
+            var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
+
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+
+            // Act
+            var result = await htmlRenderer.RenderComponentAsync<AsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                ["Value"] = 10
+            }));
+
+            // Assert
+            Assert.Equal(expectedHtml, result);
+        }
+
+        [Fact]
+        public async Task CanRender_NestedAsyncComponents()
+        {
+            // Arrange
+            var expectedHtml = new[] {
+                "<", "p", ">", "20", "</", "p", ">",
+                "<", "p", ">", "80", "</", "p", ">"
+            };
+
+            var serviceProvider = new ServiceCollection().AddSingleton<AsyncComponent>().BuildServiceProvider();
+
+            var htmlRenderer = new HtmlRenderer(serviceProvider, _encoder);
+
+            // Act
+            var result = await htmlRenderer.RenderComponentAsync<NestedAsyncComponent>(ParameterCollection.FromDictionary(new Dictionary<string, object>
+            {
+                ["Nested"] = false,
+                ["Value"] = 10
+            }));
+
+            // Assert
+            Assert.Equal(expectedHtml, result);
+        }
+
+
+        private class NestedAsyncComponent : ComponentBase
+        {
+            [Parameter] public bool Nested { get; set; }
+            [Parameter] public int Value { get; set; }
+
+            protected override async Task OnInitAsync()
+            {
+                Value = Value * 2;
+                await Task.Yield();
+            }
+
+            protected override void BuildRenderTree(RenderTreeBuilder builder)
+            {
+                base.BuildRenderTree(builder);
+                builder.OpenElement(0, "p");
+                builder.AddContent(1, Value.ToString());
+                builder.CloseElement();
+                if (!Nested)
+                {
+                    builder.OpenComponent<NestedAsyncComponent>(2);
+                    builder.AddAttribute(3, "Nested", true);
+                    builder.AddAttribute(4, "Value", Value * 2);
+                    builder.CloseComponent();
+                }
+            }
+        }
+
+        private class AsyncComponent : ComponentBase
+        {
+            public AsyncComponent()
+            {
+            }
+
+            [Parameter]
+            public int Value { get; set; }
+
+            protected override async Task OnInitAsync()
+            {
+                Value = Value * 2;
+                await Task.Delay(Value * 100);
+            }
+
+            protected override void BuildRenderTree(RenderTreeBuilder builder)
+            {
+                base.BuildRenderTree(builder);
+                builder.OpenElement(0, "p");
+                builder.AddContent(1, Value.ToString());
+                builder.CloseElement();
             }
         }
 
@@ -403,14 +502,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
         {
             private RenderHandle _renderHandle;
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
                 _renderHandle = renderHandle;
             }
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
                 _renderHandle.Render(CreateRenderFragment(parameters));
+                return Task.CompletedTask;
             }
 
             private RenderFragment CreateRenderFragment(ParameterCollection parameters)
@@ -433,14 +533,15 @@ namespace Microsoft.AspNetCore.Components.Rendering
             [Inject]
             public RenderFragment Fragment { get; set; }
 
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
             {
                 _renderHandle = renderHandle;
             }
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
             {
                 _renderHandle.Render(Fragment);
+                return Task.CompletedTask;
             }
         }
     }

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

@@ -363,10 +363,10 @@ namespace Microsoft.AspNetCore.Components.Server
 
         class FakeComponent : IComponent
         {
-            public void Init(RenderHandle renderHandle)
+            public void Configure(RenderHandle renderHandle)
                 => throw new NotImplementedException();
 
-            public void SetParameters(ParameterCollection parameters)
+            public Task SetParametersAsync(ParameterCollection parameters)
                 => throw new NotImplementedException();
         }
 

+ 4 - 2
src/Components/Shared/test/AutoRenderComponent.cs

@@ -1,6 +1,7 @@
 // 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;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 
@@ -10,15 +11,16 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
     {
         private RenderHandle _renderHandle;
 
-        public void Init(RenderHandle renderHandle)
+        public void Configure(RenderHandle renderHandle)
         {
             _renderHandle = renderHandle;
         }
 
-        public virtual void SetParameters(ParameterCollection parameters)
+        public virtual Task SetParametersAsync(ParameterCollection parameters)
         {
             parameters.SetParameterProperties(this);
             TriggerRender();
+            return Task.CompletedTask;
         }
 
         public void TriggerRender()

+ 5 - 4
src/Components/Shared/test/IComponentExtensions.cs

@@ -1,9 +1,10 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.RenderTree;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 
 namespace Microsoft.AspNetCore.Components.Test.Helpers
 {
@@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
             this IComponent component,
             Dictionary<string, object> parameters)
         {
-            component.SetParameters(DictionaryToParameterCollection(parameters));
+            component.SetParametersAsync(DictionaryToParameterCollection(parameters));
         }
 
         private static ParameterCollection DictionaryToParameterCollection(
@@ -32,8 +33,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
 
         private abstract class AbstractComponent : IComponent
         {
-            public abstract void Init(RenderHandle renderHandle);
-            public abstract void SetParameters(ParameterCollection parameters);
+            public abstract void Configure(RenderHandle renderHandle);
+            public abstract Task SetParametersAsync(ParameterCollection parameters);
         }
     }
 }

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

@@ -31,6 +31,12 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
         public new void RenderRootComponent(int componentId)
             => base.RenderRootComponent(componentId);
 
+        public new Task RenderRootComponentAsync(int componentId)
+            => base.RenderRootComponentAsync(componentId);
+
+        public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
+            => base.RenderRootComponentAsync(componentId, parameters);
+
         public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
             => base.DispatchEvent(componentId, eventHandlerId, args);