Browse Source

Merge pull request #25219 from dotnet-maestro-bot/merge/release/5.0-to-master

[automated] Merge branch 'release/5.0' => 'master'
John Luo 5 years ago
parent
commit
ec2c7b4787
100 changed files with 2684 additions and 452 deletions
  1. 15 0
      AspNetCore.sln
  2. 1 0
      eng/Dependencies.props
  3. 4 0
      eng/Version.Details.xml
  4. 1 0
      eng/Versions.props
  5. 19 1
      src/Antiforgery/src/Internal/DefaultAntiforgeryTokenStore.cs
  6. 3 0
      src/Antiforgery/src/Resources.resx
  7. 52 0
      src/Antiforgery/test/DefaultAntiforgeryTokenStoreTest.cs
  8. 1 1
      src/Components/Components/src/ComponentFactory.cs
  9. 12 9
      src/Components/Components/src/Reflection/ComponentProperties.cs
  10. 46 3
      src/Components/Components/src/Reflection/IPropertySetter.cs
  11. 0 41
      src/Components/Components/src/Reflection/MemberAssignment.cs
  12. 6 0
      src/Components/Forms/src/EditContext.cs
  13. 63 0
      src/Components/Forms/src/EditContextProperties.cs
  14. 71 0
      src/Components/Forms/test/EditContextTest.cs
  15. 3 3
      src/Components/Ignitor/src/BlazorClient.cs
  16. 5 1
      src/Components/Ignitor/src/CapturedJSInteropCall.cs
  17. 2 2
      src/Components/Server/src/Circuits/RemoteJSRuntime.cs
  18. 2 2
      src/Components/Server/src/Circuits/ServerComponentDeserializer.cs
  19. 1 1
      src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
  20. 3 1
      src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
  21. 1 1
      src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs
  22. 0 0
      src/Components/Shared/src/ComponentParametersTypeCache.cs
  23. 4 4
      src/Components/Shared/src/RootComponentTypeCache.cs
  24. 0 0
      src/Components/Web.JS/dist/Release/blazor.server.js
  25. 0 0
      src/Components/Web.JS/dist/Release/blazor.webassembly.js
  26. 4 3
      src/Components/Web.JS/src/Boot.Server.ts
  27. 52 2
      src/Components/Web.JS/src/Boot.WebAssembly.ts
  28. 0 1
      src/Components/Web.JS/src/GlobalExports.ts
  29. 3 220
      src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts
  30. 51 0
      src/Components/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts
  31. 354 0
      src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts
  32. 22 10
      src/Components/Web/src/Forms/EditContextFieldClassExtensions.cs
  33. 35 0
      src/Components/Web/src/Forms/FieldCssClassProvider.cs
  34. 15 20
      src/Components/WebAssembly/DevServer/src/Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj
  35. 1 6
      src/Components/WebAssembly/JSInterop/src/InternalCalls.cs
  36. 27 0
      src/Components/WebAssembly/JSInterop/src/JSCallInfo.cs
  37. 32 7
      src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs
  38. 22 0
      src/Components/WebAssembly/WebAssembly/src/Hosting/RegisteredComponentsInterop.cs
  39. 20 1
      src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentMapping.cs
  40. 12 1
      src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentMappingCollection.cs
  41. 1 1
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs
  42. 36 1
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
  43. 7 8
      src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj
  44. 88 0
      src/Components/WebAssembly/WebAssembly/src/Prerendering/ClientComponentParameterDeserializer.cs
  45. 6 4
      src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs
  46. 2 0
      src/Components/WebAssembly/WebAssembly/test/TestWebAssemblyJSRuntimeInvoker.cs
  47. 6 0
      src/Components/WebAssembly/testassets/Wasm.Authentication.Server/Startup.cs
  48. 2 0
      src/Components/WebAssembly/testassets/Wasm.Authentication.Server/Wasm.Authentication.Server.csproj
  49. 11 0
      src/Components/benchmarkapps/Wasm.Performance/Directory.Build.props
  50. 31 1
      src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs
  51. 1 6
      src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs
  52. 6 1
      src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj
  53. 5 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj
  54. 2 1
      src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj
  55. 95 0
      src/Components/test/E2ETest/Tests/ClientRenderingMultpleComponentsTest.cs
  56. 17 0
      src/Components/test/E2ETest/Tests/FormsTest.cs
  57. 10 0
      src/Components/test/E2ETest/Tests/InteropTest.cs
  58. 47 0
      src/Components/test/testassets/BasicTestApp/FormsTest/CustomFieldCssClassProvider.cs
  59. 7 0
      src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
  60. 33 0
      src/Components/test/testassets/BasicTestApp/InteropComponent.razor
  61. 41 0
      src/Components/test/testassets/BasicTestApp/InteropTest/JavaScriptInterop.cs
  62. 46 1
      src/Components/test/testassets/BasicTestApp/wwwroot/js/jsinteroptests.js
  63. 3 0
      src/Components/test/testassets/BasicTestApp/wwwroot/js/testmodule.js
  64. 4 1
      src/Components/test/testassets/TestServer/Components.TestServer.csproj
  65. 12 0
      src/Components/test/testassets/TestServer/MultipleComponents.cs
  66. 29 0
      src/Components/test/testassets/TestServer/Pages/Client/MultipleComponents.cshtml
  67. 45 0
      src/Components/test/testassets/TestServer/Pages/Client/MultipleComponentsLayout.cshtml
  68. 9 4
      src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs
  69. 32 1
      src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs
  70. 17 0
      src/Hosting/Hosting/src/WebHostBuilderOptions.cs
  71. 1 1
      src/Hosting/Hosting/test/Fakes/GenericWebHostBuilderWrapper.cs
  72. 41 0
      src/Hosting/Hosting/test/GenericWebHostBuilderTests.cs
  73. 0 4
      src/Hosting/Hosting/test/WebHostBuilderTests.cs
  74. 175 39
      src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts
  75. 2 1
      src/JSInterop/Microsoft.JSInterop.JS/src/tsconfig.json
  76. 59 0
      src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSObjectReferenceJsonConverter.cs
  77. 21 0
      src/JSInterop/Microsoft.JSInterop/src/JSCallResultType.cs
  78. 35 0
      src/JSInterop/Microsoft.JSInterop/src/JSInProcessObjectReference.cs
  79. 38 8
      src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs
  80. 103 0
      src/JSInterop/Microsoft.JSInterop/src/JSObjectReference.cs
  81. 49 14
      src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs
  82. 2 2
      src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs
  83. 85 0
      src/JSInterop/Microsoft.JSInterop/test/Infrastructure/JSObjectReferenceJsonConverterTest.cs
  84. 2 2
      src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs
  85. 105 0
      src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs
  86. 1 1
      src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs
  87. 1 1
      src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs
  88. 9 1
      src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
  89. 8 0
      src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs
  90. 20 1
      src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs
  91. 22 0
      src/Mvc/Mvc.Core/test/ModelBinding/CompositeValueProviderTest.cs
  92. 57 1
      src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs
  93. 57 0
      src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderFactoryTest.cs
  94. 57 0
      src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
  95. 3 1
      src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs
  96. 75 0
      src/Mvc/Mvc.ViewFeatures/src/ClientComponentSerializer.cs
  97. 3 0
      src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
  98. 3 1
      src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj
  99. 2 0
      src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt
  100. 32 3
      src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs

+ 15 - 0
AspNetCore.sln

@@ -1507,6 +1507,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagno
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Tests", "src\HealthChecks\HealthChecks\test\Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj", "{7509AA1E-3093-4BEE-984F-E11579E98A11}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Tests", "src\JSInterop\Microsoft.JSInterop\test\Microsoft.JSInterop.Tests.csproj", "{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -7179,6 +7181,18 @@ Global
 		{7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x64.Build.0 = Release|Any CPU
 		{7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x86.ActiveCfg = Release|Any CPU
 		{7509AA1E-3093-4BEE-984F-E11579E98A11}.Release|x86.Build.0 = Release|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x64.Build.0 = Debug|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Debug|x86.Build.0 = Debug|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x64.ActiveCfg = Release|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x64.Build.0 = Release|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.ActiveCfg = Release|Any CPU
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -7934,6 +7948,7 @@ Global
 		{B06040BC-DA28-4923-8CAC-20EB517D471B} = {22D7D74B-565D-4047-97B4-F149B1A13350}
 		{55CACC1F-FE96-47C8-8073-91F4CAA55C75} = {2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}
 		{7509AA1E-3093-4BEE-984F-E11579E98A11} = {7CB09412-C9B0-47E8-A8C3-311AA4CFDE04}
+		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E} = {16898702-3E33-41C1-B8D8-4CE3F1D46BD9}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

+ 1 - 0
eng/Dependencies.props

@@ -64,6 +64,7 @@ and are generated based on the last package release.
     <LatestPackageReference Include="System.ComponentModel.Annotations" />
     <LatestPackageReference Include="System.Diagnostics.DiagnosticSource" />
     <LatestPackageReference Include="System.Diagnostics.EventLog" />
+    <LatestPackageReference Include="System.DirectoryServices.Protocols" />
     <LatestPackageReference Include="System.Drawing.Common" />
     <LatestPackageReference Include="System.IO.Pipelines" />
     <LatestPackageReference Include="System.Net.Http" />

+ 4 - 0
eng/Version.Details.xml

@@ -205,6 +205,10 @@
       <Uri>https://github.com/dotnet/runtime</Uri>
       <Sha>907f7da59b40c80941b02ac2a46650adf3f606bc</Sha>
     </Dependency>
+    <Dependency Name="System.DirectoryServices.Protocols" Version="5.0.0-rc.1.20417.14">
+      <Uri>https://github.com/dotnet/runtime</Uri>
+      <Sha>907f7da59b40c80941b02ac2a46650adf3f606bc</Sha>
+    </Dependency>
     <Dependency Name="System.Drawing.Common" Version="5.0.0-rc.1.20417.14">
       <Uri>https://github.com/dotnet/runtime</Uri>
       <Sha>907f7da59b40c80941b02ac2a46650adf3f606bc</Sha>

+ 1 - 0
eng/Versions.props

@@ -108,6 +108,7 @@
     <SystemComponentModelAnnotationsPackageVersion>5.0.0-rc.1.20417.14</SystemComponentModelAnnotationsPackageVersion>
     <SystemDiagnosticsDiagnosticSourcePackageVersion>5.0.0-rc.1.20417.14</SystemDiagnosticsDiagnosticSourcePackageVersion>
     <SystemDiagnosticsEventLogPackageVersion>5.0.0-rc.1.20417.14</SystemDiagnosticsEventLogPackageVersion>
+    <SystemDirectoryServicesProtocolsPackageVersion>5.0.0-rc.1.20417.14</SystemDirectoryServicesProtocolsPackageVersion>
     <SystemDrawingCommonPackageVersion>5.0.0-rc.1.20417.14</SystemDrawingCommonPackageVersion>
     <SystemIOPipelinesPackageVersion>5.0.0-rc.1.20417.14</SystemIOPipelinesPackageVersion>
     <SystemNetHttpJsonPackageVersion>5.0.0-rc.1.20417.14</SystemNetHttpJsonPackageVersion>

+ 19 - 1
src/Antiforgery/src/Internal/DefaultAntiforgeryTokenStore.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Diagnostics;
+using System.IO;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Options;
@@ -57,7 +58,24 @@ namespace Microsoft.AspNetCore.Antiforgery
             {
                 // Check the content-type before accessing the form collection to make sure
                 // we report errors gracefully.
-                var form = await httpContext.Request.ReadFormAsync();
+                IFormCollection form;
+                try
+                {
+                    form = await httpContext.Request.ReadFormAsync();
+                }
+                catch (InvalidDataException ex)
+                {
+                    // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+                    // Wrap it in an AntiforgeryValidationException and allow the caller to handle it as just another antiforgery failure.
+                    throw new AntiforgeryValidationException(Resources.AntiforgeryToken_UnableToReadRequest, ex);
+                }
+                catch (IOException ex)
+                {
+                    // Reading the request body (which happens as part of ReadFromAsync) may throw an exception if a client disconnects.
+                    // Wrap it in an AntiforgeryValidationException and allow the caller to handle it as just another antiforgery failure.
+                    throw new AntiforgeryValidationException(Resources.AntiforgeryToken_UnableToReadRequest, ex);
+                }
+
                 requestToken = form[_options.FormFieldName];
             }
 

+ 3 - 0
src/Antiforgery/src/Resources.resx

@@ -136,6 +136,9 @@
   <data name="AntiforgeryToken_TokensSwapped" xml:space="preserve">
     <value>Validation of the provided antiforgery token failed. The cookie token and the request token were swapped.</value>
   </data>
+  <data name="AntiforgeryToken_UnableToReadRequest" xml:space="preserve">
+    <value>Unable to read the antiforgery request token from the posted form.</value>
+  </data>
   <data name="AntiforgeryToken_UsernameMismatch" xml:space="preserve">
     <value>The provided antiforgery token was meant for user "{0}", but the current user is "{1}".</value>
   </data>

+ 52 - 0
src/Antiforgery/test/DefaultAntiforgeryTokenStoreTest.cs

@@ -3,6 +3,8 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Primitives;
@@ -235,6 +237,56 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
             Assert.Null(tokenSet.RequestToken);
         }
 
+        [Fact]
+        public async Task GetRequestTokens_ReadFormAsyncThrowsIOException_ThrowsAntiforgeryValidationException()
+        {
+            // Arrange
+            var ioException = new IOException();
+            var httpContext = new Mock<HttpContext>();
+
+            httpContext.Setup(r => r.Request.Cookies).Returns(Mock.Of<IRequestCookieCollection>());
+            httpContext.SetupGet(r => r.Request.HasFormContentType).Returns(true);
+            httpContext.Setup(r => r.Request.ReadFormAsync(It.IsAny<CancellationToken>())).Throws(ioException);
+
+            var options = new AntiforgeryOptions
+            {
+                Cookie = { Name = "cookie-name" },
+                FormFieldName = "form-field-name",
+                HeaderName = null,
+            };
+
+            var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options));
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<AntiforgeryValidationException>(() => tokenStore.GetRequestTokensAsync(httpContext.Object));
+            Assert.Same(ioException, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetRequestTokens_ReadFormAsyncThrowsInvalidDataException_ThrowsAntiforgeryValidationException()
+        {
+            // Arrange
+            var exception = new InvalidDataException();
+            var httpContext = new Mock<HttpContext>();
+
+            httpContext.Setup(r => r.Request.Cookies).Returns(Mock.Of<IRequestCookieCollection>());
+            httpContext.SetupGet(r => r.Request.HasFormContentType).Returns(true);
+            httpContext.Setup(r => r.Request.ReadFormAsync(It.IsAny<CancellationToken>())).Throws(exception);
+
+            var options = new AntiforgeryOptions
+            {
+                Cookie = { Name = "cookie-name" },
+                FormFieldName = "form-field-name",
+                HeaderName = null,
+            };
+
+            var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options));
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<AntiforgeryValidationException>(() => tokenStore.GetRequestTokensAsync(httpContext.Object));
+            Assert.Same(exception, ex.InnerException);
+        }
+
         [Theory]
         [InlineData(false, CookieSecurePolicy.SameAsRequest, null)]
         [InlineData(true, CookieSecurePolicy.SameAsRequest, true)]

+ 1 - 1
src/Components/Components/src/ComponentFactory.cs

@@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Components
             (
                 propertyName: property.Name,
                 propertyType: property.PropertyType,
-                setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false)
+                setter: new PropertySetter(type, property)
             )).ToArray();
 
             return Initialize;

+ 12 - 9
src/Components/Components/src/Reflection/ComponentProperties.cs

@@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Components.Reflection
                 }
             }
 
-            static void SetProperty(object target, IPropertySetter writer, string parameterName, object value)
+            static void SetProperty(object target, PropertySetter writer, string parameterName, object value)
             {
                 try
                 {
@@ -246,13 +246,13 @@ namespace Microsoft.AspNetCore.Components.Reflection
         private class WritersForType
         {
             private const int MaxCachedWriterLookups = 100;
-            private readonly Dictionary<string, IPropertySetter> _underlyingWriters;
-            private readonly ConcurrentDictionary<string, IPropertySetter?> _referenceEqualityWritersCache;
+            private readonly Dictionary<string, PropertySetter> _underlyingWriters;
+            private readonly ConcurrentDictionary<string, PropertySetter?> _referenceEqualityWritersCache;
 
             public WritersForType(Type targetType)
             {
-                _underlyingWriters = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
-                _referenceEqualityWritersCache = new ConcurrentDictionary<string, IPropertySetter?>(ReferenceEqualityComparer.Instance);
+                _underlyingWriters = new Dictionary<string, PropertySetter>(StringComparer.OrdinalIgnoreCase);
+                _referenceEqualityWritersCache = new ConcurrentDictionary<string, PropertySetter?>(ReferenceEqualityComparer.Instance);
 
                 foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
                 {
@@ -271,7 +271,10 @@ namespace Microsoft.AspNetCore.Components.Reflection
                             $"The type '{targetType.FullName}' declares a parameter matching the name '{propertyName}' that is not public. Parameters must be public.");
                     }
 
-                    var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null);
+                    var propertySetter = new PropertySetter(targetType, propertyInfo)
+                    {
+                        Cascading = cascadingParameterAttribute != null,
+                    };
 
                     if (_underlyingWriters.ContainsKey(propertyName))
                     {
@@ -298,17 +301,17 @@ namespace Microsoft.AspNetCore.Components.Reflection
                             ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo);
                         }
 
-                        CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false);
+                        CaptureUnmatchedValuesWriter = new PropertySetter(targetType, propertyInfo);
                         CaptureUnmatchedValuesPropertyName = propertyInfo.Name;
                     }
                 }
             }
 
-            public IPropertySetter? CaptureUnmatchedValuesWriter { get; }
+            public PropertySetter? CaptureUnmatchedValuesWriter { get; }
 
             public string? CaptureUnmatchedValuesPropertyName { get; }
 
-            public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out IPropertySetter writer)
+            public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out PropertySetter writer)
             {
                 // In intensive parameter-passing scenarios, one of the most expensive things we do is the
                 // lookup from parameterName to writer. Pre-5.0 that was because of the string hashing.

+ 46 - 3
src/Components/Components/src/Reflection/IPropertySetter.cs

@@ -1,12 +1,55 @@
 // 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.Reflection;
+
 namespace Microsoft.AspNetCore.Components.Reflection
 {
-    internal interface IPropertySetter
+    internal sealed class PropertySetter
     {
-        bool Cascading { get; }
+        private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
+            typeof(PropertySetter).GetMethod(nameof(CallPropertySetter), BindingFlags.NonPublic | BindingFlags.Static)!;
+
+        private readonly Action<object, object> _setterDelegate;
+
+        public PropertySetter(Type targetType, PropertyInfo property)
+        {
+            if (property.SetMethod == null)
+            {
+                throw new InvalidOperationException($"Cannot provide a value for property " +
+                    $"'{property.Name}' on type '{targetType.FullName}' because the property " +
+                    $"has no setter.");
+            }
+
+            var setMethod = property.SetMethod;
+
+            var propertySetterAsAction =
+                setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(targetType, property.PropertyType));
+            var callPropertySetterClosedGenericMethod =
+                CallPropertySetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
+            _setterDelegate = (Action<object, object>)
+                callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action<object, object>), propertySetterAsAction);
+        }
+
+        public bool Cascading { get; init;  }
+
+        public void SetValue(object target, object value) => _setterDelegate(target, value);
 
-        void SetValue(object target, object value);
+        private static void CallPropertySetter<TTarget, TValue>(
+            Action<TTarget, TValue> setter,
+            object target,
+            object value)
+            where TTarget : notnull
+        {
+            if (value == null)
+            {
+                setter((TTarget)target, default!);
+            }
+            else
+            {
+                setter((TTarget)target, (TValue)value);
+            }
+        }
     }
 }

+ 0 - 41
src/Components/Components/src/Reflection/MemberAssignment.cs

@@ -44,46 +44,5 @@ namespace Microsoft.AspNetCore.Components.Reflection
 
             return dictionary.Values.SelectMany(p => p);
         }
-
-        public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property, bool cascading)
-        {
-            if (property.SetMethod == null)
-            {
-                throw new InvalidOperationException($"Cannot provide a value for property " +
-                    $"'{property.Name}' on type '{targetType.FullName}' because the property " +
-                    $"has no setter.");
-            }
-
-            return (IPropertySetter)Activator.CreateInstance(
-                typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType),
-                property.SetMethod,
-                cascading)!;
-        }
-
-        class PropertySetter<TTarget, TValue> : IPropertySetter where TTarget : notnull
-        {
-            private readonly Action<TTarget, TValue> _setterDelegate;
-
-            public PropertySetter(MethodInfo setMethod, bool cascading)
-            {
-                _setterDelegate = (Action<TTarget, TValue>)Delegate.CreateDelegate(
-                    typeof(Action<TTarget, TValue>), setMethod);
-                Cascading = cascading;
-            }
-
-            public bool Cascading { get; }
-
-            public void SetValue(object target, object value)
-            {
-                if (value == null)
-                {
-                    _setterDelegate((TTarget)target, default!);
-                }
-                else
-                {
-                    _setterDelegate((TTarget)target, (TValue)value);
-                }
-            }
-        }
     }
 }

+ 6 - 0
src/Components/Forms/src/EditContext.cs

@@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Components.Forms
             // really don't, you can pass an empty object then ignore it. Ensuring it's nonnull
             // simplifies things for all consumers of EditContext.
             Model = model ?? throw new ArgumentNullException(nameof(model));
+            Properties = new EditContextProperties();
         }
 
         /// <summary>
@@ -62,6 +63,11 @@ namespace Microsoft.AspNetCore.Components.Forms
         /// </summary>
         public object Model { get; }
 
+        /// <summary>
+        /// Gets a collection of arbitrary properties associated with this instance.
+        /// </summary>
+        public EditContextProperties Properties { get; }
+
         /// <summary>
         /// Signals that the value for the specified field has changed.
         /// </summary>

+ 63 - 0
src/Components/Forms/src/EditContextProperties.cs

@@ -0,0 +1,63 @@
+// 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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    /// <summary>
+    /// Holds arbitrary key/value pairs associated with an <see cref="EditContext"/>.
+    /// This can be used to track additional metadata for application-specific purposes.
+    /// </summary>
+    public sealed class EditContextProperties
+    {
+        // We don't want to expose any way of enumerating the underlying dictionary, because that would
+        // prevent its usage to store private information. So we only expose an indexer and TryGetValue.
+        private Dictionary<object, object>? _contents;
+
+        /// <summary>
+        /// Gets or sets a value in the collection.
+        /// </summary>
+        /// <param name="key">The key under which the value is stored.</param>
+        /// <returns>The stored value.</returns>
+        public object this[object key]
+        {
+            get => _contents is null ? throw new KeyNotFoundException() : _contents[key];
+            set
+            {
+                _contents ??= new Dictionary<object, object>();
+                _contents[key] = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets the value associated with the specified key, if any.
+        /// </summary>
+        /// <param name="key">The key under which the value is stored.</param>
+        /// <param name="value">The value, if present.</param>
+        /// <returns>True if the value was present, otherwise false.</returns>
+        public bool TryGetValue(object key, [NotNullWhen(true)] out object? value)
+        {
+            if (_contents is null)
+            {
+                value = default;
+                return false;
+            }
+            else
+            {
+                return _contents.TryGetValue(key, out value);
+            }
+        }
+
+        /// <summary>
+        /// Removes the specified entry from the collection.
+        /// </summary>
+        /// <param name="key">The key of the entry to be removed.</param>
+        /// <returns>True if the value was present, otherwise false.</returns>
+        public bool Remove(object key)
+        {
+            return _contents?.Remove(key) ?? false;
+        }
+    }
+}

+ 71 - 0
src/Components/Forms/test/EditContextTest.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.Generic;
 using System.Linq;
 using Xunit;
 
@@ -252,6 +253,76 @@ namespace Microsoft.AspNetCore.Components.Forms
             Assert.True(editContext.IsModified(editContext.Field(nameof(EquatableModel.Property))));
         }
 
+        [Fact]
+        public void Properties_CanRetrieveViaIndexer()
+        {
+            // Arrange
+            var editContext = new EditContext(new object());
+            var key1 = new object();
+            var key2 = new object();
+            var key3 = new object();
+            var value1 = new object();
+            var value2 = new object();
+
+            // Initially, the values are not present
+            Assert.Throws<KeyNotFoundException>(() => editContext.Properties[key1]);
+
+            // Can store and retrieve values
+            editContext.Properties[key1] = value1;
+            editContext.Properties[key2] = value2;
+            Assert.Same(value1, editContext.Properties[key1]);
+            Assert.Same(value2, editContext.Properties[key2]);
+
+            // Unrelated keys are still not found
+            Assert.Throws<KeyNotFoundException>(() => editContext.Properties[key3]);
+        }
+
+        [Fact]
+        public void Properties_CanRetrieveViaTryGetValue()
+        {
+            // Arrange
+            var editContext = new EditContext(new object());
+            var key1 = new object();
+            var key2 = new object();
+            var key3 = new object();
+            var value1 = new object();
+            var value2 = new object();
+
+            // Initially, the values are not present
+            Assert.False(editContext.Properties.TryGetValue(key1, out _));
+
+            // Can store and retrieve values
+            editContext.Properties[key1] = value1;
+            editContext.Properties[key2] = value2;
+            Assert.True(editContext.Properties.TryGetValue(key1, out var retrievedValue1));
+            Assert.True(editContext.Properties.TryGetValue(key2, out var retrievedValue2));
+            Assert.Same(value1, retrievedValue1);
+            Assert.Same(value2, retrievedValue2);
+
+            // Unrelated keys are still not found
+            Assert.False(editContext.Properties.TryGetValue(key3, out _));
+        }
+
+        [Fact]
+        public void Properties_CanRemove()
+        {
+            // Arrange
+            var editContext = new EditContext(new object());
+            var key = new object();
+            var value = new object();
+            editContext.Properties[key] = value;
+
+            // Act
+            var resultForExistingKey = editContext.Properties.Remove(key);
+            var resultForNonExistingKey = editContext.Properties.Remove(new object());
+
+            // Assert
+            Assert.True(resultForExistingKey);
+            Assert.False(resultForNonExistingKey);
+            Assert.False(editContext.Properties.TryGetValue(key, out _));
+            Assert.Throws<KeyNotFoundException>(() => editContext.Properties[key]);
+        }
+
         class EquatableModel : IEquatable<EquatableModel>
         {
             public string Property { get; set; } = "";

+ 3 - 3
src/Components/Ignitor/src/BlazorClient.cs

@@ -353,7 +353,7 @@ namespace Ignitor
             _hubConnection = builder.Build();
 
             HubConnection.On<int, string>("JS.AttachComponent", OnAttachComponent);
-            HubConnection.On<int, string, string>("JS.BeginInvokeJS", OnBeginInvokeJS);
+            HubConnection.On<int, string, string, int, long>("JS.BeginInvokeJS", OnBeginInvokeJS);
             HubConnection.On<string>("JS.EndInvokeDotNet", OnEndInvokeDotNet);
             HubConnection.On<int, byte[]>("JS.RenderBatch", OnRenderBatch);
             HubConnection.On<string>("JS.Error", OnError);
@@ -401,9 +401,9 @@ namespace Ignitor
             NextAttachComponentReceived?.Completion?.TrySetResult(call);
         }
 
-        private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
+        private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson, int resultType, long targetInstanceId)
         {
-            var call = new CapturedJSInteropCall(asyncHandle, identifier, argsJson);
+            var call = new CapturedJSInteropCall(asyncHandle, identifier, argsJson, resultType, targetInstanceId);
             Operations?.JSInteropCalls.Enqueue(call);
             JSInterop?.Invoke(call);
 

+ 5 - 1
src/Components/Ignitor/src/CapturedJSInteropCall.cs

@@ -5,15 +5,19 @@ namespace Ignitor
 {
     public class CapturedJSInteropCall
     {
-        public CapturedJSInteropCall(int asyncHandle, string identifier, string argsJson)
+        public CapturedJSInteropCall(int asyncHandle, string identifier, string argsJson, int resultType, long targetInstanceId)
         {
             AsyncHandle = asyncHandle;
             Identifier = identifier;
             ArgsJson = argsJson;
+            ResultType = resultType;
+            TargetInstanceId = targetInstanceId;
         }
 
         public int AsyncHandle { get; }
         public string Identifier { get; }
         public string ArgsJson { get; }
+        public int ResultType { get; }
+        public long TargetInstanceId { get; }
     }
 }

+ 2 - 2
src/Components/Server/src/Circuits/RemoteJSRuntime.cs

@@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions));
         }
 
-        protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
+        protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
         {
             if (_clientProxy is null)
             {
@@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 
             Log.BeginInvokeJS(_logger, asyncHandle, identifier);
 
-            _clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson);
+            _clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, (int)resultType, targetInstanceId);
         }
 
         public static class Log

+ 2 - 2
src/Components/Server/src/Circuits/ServerComponentDeserializer.cs

@@ -59,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.Server
     {
         private readonly IDataProtector _dataProtector;
         private readonly ILogger<ServerComponentDeserializer> _logger;
-        private readonly ServerComponentTypeCache _rootComponentTypeCache;
+        private readonly RootComponentTypeCache _rootComponentTypeCache;
         private readonly ComponentParameterDeserializer _parametersDeserializer;
 
         public ServerComponentDeserializer(
             IDataProtectionProvider dataProtectionProvider,
             ILogger<ServerComponentDeserializer> logger,
-            ServerComponentTypeCache rootComponentTypeCache,
+            RootComponentTypeCache rootComponentTypeCache,
             ComponentParameterDeserializer parametersDeserializer)
         {
             // When we protect the data we use a time-limited data protector with the

+ 1 - 1
src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

@@ -57,7 +57,7 @@ namespace Microsoft.Extensions.DependencyInjection
             services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
             services.TryAddSingleton<CircuitFactory>();
             services.TryAddSingleton<ServerComponentDeserializer>();
-            services.TryAddSingleton<ServerComponentTypeCache>();
+            services.TryAddSingleton<RootComponentTypeCache>();
             services.TryAddSingleton<ComponentParameterDeserializer>();
             services.TryAddSingleton<ComponentParametersTypeCache>();
             services.TryAddSingleton<CircuitIdFactory>();

+ 3 - 1
src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
     <Description>Runtime server features for ASP.NET Core Components.</Description>
@@ -54,6 +54,8 @@
     <Compile Include="$(ComponentsSharedSourceRoot)src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
     <Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Circuits" />
     <Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
+    <Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
+    <Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
 
     <Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
     <Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />

+ 1 - 1
src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs

@@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             return new ServerComponentDeserializer(
                 _ephemeralDataProtectionProvider,
                 NullLogger<ServerComponentDeserializer>.Instance,
-                new ServerComponentTypeCache(),
+                new RootComponentTypeCache(),
                 new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
         }
 

+ 0 - 0
src/Components/Server/src/Circuits/ComponentParametersTypeCache.cs → src/Components/Shared/src/ComponentParametersTypeCache.cs


+ 4 - 4
src/Components/Server/src/Circuits/ServerComponentTypeCache.cs → src/Components/Shared/src/RootComponentTypeCache.cs

@@ -9,7 +9,7 @@ using System.Reflection;
 namespace Microsoft.AspNetCore.Components
 {
     // A cache for root component types
-    internal class ServerComponentTypeCache
+    internal class RootComponentTypeCache
     {
         private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
 
@@ -39,14 +39,14 @@ namespace Microsoft.AspNetCore.Components
             return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
         }
 
-        private struct Key : IEquatable<Key>
+        private readonly struct Key : IEquatable<Key>
         {
             public Key(string assembly, string type) =>
                 (Assembly, Type) = (assembly, type);
 
-            public string Assembly { get; set; }
+            public string Assembly { get; }
 
-            public string Type { get; set; }
+            public string Type { get; }
 
             public override bool Equals(object obj) => Equals((Key)obj);
 

File diff suppressed because it is too large
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.server.js


File diff suppressed because it is too large
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.webassembly.js


+ 4 - 3
src/Components/Web.JS/src/Boot.Server.ts

@@ -7,11 +7,12 @@ import { shouldAutoStart } from './BootCommon';
 import { RenderQueue } from './Platform/Circuits/RenderQueue';
 import { ConsoleLogger } from './Platform/Logging/Loggers';
 import { LogLevel, Logger } from './Platform/Logging/Logger';
-import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
+import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
 import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
 import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
 import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
 import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
+import { discoverComponents, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
 
 let renderingFailed = false;
 let started = false;
@@ -29,7 +30,7 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
   options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
   logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
 
-  const components = discoverComponents(document);
+  const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
   const circuit = new CircuitDescriptor(components);
 
   const initialConnection = await initializeConnection(options, logger, circuit);
@@ -97,7 +98,7 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
 
   connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
   connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
-  connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(JSON.parse(args) as [string, boolean, unknown])));
+  connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(DotNet.parseJsonWithRevivers(args) as [string, boolean, unknown])));
 
   const renderQueue = RenderQueue.getOrCreate(logger);
   connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {

+ 52 - 2
src/Components/Web.JS/src/Boot.WebAssembly.ts

@@ -2,7 +2,7 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
 import './GlobalExports';
 import * as Environment from './Environment';
 import { monoPlatform } from './Platform/Mono/MonoPlatform';
-import { renderBatch, getRendererer } from './Rendering/Renderer';
+import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
 import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
 import { shouldAutoStart } from './BootCommon';
 import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
@@ -11,6 +11,8 @@ import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
 import { BootConfigResult } from './Platform/BootConfig';
 import { Pointer } from './Platform/Platform';
 import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
+import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
+import { discoverComponents, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
 
 let started = false;
 
@@ -32,6 +34,9 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
     }
   });
 
+  // Configure JS interop
+  window['Blazor']._internal.invokeJSFromDotNet = invokeJSFromDotNet;
+
   // Configure environment for execution under Mono WebAssembly with shared-memory rendering
   const platform = Environment.setPlatform(monoPlatform);
   window['Blazor'].platform = platform;
@@ -68,8 +73,31 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
   const environment = options?.environment;
 
   // Fetch the resources and prepare the Mono runtime
-  const bootConfigResult = await BootConfigResult.initAsync(environment);
+  const bootConfigPromise = BootConfigResult.initAsync(environment);
+
+  // Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on
+  // the document.
+  const discoveredComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
+  const componentAttacher = new WebAssemblyComponentAttacher(discoveredComponents);
+  window['Blazor']._internal.registeredComponents = {
+    getRegisteredComponentsCount: () => componentAttacher.getCount(),
+    getId: (index) => componentAttacher.getId(index),
+    getAssembly: (id) => BINDING.js_string_to_mono_string(componentAttacher.getAssembly(id)),
+    getTypeName: (id) => BINDING.js_string_to_mono_string(componentAttacher.getTypeName(id)),
+    getParameterDefinitions: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterDefinitions(id) || ''),
+    getParameterValues: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterValues(id) || ''),
+  };
+
+  window['Blazor']._internal.attachRootComponentToElement = (selector, componentId, rendererId) => {
+    const element = componentAttacher.resolveRegisteredElement(selector);
+    if (!element) {
+      attachRootComponentToElement(selector, componentId, rendererId);
+    } else {
+      attachRootComponentToLogicalElement(rendererId, element, componentId);
+    }
+  };
 
+  const bootConfigResult = await bootConfigPromise;
   const [resourceLoader] = await Promise.all([
     WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}),
     WebAssemblyConfigLoader.initAsync(bootConfigResult)]);
@@ -84,6 +112,28 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
   platform.callEntryPoint(resourceLoader.bootConfig.entryAssembly);
 }
 
+function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any): any {
+  const functionIdentifier = monoPlatform.readStringField(callInfo, 0)!;
+  const resultType = monoPlatform.readInt32Field(callInfo, 4);
+  const marshalledCallArgsJson = monoPlatform.readStringField(callInfo, 8);
+  const targetInstanceId = monoPlatform.readUint64Field(callInfo, 20);
+
+  if (marshalledCallArgsJson !== null) {
+    const marshalledCallAsyncHandle = monoPlatform.readUint64Field(callInfo, 12);
+
+    if (marshalledCallAsyncHandle !== 0) {
+      DotNet.jsCallDispatcher.beginInvokeJSFromDotNet(marshalledCallAsyncHandle, functionIdentifier, marshalledCallArgsJson, resultType, targetInstanceId);
+      return 0;
+    } else {
+      const resultJson = DotNet.jsCallDispatcher.invokeJSFromDotNet(functionIdentifier, marshalledCallArgsJson, resultType, targetInstanceId)!;
+      return resultJson === null ? 0 : BINDING.js_string_to_mono_string(resultJson);
+    }
+  } else {
+    const func = DotNet.jsCallDispatcher.findJSFunction(functionIdentifier, targetInstanceId);
+    return func.call(null, arg0, arg1, arg2);
+  }
+}
+
 window['Blazor'].start = boot;
 if (shouldAutoStart()) {
   boot().catch(error => {

+ 0 - 1
src/Components/Web.JS/src/GlobalExports.ts

@@ -8,7 +8,6 @@ window['Blazor'] = {
   navigateTo,
 
   _internal: {
-    attachRootComponentToElement,
     navigationManager: navigationManagerInternalFunctions,
     domWrapper: domFunctions,
     Virtualize,

+ 3 - 220
src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts

@@ -1,12 +1,13 @@
 import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
 import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
+import { ServerComponentDescriptor } from '../../Services/ComponentDescriptorDiscovery';
 
 export class CircuitDescriptor {
   public circuitId?: string;
 
-  public components: ComponentDescriptor[];
+  public components: ServerComponentDescriptor[];
 
-  public constructor(components: ComponentDescriptor[]) {
+  public constructor(components: ServerComponentDescriptor[]) {
     this.circuitId = undefined;
     this.components = components;
   }
@@ -54,221 +55,3 @@ export class CircuitDescriptor {
   }
 }
 
-interface ComponentMarker {
-  type: string;
-  sequence: number;
-  descriptor: string;
-}
-
-export class ComponentDescriptor {
-  public type: string;
-
-  public start: Node;
-
-  public end?: Node;
-
-  public sequence: number;
-
-  public descriptor: string;
-
-  public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
-    this.type = type;
-    this.start = start;
-    this.end = end;
-    this.sequence = sequence;
-    this.descriptor = descriptor;
-  }
-
-  public toRecord(): ComponentMarker {
-    const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
-    return result;
-  }
-}
-
-export function discoverComponents(document: Document): ComponentDescriptor[] {
-  const componentComments = resolveComponentComments(document);
-  const discoveredComponents: ComponentDescriptor[] = [];
-  for (let i = 0; i < componentComments.length; i++) {
-    const componentComment = componentComments[i];
-    const entry = new ComponentDescriptor(
-      componentComment.type,
-      componentComment.start,
-      componentComment.end,
-      componentComment.sequence,
-      componentComment.descriptor,
-    );
-
-    discoveredComponents.push(entry);
-  }
-
-  return discoveredComponents.sort((a, b) => a.sequence - b.sequence);
-}
-
-
-interface ComponentComment {
-  type: 'server';
-  sequence: number;
-  descriptor: string;
-  start: Node;
-  end?: Node;
-  prerenderId?: string;
-}
-
-function resolveComponentComments(node: Node): ComponentComment[] {
-  if (!node.hasChildNodes()) {
-    return [];
-  }
-
-  const result: ComponentComment[] = [];
-  const childNodeIterator = new ComponentCommentIterator(node.childNodes);
-  while (childNodeIterator.next() && childNodeIterator.currentElement) {
-    const componentComment = getComponentComment(childNodeIterator);
-    if (componentComment) {
-      result.push(componentComment);
-    } else {
-      const childResults = resolveComponentComments(childNodeIterator.currentElement);
-      for (let j = 0; j < childResults.length; j++) {
-        const childResult = childResults[j];
-        result.push(childResult);
-      }
-    }
-  }
-
-  return result;
-}
-
-const blazorCommentRegularExpression = /\W*Blazor:[^{]*(.*)$/;
-
-function getComponentComment(commentNodeIterator: ComponentCommentIterator): ComponentComment | undefined {
-  const candidateStart = commentNodeIterator.currentElement;
-
-  if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
-    return;
-  }
-  if (candidateStart.textContent) {
-    const componentStartComment = new RegExp(blazorCommentRegularExpression);
-    const definition = componentStartComment.exec(candidateStart.textContent);
-    const json = definition && definition[1];
-
-    if (json) {
-      try {
-        return createComponentComment(json, candidateStart, commentNodeIterator);
-      } catch (error) {
-        throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
-      }
-    } else {
-      return;
-    }
-  }
-}
-
-function createComponentComment(json: string, start: Node, iterator: ComponentCommentIterator): ComponentComment {
-  const payload = JSON.parse(json) as ComponentComment;
-  const { type, sequence, descriptor, prerenderId } = payload;
-  if (type !== 'server') {
-    throw new Error(`Invalid component type '${type}'.`);
-  }
-
-  if (!descriptor) {
-    throw new Error('descriptor must be defined when using a descriptor.');
-  }
-
-  if (sequence === undefined) {
-    throw new Error('sequence must be defined when using a descriptor.');
-  }
-
-  if (!Number.isInteger(sequence)) {
-    throw new Error(`Error parsing the sequence '${sequence}' for component '${json}'`);
-  }
-
-  if (!prerenderId) {
-    return {
-      type,
-      sequence: sequence,
-      descriptor,
-      start,
-    };
-  } else {
-    const end = getComponentEndComment(prerenderId, iterator);
-    if (!end) {
-      throw new Error(`Could not find an end component comment for '${start}'`);
-    }
-
-    return {
-      type,
-      sequence,
-      descriptor,
-      start,
-      prerenderId,
-      end,
-    };
-  }
-}
-
-function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
-  while (iterator.next() && iterator.currentElement) {
-    const node = iterator.currentElement;
-    if (node.nodeType !== Node.COMMENT_NODE) {
-      continue;
-    }
-    if (!node.textContent) {
-      continue;
-    }
-
-    const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
-    const json = definition && definition[1];
-    if (!json) {
-      continue;
-    }
-
-    validateEndComponentPayload(json, prerenderedId);
-
-    return node;
-  }
-
-  return undefined;
-}
-
-function validateEndComponentPayload(json: string, prerenderedId: string): void {
-  const payload = JSON.parse(json) as ComponentComment;
-  if (Object.keys(payload).length !== 1) {
-    throw new Error(`Invalid end of component comment: '${json}'`);
-  }
-  const prerenderedEndId = payload.prerenderId;
-  if (!prerenderedEndId) {
-    throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
-  }
-  if (prerenderedEndId !== prerenderedId) {
-    throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
-  }
-}
-
-class ComponentCommentIterator {
-
-  private childNodes: NodeListOf<ChildNode>;
-
-  private currentIndex: number;
-
-  private length: number;
-
-  public currentElement: ChildNode | undefined;
-
-  public constructor(childNodes: NodeListOf<ChildNode>) {
-    this.childNodes = childNodes;
-    this.currentIndex = -1;
-    this.length = childNodes.length;
-  }
-
-  public next(): boolean {
-    this.currentIndex++;
-    if (this.currentIndex < this.length) {
-      this.currentElement = this.childNodes[this.currentIndex];
-      return true;
-    } else {
-      this.currentElement = undefined;
-      return false;
-    }
-  }
-}
-
-

+ 51 - 0
src/Components/Web.JS/src/Platform/WebAssemblyComponentAttacher.ts

@@ -0,0 +1,51 @@
+import { LogicalElement, toLogicalRootCommentElement } from '../Rendering/LogicalElements';
+import { WebAssemblyComponentDescriptor } from '../Services/ComponentDescriptorDiscovery';
+
+export class WebAssemblyComponentAttacher {
+  public preregisteredComponents: WebAssemblyComponentDescriptor[];
+
+  private componentsById: { [index: number]: WebAssemblyComponentDescriptor };
+
+  public constructor(components: WebAssemblyComponentDescriptor[]) {
+    this.preregisteredComponents = components;
+    const componentsById = {};
+    for (let index = 0; index < components.length; index++) {
+      const component = components[index];
+      componentsById[component.id] = component;
+    }
+    this.componentsById = componentsById;
+  }
+
+  public resolveRegisteredElement(id: string): LogicalElement | undefined {
+    const parsedId = Number.parseInt(id);
+    if (!Number.isNaN(parsedId)) {
+      return toLogicalRootCommentElement(this.componentsById[parsedId].start as Comment, this.componentsById[parsedId].end as Comment);
+    } else {
+      return undefined;
+    }
+  }
+
+  public getParameterValues(id: number): string | undefined {
+    return this.componentsById[id].parameterValues;
+  }
+
+  public getParameterDefinitions(id: number): string | undefined {
+    return this.componentsById[id].parameterDefinitions;
+  }
+
+  public getTypeName(id: number): string {
+    return this.componentsById[id].typeName;
+  }
+
+  public getAssembly(id: number): string {
+    return this.componentsById[id].assembly;
+  }
+
+  public getId(index: number): number {
+    return this.preregisteredComponents[index].id;
+  }
+
+  public getCount(): number {
+    return this.preregisteredComponents.length;
+  }
+}

+ 354 - 0
src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts

@@ -0,0 +1,354 @@
+export function discoverComponents(document: Document, type: 'webassembly' | 'server'): ServerComponentDescriptor[] | WebAssemblyComponentDescriptor[] {
+  switch (type){
+    case 'webassembly':
+      return discoverWebAssemblyComponents(document);
+    case 'server':
+      return discoverServerComponents(document);
+  }
+}
+
+function discoverServerComponents(document: Document): ServerComponentDescriptor[] {
+  const componentComments = resolveComponentComments(document, 'server') as ServerComponentComment[];
+  const discoveredComponents: ServerComponentDescriptor[] = [];
+  for (let i = 0; i < componentComments.length; i++) {
+    const componentComment = componentComments[i];
+    const entry = new ServerComponentDescriptor(
+      componentComment.type,
+      componentComment.start,
+      componentComment.end,
+      componentComment.sequence,
+      componentComment.descriptor,
+    );
+
+    discoveredComponents.push(entry);
+  }
+
+  return discoveredComponents.sort((a, b): number => a.sequence - b.sequence);
+}
+
+function discoverWebAssemblyComponents(document: Document): WebAssemblyComponentDescriptor[] {
+  const componentComments = resolveComponentComments(document, 'webassembly') as WebAssemblyComponentDescriptor[];
+  const discoveredComponents: WebAssemblyComponentDescriptor[] = [];
+  for (let i = 0; i < componentComments.length; i++) {
+    const componentComment = componentComments[i];
+    const entry = new WebAssemblyComponentDescriptor(
+      componentComment.type,
+      componentComment.start,
+      componentComment.end,
+      componentComment.assembly,
+      componentComment.typeName,
+      componentComment.parameterDefinitions,
+      componentComment.parameterValues,
+    );
+
+    discoveredComponents.push(entry);
+  }
+
+  return discoveredComponents.sort((a, b): number => a.id - b.id);
+}
+
+interface ComponentComment {
+  type: 'server' | 'webassembly';
+  prerenderId?: string;
+}
+
+interface ServerComponentComment {
+  type: 'server';
+  sequence: number;
+  descriptor: string;
+  start: Node;
+  end?: Node;
+  prerenderId?: string;
+}
+
+interface WebAssemblyComponentComment {
+  type: 'webassembly';
+  typeName: string;
+  assembly: string;
+  parameterDefinitions?: string;
+  parameterValues?: string;
+  prerenderId?: string;
+  start: Node;
+  end?: Node;
+}
+
+function resolveComponentComments(node: Node, type: 'webassembly' | 'server'): ComponentComment[] {
+  if (!node.hasChildNodes()) {
+    return [];
+  }
+
+  const result: ComponentComment[] = [];
+  const childNodeIterator = new ComponentCommentIterator(node.childNodes);
+  while (childNodeIterator.next() && childNodeIterator.currentElement) {
+    const componentComment = getComponentComment(childNodeIterator, type);
+    if (componentComment) {
+      result.push(componentComment);
+    } else {
+      const childResults = resolveComponentComments(childNodeIterator.currentElement, type);
+      for (let j = 0; j < childResults.length; j++) {
+        const childResult = childResults[j];
+        result.push(childResult);
+      }
+    }
+  }
+
+  return result;
+}
+
+const blazorCommentRegularExpression = /\W*Blazor:[^{]*(?<descriptor>.*)$/;
+
+function getComponentComment(commentNodeIterator: ComponentCommentIterator, type: 'webassembly' | 'server'): ComponentComment | undefined {
+  const candidateStart = commentNodeIterator.currentElement;
+
+  if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
+    return;
+  }
+  if (candidateStart.textContent) {
+    const componentStartComment = new RegExp(blazorCommentRegularExpression);
+    const definition = componentStartComment.exec(candidateStart.textContent);
+    const json = definition && definition.groups && definition.groups['descriptor'];
+
+    if (json) {
+      try {
+        const componentComment = parseCommentPayload(json);
+        switch (type) {
+          case 'webassembly':
+            return createWebAssemblyComponentComment(componentComment as WebAssemblyComponentComment, candidateStart, commentNodeIterator);
+          case 'server':
+            return createServerComponentComment(componentComment as ServerComponentComment, candidateStart, commentNodeIterator);
+        }
+      } catch (error) {
+        throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
+      }
+    } else {
+      return;
+    }
+  }
+}
+
+function parseCommentPayload(json: string): ComponentComment {
+  const payload = JSON.parse(json) as ComponentComment;
+  const { type } = payload;
+  if (type !== 'server' && type !== 'webassembly') {
+    throw new Error(`Invalid component type '${type}'.`);
+  }
+
+  return payload;
+}
+
+function createServerComponentComment(payload: ServerComponentComment, start: Node, iterator: ComponentCommentIterator): ServerComponentComment | undefined {
+  const { type, descriptor, sequence, prerenderId } = payload;
+  if (type !== 'server') {
+    return undefined;
+  }
+
+  if (!descriptor) {
+    throw new Error('descriptor must be defined when using a descriptor.');
+  }
+
+  if (sequence === undefined) {
+    throw new Error('sequence must be defined when using a descriptor.');
+  }
+
+  if (!Number.isInteger(sequence)) {
+    throw new Error(`Error parsing the sequence '${sequence}' for component '${JSON.stringify(payload)}'`);
+  }
+
+  if (!prerenderId) {
+    return {
+      type,
+      sequence: sequence,
+      descriptor,
+      start,
+    };
+  } else {
+    const end = getComponentEndComment(prerenderId, iterator);
+    if (!end) {
+      throw new Error(`Could not find an end component comment for '${start}'`);
+    }
+
+    return {
+      type,
+      sequence,
+      descriptor,
+      start,
+      prerenderId,
+      end,
+    };
+  }
+}
+
+function createWebAssemblyComponentComment(payload: WebAssemblyComponentComment, start: Node, iterator: ComponentCommentIterator): WebAssemblyComponentComment | undefined {
+  const { type, assembly, typeName, parameterDefinitions, parameterValues, prerenderId } = payload;
+  if (type !== 'webassembly') {
+    return undefined;
+  }
+
+  if (!assembly) {
+    throw new Error('assembly must be defined when using a descriptor.');
+  }
+
+  if (!typeName) {
+    throw new Error('typeName must be defined when using a descriptor.');
+  }
+
+  if (!prerenderId) {
+    return {
+      type,
+      assembly,
+      typeName,
+      // Parameter definitions and values come Base64 encoded from the server, since they contain random data and can make the
+      // comment invalid. We could unencode them in .NET Code, but that would be slower to do and we can leverage the fact that
+      // JS provides a native function that will be much faster and that we are doing this work while we are fetching
+      // blazor.boot.json
+      parameterDefinitions: parameterDefinitions && atob(parameterDefinitions),
+      parameterValues: parameterValues && atob(parameterValues),
+      start,
+    };
+  } else {
+    const end = getComponentEndComment(prerenderId, iterator);
+    if (!end) {
+      throw new Error(`Could not find an end component comment for '${start}'`);
+    }
+
+    return {
+      type,
+      assembly,
+      typeName,
+      // Same comment as above.
+      parameterDefinitions: parameterDefinitions && atob(parameterDefinitions),
+      parameterValues: parameterValues && atob(parameterValues),
+      start,
+      prerenderId,
+      end,
+    };
+  }
+}
+
+function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
+  while (iterator.next() && iterator.currentElement) {
+    const node = iterator.currentElement;
+    if (node.nodeType !== Node.COMMENT_NODE) {
+      continue;
+    }
+    if (!node.textContent) {
+      continue;
+    }
+
+    const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
+    const json = definition && definition[1];
+    if (!json) {
+      continue;
+    }
+
+    validateEndComponentPayload(json, prerenderedId);
+
+    return node;
+  }
+
+  return undefined;
+}
+
+function validateEndComponentPayload(json: string, prerenderedId: string): void {
+  const payload = JSON.parse(json) as ComponentComment;
+  if (Object.keys(payload).length !== 1) {
+    throw new Error(`Invalid end of component comment: '${json}'`);
+  }
+  const prerenderedEndId = payload.prerenderId;
+  if (!prerenderedEndId) {
+    throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
+  }
+  if (prerenderedEndId !== prerenderedId) {
+    throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
+  }
+}
+
+class ComponentCommentIterator {
+
+  private childNodes: NodeListOf<ChildNode>;
+
+  private currentIndex: number;
+
+  private length: number;
+
+  public currentElement: ChildNode | undefined;
+
+  public constructor(childNodes: NodeListOf<ChildNode>) {
+    this.childNodes = childNodes;
+    this.currentIndex = -1;
+    this.length = childNodes.length;
+  }
+
+  public next(): boolean {
+    this.currentIndex++;
+    if (this.currentIndex < this.length) {
+      this.currentElement = this.childNodes[this.currentIndex];
+      return true;
+    } else {
+      this.currentElement = undefined;
+      return false;
+    }
+  }
+}
+
+interface ServerComponentMarker {
+  type: string;
+  sequence: number;
+  descriptor: string;
+}
+
+export class ServerComponentDescriptor {
+  public type: string;
+
+  public start: Node;
+
+  public end?: Node;
+
+  public sequence: number;
+
+  public descriptor: string;
+
+  public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
+    this.type = type;
+    this.start = start;
+    this.end = end;
+    this.sequence = sequence;
+    this.descriptor = descriptor;
+  }
+
+  public toRecord(): ServerComponentMarker {
+    const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
+    return result;
+  }
+}
+
+export class WebAssemblyComponentDescriptor {
+  private static globalId = 1;
+
+  public type: 'webassembly';
+
+  public typeName: string;
+
+  public assembly: string;
+
+  public parameterDefinitions?: string;
+
+  public parameterValues?: string;
+
+  public id: number;
+
+  public start: Node;
+
+  public end?: Node;
+
+  public constructor(type: 'webassembly', start: Node, end: Node | undefined, assembly: string, typeName: string, parameterDefinitions?: string, parameterValues?: string) {
+    this.id = WebAssemblyComponentDescriptor.globalId++;
+    this.type = type;
+    this.assembly = assembly;
+    this.typeName = typeName;
+    this.parameterDefinitions = parameterDefinitions;
+    this.parameterValues = parameterValues;
+    this.start = start;
+    this.end = end;
+  }
+}

+ 22 - 10
src/Components/Web/src/Forms/EditContextFieldClassExtensions.cs

@@ -2,7 +2,6 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using System.Linq;
 using System.Linq.Expressions;
 
 namespace Microsoft.AspNetCore.Components.Forms
@@ -13,6 +12,8 @@ namespace Microsoft.AspNetCore.Components.Forms
     /// </summary>
     public static class EditContextFieldClassExtensions
     {
+        private readonly static object FieldCssClassProviderKey = new object();
+
         /// <summary>
         /// Gets a string that indicates the status of the specified field as a CSS class. This will include
         /// some combination of "modified", "valid", or "invalid", depending on the status of the field.
@@ -24,23 +25,34 @@ namespace Microsoft.AspNetCore.Components.Forms
             => FieldCssClass(editContext, FieldIdentifier.Create(accessor));
 
         /// <summary>
-        /// Gets a string that indicates the status of the specified field as a CSS class. This will include
-        /// some combination of "modified", "valid", or "invalid", depending on the status of the field.
+        /// Gets a string that indicates the status of the specified field as a CSS class.
         /// </summary>
         /// <param name="editContext">The <see cref="EditContext"/>.</param>
         /// <param name="fieldIdentifier">An identifier for the field.</param>
         /// <returns>A string that indicates the status of the field.</returns>
         public static string FieldCssClass(this EditContext editContext, in FieldIdentifier fieldIdentifier)
         {
-            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
-            if (editContext.IsModified(fieldIdentifier))
-            {
-                return isValid ? "modified valid" : "modified invalid";
-            }
-            else
+            var provider = editContext.Properties.TryGetValue(FieldCssClassProviderKey, out var customProvider)
+                ? (FieldCssClassProvider)customProvider
+                : FieldCssClassProvider.Instance;
+
+            return provider.GetFieldCssClass(editContext, fieldIdentifier);
+        }
+
+        /// <summary>
+        /// Associates the supplied <see cref="FieldCssClassProvider"/> with the supplied <see cref="EditContext"/>.
+        /// This customizes the field CSS class names used within the <see cref="EditContext"/>.
+        /// </summary>
+        /// <param name="editContext">The <see cref="EditContext"/>.</param>
+        /// <param name="fieldCssClassProvider">The <see cref="FieldCssClassProvider"/>.</param>
+        public static void SetFieldCssClassProvider(this EditContext editContext, FieldCssClassProvider fieldCssClassProvider)
+        {
+            if (fieldCssClassProvider is null)
             {
-                return isValid ? "valid" : "invalid";
+                throw new ArgumentNullException(nameof(fieldCssClassProvider));
             }
+
+            editContext.Properties[FieldCssClassProviderKey] = fieldCssClassProvider;
         }
     }
 }

+ 35 - 0
src/Components/Web/src/Forms/FieldCssClassProvider.cs

@@ -0,0 +1,35 @@
+// 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.Linq;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    /// <summary>
+    /// Supplies CSS class names for form fields to represent their validation state or other
+    /// state information from an <see cref="EditContext"/>.
+    /// </summary>
+    public class FieldCssClassProvider
+    {
+        internal readonly static FieldCssClassProvider Instance = new FieldCssClassProvider();
+
+        /// <summary>
+        /// Gets a string that indicates the status of the specified field as a CSS class.
+        /// </summary>
+        /// <param name="editContext">The <see cref="EditContext"/>.</param>
+        /// <param name="fieldIdentifier">The <see cref="FieldIdentifier"/>.</param>
+        /// <returns>A CSS class name string.</returns>
+        public virtual string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
+        {
+            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
+            if (editContext.IsModified(fieldIdentifier))
+            {
+                return isValid ? "modified valid" : "modified invalid";
+            }
+            else
+            {
+                return isValid ? "valid" : "invalid";
+            }
+        }
+    }
+}

+ 15 - 20
src/Components/WebAssembly/DevServer/src/Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj

@@ -11,17 +11,26 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <!-- Set this to false because assemblies should not reference this assembly directly, (except for tests, of course). -->
     <IsProjectReferenceProvider>false</IsProjectReferenceProvider>
+
+    <!--
+      This project compiles against Microsoft.AspNetCore.App from the SDK.
+      This ensures that it's packaging output is correct and does not include local artifacts.
+    -->
+    <UseAspNetCoreSharedRuntime>true</UseAspNetCoreSharedRuntime>
+    <DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp>true</DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp>
   </PropertyGroup>
 
   <ItemGroup>
-    <Reference Include="Microsoft.AspNetCore" />
-    <Reference Include="Microsoft.AspNetCore.Diagnostics" />
-    <Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
-    <Reference Include="Microsoft.AspNetCore.Components.Server" />
-    <Reference Include="Microsoft.AspNetCore.ResponseCompression" />
+    <FrameworkReference Include="Microsoft.AspNetCore.App" />
 
+    <ProjectReference
+      Include="$(RepoRoot)src\Framework\App.Runtime\src\Microsoft.AspNetCore.App.Runtime.csproj"
+      PrivateAssets="All"
+      ReferenceOutputAssembly="false"
+      SkipGetTargetFrameworkProperties="true" />
+
+    <Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
     <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
-    <Reference Include="Microsoft.Extensions.Hosting" />
   </ItemGroup>
 
   <!-- Pack settings -->
@@ -36,19 +45,5 @@
     <NuspecProperty Include="PackageThirdPartyNoticesFile=$(PackageThirdPartyNoticesFile)" />
   </ItemGroup>
 
-  <Target Name="_FixupRuntimeConfig" BeforeTargets="_GenerateRuntimeConfigurationFilesInputCache">
-    <ItemGroup>
-      <_RuntimeFramework Include="@(RuntimeFramework)" />
-      <RuntimeFramework Remove="@(RuntimeFramework)" />
-      <RuntimeFramework Include="Microsoft.AspNetCore.App" FrameworkName="Microsoft.AspNetCore.App" Version="5.0.0-preview" />
-    </ItemGroup>
-  </Target>
-
-   <Target Name="_UndoRuntimeConfigWorkarounds" AfterTargets="GenerateBuildRuntimeConfigurationFiles">
-    <ItemGroup>
-      <RuntimeFramework Remove="@(RuntimeFramework)" />
-      <RuntimeFramework Include="@(_RuntimeFramework)" />
-    </ItemGroup>
-  </Target>
 
 </Project>

+ 1 - 6
src/Components/WebAssembly/JSInterop/src/InternalCalls.cs

@@ -16,12 +16,7 @@ namespace WebAssembly.JSInterop
         // in driver.c in the Mono distribution
         /// See: https://github.com/mono/mono/blob/90574987940959fe386008a850982ea18236a533/sdks/wasm/src/driver.c#L318-L319
 
-        // We're passing asyncHandle by ref not because we want it to be writable, but so it gets
-        // passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones.
         [MethodImpl(MethodImplOptions.InternalCall)]
-        public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
-
-        [MethodImpl(MethodImplOptions.InternalCall)]
-        public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
+        public static extern TRes InvokeJS<T0, T1, T2, TRes>(out string exception, ref JSCallInfo callInfo, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
     }
 }

+ 27 - 0
src/Components/WebAssembly/JSInterop/src/JSCallInfo.cs

@@ -0,0 +1,27 @@
+// 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.Runtime.InteropServices;
+using Microsoft.JSInterop;
+
+namespace WebAssembly.JSInterop
+{
+    [StructLayout(LayoutKind.Explicit, Pack = 4)]
+    internal struct JSCallInfo
+    {
+        [FieldOffset(0)]
+        public string FunctionIdentifier;
+
+        [FieldOffset(4)]
+        public JSCallResultType ResultType;
+
+        [FieldOffset(8)]
+        public string MarshalledCallArgsJson;
+
+        [FieldOffset(12)]
+        public long MarshalledCallAsyncHandle;
+
+        [FieldOffset(20)]
+        public long TargetInstanceId;
+    }
+}

+ 32 - 7
src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs

@@ -14,19 +14,37 @@ namespace Microsoft.JSInterop.WebAssembly
     public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalledRuntime
     {
         /// <inheritdoc />
-        protected override string InvokeJS(string identifier, string argsJson)
+        protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
         {
-            var noAsyncHandle = default(long);
-            var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson);
+            var callInfo = new JSCallInfo
+            {
+                FunctionIdentifier = identifier,
+                TargetInstanceId = targetInstanceId,
+                ResultType = resultType,
+                MarshalledCallArgsJson = argsJson ?? "[]",
+                MarshalledCallAsyncHandle = default
+            };
+
+            var result = InternalCalls.InvokeJS<object, object, object, string>(out var exception, ref callInfo, null, null, null);
+
             return exception != null
                 ? throw new JSException(exception)
                 : result;
         }
 
         /// <inheritdoc />
-        protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
+        protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
         {
-            InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson);
+            var callInfo = new JSCallInfo
+            {
+                FunctionIdentifier = identifier,
+                TargetInstanceId = targetInstanceId,
+                ResultType = resultType,
+                MarshalledCallArgsJson = argsJson ?? "[]",
+                MarshalledCallAsyncHandle = asyncHandle
+            };
+
+            InternalCalls.InvokeJS<object, object, object, string>(out _, ref callInfo, null, null, null);
         }
 
         protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult)
@@ -39,7 +57,7 @@ namespace Microsoft.JSInterop.WebAssembly
             // We pass 0 as the async handle because we don't want the JS-side code to
             // send back any notification (we're just providing a result for an existing async call)
             var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions);
-            BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
+            BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args, JSCallResultType.Default, 0);
         }
 
         /// <inheritdoc />
@@ -57,7 +75,14 @@ namespace Microsoft.JSInterop.WebAssembly
         /// <inheritdoc />
         TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
         {
-            var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TResult>(out var exception, identifier, arg0, arg1, arg2);
+            var callInfo = new JSCallInfo
+            {
+                FunctionIdentifier = identifier,
+                ResultType = ResultTypeFromGeneric<TResult>()
+            };
+
+            var result = InternalCalls.InvokeJS<T0, T1, T2, TResult>(out var exception, ref callInfo, arg0, arg1, arg2);
+
             return exception != null
                 ? throw new JSException(exception)
                 : result;

+ 22 - 0
src/Components/WebAssembly/WebAssembly/src/Hosting/RegisteredComponentsInterop.cs

@@ -0,0 +1,22 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
+{
+    internal class RegisteredComponentsInterop
+    {
+        private static readonly string Prefix = "Blazor._internal.registeredComponents.";
+
+        public static readonly string GetRegisteredComponentsCount = Prefix + "getRegisteredComponentsCount";
+
+        public static readonly string GetId = Prefix + "getId";
+
+        public static readonly string GetAssembly = Prefix + "getAssembly";
+
+        public static readonly string GetTypeName = Prefix + "getTypeName";
+
+        public static readonly string GetParameterDefinitions = Prefix + "getParameterDefinitions";
+
+        public static readonly string GetParameterValues = Prefix + "getParameterValues";
+    }
+}

+ 20 - 1
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentMapping.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;
 using Microsoft.AspNetCore.Components;
 
 namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
@@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
         /// and <paramref name="selector"/>.
         /// </summary>
         /// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
-        /// <param name="selector">The DOM element selector.</param>
+        /// <param name="selector">The DOM element selector or component registration id for the component.</param>
         public RootComponentMapping(Type componentType, string selector)
         {
             if (componentType is null)
@@ -38,6 +39,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
 
             ComponentType = componentType;
             Selector = selector;
+            Parameters = ParameterView.Empty;
+        }
+
+        /// <summary>
+        /// Creates a new instance of <see cref="RootComponentMapping"/> with the provided <paramref name="componentType"/>
+        /// and <paramref name="selector"/>.
+        /// </summary>
+        /// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
+        /// <param name="selector">The DOM element selector or registration id for the component.</param>
+        /// <param name="parameters">The parameters to pass to the component.</param>
+        public RootComponentMapping(Type componentType, string selector, ParameterView parameters) : this(componentType, selector)
+        {
+            Parameters = parameters;
         }
 
         /// <summary>
@@ -49,5 +63,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
         /// Gets the DOM element selector.
         /// </summary>
         public string Selector { get; }
+
+        /// <summary>
+        /// Gets the parameters to pass to the root component.
+        /// </summary>
+        public ParameterView Parameters { get; }
     }
 }

+ 12 - 1
src/Components/WebAssembly/WebAssembly/src/Hosting/RootComponentMappingCollection.cs

@@ -34,6 +34,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
         /// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
         /// <param name="selector">The DOM element selector.</param>
         public void Add(Type componentType, string selector)
+        {
+            Add(componentType, selector, ParameterView.Empty);
+        }
+
+        /// <summary>
+        /// Adds a component mapping to the collection.
+        /// </summary>
+        /// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
+        /// <param name="selector">The DOM element selector.</param>
+        /// <param name="parameters">The parameters to the root component.</param>
+        public void Add(Type componentType, string selector, ParameterView parameters)
         {
             if (componentType is null)
             {
@@ -45,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
                 throw new ArgumentNullException(nameof(selector));
             }
 
-            Add(new RootComponentMapping(componentType, selector));
+            Add(new RootComponentMapping(componentType, selector, parameters));
         }
 
         /// <summary>

+ 1 - 1
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs

@@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
                 for (var i = 0; i < rootComponents.Length; i++)
                 {
                     var rootComponent = rootComponents[i];
-                    await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector);
+                    await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector, rootComponent.Parameters);
                 }
 
                 await tcs.Task;

+ 36 - 1
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

@@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
     public sealed class WebAssemblyHostBuilder
     {
         private Func<IServiceProvider> _createServiceProvider;
+        private RootComponentTypeCache _rootComponentCache;
 
         /// <summary>
         /// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
@@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
 
             // Retrieve required attributes from JSRuntimeInvoker
             InitializeNavigationManager(jsRuntimeInvoker);
+            InitializeRegisteredRootComponents(jsRuntimeInvoker);
             InitializeDefaultServices();
 
             var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker);
@@ -68,6 +70,38 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
             };
         }
 
+        private void InitializeRegisteredRootComponents(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
+        {
+            var componentsCount = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, int>(RegisteredComponentsInterop.GetRegisteredComponentsCount, null, null, null);
+            if (componentsCount == 0)
+            {
+                return;
+            }
+
+            var registeredComponents = new WebAssemblyComponentMarker[componentsCount];
+            for (var i = 0; i < componentsCount; i++)
+            {
+                var id = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, int>(RegisteredComponentsInterop.GetId, i, null, null);
+                var assembly = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetAssembly, id, null, null);
+                var typeName = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetTypeName, id, null, null);
+                var serializedParameterDefinitions = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetParameterDefinitions, id, null, null);
+                var serializedParameterValues = jsRuntimeInvoker.InvokeUnmarshalled<int, object, object, string>(RegisteredComponentsInterop.GetParameterValues, id, null, null);
+                registeredComponents[i] = new WebAssemblyComponentMarker(WebAssemblyComponentMarker.ClientMarkerType, assembly, typeName, serializedParameterDefinitions, serializedParameterValues, id.ToString());
+            }
+
+            var componentDeserializer = WebAssemblyComponentParameterDeserializer.Instance;
+            foreach (var registeredComponent in registeredComponents)
+            {
+                _rootComponentCache = new RootComponentTypeCache();
+                var componentType = _rootComponentCache.GetRootComponent(registeredComponent.Assembly, registeredComponent.TypeName);
+                var definitions = componentDeserializer.GetParameterDefinitions(registeredComponent.ParameterDefinitions);
+                var values = componentDeserializer.GetParameterValues(registeredComponent.ParameterValues);
+                var parameters = componentDeserializer.DeserializeParameters(definitions, values);
+
+                RootComponents.Add(componentType, registeredComponent.PrerenderId, parameters);
+            }
+        }
+
         private void InitializeNavigationManager(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
         {
             var baseUri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetBaseUri, null, null, null);
@@ -190,7 +224,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
             Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
             Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
             Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
-            Services.AddLogging(builder => {
+            Services.AddLogging(builder =>
+            {
                 builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
             });
         }

+ 7 - 8
src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -15,13 +15,7 @@
     <Reference Include="Microsoft.Extensions.Logging" />
     <Reference Include="Microsoft.JSInterop.WebAssembly" />
 
-    <ProjectReference
-      Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj"
-      ReferenceOutputAssemblies="false"
-      SkipGetTargetFrameworkProperties="true"
-      UndefineProperties="TargetFramework"
-      Private="false"
-      Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
+    <ProjectReference Include="..\..\..\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" ReferenceOutputAssemblies="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework" Private="false" Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
 
     <SuppressBaselineReference Include="Microsoft.AspNetCore.Components.WebAssembly.HttpHandler" />
   </ItemGroup>
@@ -31,6 +25,11 @@
     <Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
     <Compile Include="$(ComponentsSharedSourceRoot)src\WebEventData.cs" />
     <Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
+    <Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
+    <Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
+    <Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" Link="Prerendering/WebAssemblyComponentSerializationSettings.cs" />
+    <Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" Link="Prerendering/WebAssemblyComponentMarker.cs" />
+    <Compile Include="$(SharedSourceRoot)Components\ComponentParameter.cs" Link="Prerendering/ComponentParameter.cs" />
   </ItemGroup>
 
   <ItemGroup>

+ 88 - 0
src/Components/WebAssembly/WebAssembly/src/Prerendering/ClientComponentParameterDeserializer.cs

@@ -0,0 +1,88 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.Components
+{
+    internal class WebAssemblyComponentParameterDeserializer
+    {
+        private readonly ComponentParametersTypeCache _parametersCache;
+
+        public WebAssemblyComponentParameterDeserializer(
+            ComponentParametersTypeCache parametersCache)
+        {
+            _parametersCache = parametersCache;
+        }
+
+        public static WebAssemblyComponentParameterDeserializer Instance { get; } = new WebAssemblyComponentParameterDeserializer(new ComponentParametersTypeCache());
+
+        public ParameterView DeserializeParameters(IList<ComponentParameter> parametersDefinitions, IList<object> parameterValues)
+        {
+            var parametersDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+
+            if (parameterValues.Count != parametersDefinitions.Count)
+            {
+                // Mismatched number of definition/parameter values.
+                throw new InvalidOperationException($"The number of parameter definitions '{parametersDefinitions.Count}' does not match the number parameter values '{parameterValues.Count}'.");
+            }
+
+            for (var i = 0; i < parametersDefinitions.Count; i++)
+            {
+                var definition = parametersDefinitions[i];
+                if (definition.Name == null)
+                {
+                    throw new InvalidOperationException("The name is missing in a parameter definition.");
+                }
+
+                if (definition.TypeName == null && definition.Assembly == null)
+                {
+                    parametersDictionary[definition.Name] = null;
+                }
+                else if (definition.TypeName == null || definition.Assembly == null)
+                {
+                    throw new InvalidOperationException($"The parameter definition for '{definition.Name}' is incomplete: Type='{definition.TypeName}' Assembly='{definition.Assembly}'.");
+                }
+                else
+                {
+                    var parameterType = _parametersCache.GetParameterType(definition.Assembly, definition.TypeName);
+                    if (parameterType == null)
+                    {
+                        throw new InvalidOperationException($"The parameter '{definition.Name} with type '{definition.TypeName}' in assembly '{definition.Assembly}' could not be found.");
+                    }
+                    try
+                    {
+                        var value = (JsonElement)parameterValues[i];
+                        var parameterValue = JsonSerializer.Deserialize(
+                            value.GetRawText(),
+                            parameterType,
+                            WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
+
+                        parametersDictionary[definition.Name] = parameterValue;
+                    }
+                    catch (Exception e)
+                    {
+                        throw new InvalidOperationException("Could not parse the parameter value for parameter '{definition.Name}' of type '{definition.TypeName}' and assembly '{definition.Assembly}'.", e);
+                    }
+                }
+            }
+
+            return ParameterView.FromDictionary(parametersDictionary);
+        }
+
+        public ComponentParameter[] GetParameterDefinitions(string parametersDefinitions)
+        {
+            return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
+        }
+
+        public IList<object> GetParameterValues(string parameterValues)
+        {
+            return JsonSerializer.Deserialize<IList<object>>(parameterValues, WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
+        }
+    }
+}

+ 6 - 4
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs

@@ -47,13 +47,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
         /// </summary>
         /// <typeparam name="TComponent">The type of the component.</typeparam>
         /// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
+        /// <param name="parameters">The parameters for the component.</param>
         /// <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);
+        public Task AddComponentAsync<TComponent>(string domElementSelector, ParameterView parameters) where TComponent : IComponent
+            => AddComponentAsync(typeof(TComponent), domElementSelector, parameters);
 
         /// <summary>
         /// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
@@ -61,12 +62,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
         /// </summary>
         /// <param name="componentType">The type of the component.</param>
         /// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
+        /// <param name="parameters">The list of root component parameters.</param>
         /// <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)
+        public Task AddComponentAsync(Type componentType, string domElementSelector, ParameterView parameters)
         {
             var component = InstantiateComponent(componentType);
             var componentId = AssignRootComponentId(component);
@@ -83,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
                 componentId,
                 _webAssemblyRendererId);
 
-            return RenderRootComponentAsync(componentId);
+            return RenderRootComponentAsync(componentId, parameters);
         }
 
         /// <inheritdoc />

+ 2 - 0
src/Components/WebAssembly/WebAssembly/test/TestWebAssemblyJSRuntimeInvoker.cs

@@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
                 case "Blazor._internal.navigationManager.getUnmarshalledLocationHref":
                     var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
                     return (TResult)(object)testHref;
+                case "Blazor._internal.registeredComponents.getRegisteredComponentsCount":
+                    return (TResult)(object)0;
                 default:
                     throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'.");
             }

+ 6 - 0
src/Components/WebAssembly/testassets/Wasm.Authentication.Server/Startup.cs

@@ -3,6 +3,7 @@ using System.Linq;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
@@ -57,6 +58,11 @@ namespace Wasm.Authentication.Server
                 app.UseWebAssemblyDebugging();
             }
 
+            app.UseCookiePolicy(new CookiePolicyOptions
+            {
+                MinimumSameSitePolicy = SameSiteMode.Lax
+            });
+
             app.UseBlazorFrameworkFiles();
             app.UseStaticFiles();
 

+ 2 - 0
src/Components/WebAssembly/testassets/Wasm.Authentication.Server/Wasm.Authentication.Server.csproj

@@ -10,10 +10,12 @@
 
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.AspNetCore.CookiePolicy" />
     <Reference Include="Microsoft.AspNetCore.Diagnostics" />
     <Reference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" />
     <Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
     <Reference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
+    <Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
     <Reference Include="Microsoft.EntityFrameworkCore.Design" />
     <Reference Include="Microsoft.AspNetCore.Identity.UI" />
     <Reference Include="Microsoft.EntityFrameworkCore.Relational" />

+ 11 - 0
src/Components/benchmarkapps/Wasm.Performance/Directory.Build.props

@@ -0,0 +1,11 @@
+<Project>
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
+
+  <PropertyGroup>
+    <!-- Makes our docker composition simpler by not redirecting build and publish output to the artifacts dir -->
+    <BaseIntermediateOutputPath />
+    <IntermediateOutputPath />
+    <BaseOutputPath />
+    <OutputPath />
+  </PropertyGroup>
+</Project>

+ 31 - 1
src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs

@@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Hosting.Server.Features;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
+using OpenQA.Selenium;
 using DevHostServerProgram = Microsoft.AspNetCore.Components.WebAssembly.DevServer.Server.Program;
 
 namespace Wasm.Performance.Driver
@@ -81,7 +82,20 @@ namespace Wasm.Performance.Driver
             {
                 BenchmarkResultTask = new TaskCompletionSource<BenchmarkResult>();
                 using var runCancellationToken = new CancellationTokenSource(timeForEachRun);
-                using var registration = runCancellationToken.Token.Register(() => BenchmarkResultTask.TrySetException(new TimeoutException($"Timed out after {timeForEachRun}")));
+                using var registration = runCancellationToken.Token.Register(() =>
+                {
+                    string exceptionMessage = $"Timed out after {timeForEachRun}.";
+                    try
+                    {
+                        var innerHtml = browser.FindElement(By.CssSelector(":first-child")).GetAttribute("innerHTML");
+                        exceptionMessage += Environment.NewLine + "Browser state: " + Environment.NewLine + innerHtml;
+                    }
+                    catch
+                    {
+                        // Do nothing;
+                    }
+                    BenchmarkResultTask.TrySetException(new TimeoutException(exceptionMessage));
+                });
 
                 var results = await BenchmarkResultTask.Task;
 
@@ -89,6 +103,11 @@ namespace Wasm.Performance.Driver
                     includeMetadata: firstRun,
                     isStressRun: isStressRun);
 
+                if (!isStressRun)
+                {
+                    PrettyPrint(results);
+                }
+
                 firstRun = false;
             } while (isStressRun && !stressRunCancellation.IsCancellationRequested);
 
@@ -230,6 +249,17 @@ namespace Wasm.Performance.Driver
             Console.WriteLine(builder);
         }
 
+        static void PrettyPrint(BenchmarkResult benchmarkResult)
+        {
+            Console.WriteLine();
+            Console.WriteLine("| Name | Description | Duration | NumExecutions | ");
+            Console.WriteLine("--------------------------");
+            foreach (var result in benchmarkResult.ScenarioResults)
+            {
+                Console.WriteLine($"| {result.Descriptor.Name} | {result.Name} | {result.Duration} | {result.NumExecutions} |");
+            }
+        }
+
         static IHost StartTestApp()
         {
             var args = new[]

+ 1 - 6
src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs

@@ -16,12 +16,7 @@ namespace Wasm.Performance.Driver
         const int SeleniumPort = 4444;
         static bool RunHeadlessBrowser = true;
 
-        static bool PoolForBrowserLogs =
-#if DEBUG
-            true;
-#else
-            false;
-#endif
+        static bool PoolForBrowserLogs = true;
 
         private static async ValueTask<Uri> WaitForServerAsync(int port, CancellationToken cancellationToken)
         {

+ 6 - 1
src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj

@@ -12,13 +12,18 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
     <Reference Include="Microsoft.AspNetCore.Cors" />
+    <Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
     <Reference Include="Selenium.Support" />
     <Reference Include="Selenium.WebDriver" />
-    <ProjectReference Include="..\..\..\WebAssembly\DevServer\src\Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj" />
     <ProjectReference Include="..\TestApp\Wasm.Performance.TestApp.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Include="..\..\..\WebAssembly\DevServer\src\Server\*.cs" />
+  </ItemGroup>
+
   <Target Name="_AddTestProjectMetadataAttributes" BeforeTargets="BeforeCompile">
     <ItemGroup>
       <AssemblyAttribute

+ 5 - 0
src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj

@@ -3,6 +3,11 @@
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
     <IsTestAssetProject>true</IsTestAssetProject>
+    <!--
+      Chrome in docker appears to run in to cache corruption issues when the cache is read multiple times over.
+      Clien caching isn't part of our performance measurement, so we'll skip it.
+    -->
+    <BlazorCacheBootResources>false</BlazorCacheBootResources>
   </PropertyGroup>
 
   <ItemGroup>

+ 2 - 1
src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj

@@ -44,7 +44,6 @@
     <ProjectReference Include="..\..\WebAssembly\testassets\HostedInAspNet.Client\HostedInAspNet.Client.csproj" />
     <ProjectReference Include="..\..\WebAssembly\testassets\HostedInAspNet.Server\HostedInAspNet.Server.csproj" />
     <ProjectReference Include="..\..\WebAssembly\testassets\StandaloneApp\StandaloneApp.csproj" />
-    <ProjectReference Include="..\..\WebAssembly\DevServer\src\Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj" />
     <ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
     <ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
     <ProjectReference Include="..\..\WebAssembly\testassets\Wasm.Authentication.Server\Wasm.Authentication.Server.csproj" />
@@ -59,6 +58,8 @@
     <Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
     <Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
     <Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
+    <Compile Include="$(RepoRoot)src\Shared\Components\WebAssemblyComponentSerializationSettings.cs" />
+    <Compile Include="$(RepoRoot)src\Shared\Components\WebAssemblyComponentMarker.cs" />
   </ItemGroup>
 
   <ItemGroup>

+ 95 - 0
src/Components/test/E2ETest/Tests/ClientRenderingMultpleComponentsTest.cs

@@ -0,0 +1,95 @@
+// 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.Linq;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using Microsoft.AspNetCore.E2ETesting;
+using OpenQA.Selenium;
+using TestServer;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.Components.E2ETests.Tests
+{
+    public class ClientRenderingMultpleComponentsTest : ServerTestBase<BasicTestAppServerSiteFixture<MultipleComponents>>
+    {
+        private const string MarkerPattern = ".*?<!--Blazor:(.*?)-->.*?";
+
+        public ClientRenderingMultpleComponentsTest(
+            BrowserFixture browserFixture,
+            BasicTestAppServerSiteFixture<MultipleComponents> serverFixture,
+            ITestOutputHelper output)
+            : base(browserFixture, serverFixture, output)
+        {
+        }
+
+        public DateTime LastLogTimeStamp { get; set; } = DateTime.MinValue;
+
+        public override async Task InitializeAsync()
+        {
+            await base.InitializeAsync();
+
+            // Capture the last log timestamp so that we can filter logs when we
+            // check for duplicate connections.
+            var lastLog = Browser.Manage().Logs.GetLog(LogType.Browser).LastOrDefault();
+            if (lastLog != null)
+            {
+                LastLogTimeStamp = lastLog.Timestamp;
+            }
+        }
+
+        [Fact]
+        public void CanRenderMultipleRootComponents()
+        {
+            Navigate("/Client/multiple-components");
+
+            var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
+
+            Assert.Equal(7, greets.Length); // 1 statically rendered + 5 prerendered + 1 server prerendered
+            Assert.DoesNotContain("Hello Red fish", greets);
+            Assert.Single(greets, "Hello John");
+            Assert.Single(greets, "Hello Abraham");
+            Assert.Equal(2, greets.Where(g => g == "Hello Blue fish").Count());
+            Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 server prerendered without parameters
+            var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
+            var markers = ReadMarkers(content);
+            var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
+            Assert.Equal(13, componentSequence.Length);
+
+            // Once the app starts, output changes
+            BeginInteractivity();
+
+            Browser.Exists(By.CssSelector("h3.interactive"));
+            var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
+            Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
+            Assert.Equal(2, updatedGreets.Where(g => g == "Hello Red fish").Count());
+            Assert.Equal(2, updatedGreets.Where(g => g == "Hello Blue fish").Count());
+            Assert.Single(updatedGreets.Where(g => string.Equals("Hello Albert", g)));
+            Assert.Single(updatedGreets.Where(g => string.Equals("Hello Abraham", g)));
+        }
+
+        private (WebAssemblyComponentMarker, WebAssemblyComponentMarker)[] ReadMarkers(string content)
+        {
+            content = content.Replace("\r\n", "");
+            var matches = Regex.Matches(content, MarkerPattern);
+            var markers = matches.Select(s => JsonSerializer.Deserialize<WebAssemblyComponentMarker>(
+                s.Groups[1].Value,
+                WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
+
+            var prerenderMarkers = markers.Where(m => m.PrerenderId != null).GroupBy(p => p.PrerenderId).Select(g => (g.First(), g.Skip(1).First())).ToArray();
+            var nonPrerenderMarkers = markers.Where(m => m.PrerenderId == null).Select(g => (g, (WebAssemblyComponentMarker)default)).ToArray();
+
+            return prerenderMarkers.Concat(nonPrerenderMarkers).ToArray();
+        }
+
+        private void BeginInteractivity()
+        {
+            Browser.FindElement(By.Id("load-boot-script")).Click();
+        }
+    }
+}

+ 17 - 0
src/Components/test/E2ETest/Tests/FormsTest.cs

@@ -560,6 +560,23 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
             Browser.Equal("", () => selectWithoutComponent.GetAttribute("value"));
         }
 
+        [Fact]
+        public void RespectsCustomFieldCssClassProvider()
+        {
+            var appElement = MountTypicalValidationComponent();
+            var socksInput = appElement.FindElement(By.ClassName("socks")).FindElement(By.TagName("input"));
+            var messagesAccessor = CreateValidationMessagesAccessor(appElement);
+
+            // Validates on edit
+            Browser.Equal("valid-socks", () => socksInput.GetAttribute("class"));
+            socksInput.SendKeys("Purple\t");
+            Browser.Equal("modified valid-socks", () => socksInput.GetAttribute("class"));
+
+            // Can become invalid
+            socksInput.SendKeys(" with yellow spots\t");
+            Browser.Equal("modified invalid-socks", () => socksInput.GetAttribute("class"));
+        }
+
         [Fact]
         public void NavigateOnSubmitWorks()
         {

+ 10 - 0
src/Components/test/E2ETest/Tests/InteropTest.cs

@@ -55,10 +55,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
                 ["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
                 ["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
                 ["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
+                ["roundTripJSObjectReferenceAsync"] = @"""successful""",
+                ["invokeDisposedJSObjectReferenceExceptionAsync"] = @"""JS object instance with ID",
                 ["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!",
                 ["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!",
                 ["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!",
                 ["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!",
+                ["JSObjectReferenceInvokeNonFunctionException"] = "The value 'nonFunction' is not a function.",
                 ["resultReturnDotNetObjectByRefAsync"] = "1001",
                 ["instanceMethodThisTypeNameAsync"] = @"""JavaScriptInterop""",
                 ["instanceMethodStringValueUpperAsync"] = @"""MY STRING""",
@@ -69,6 +72,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
                 ["testDtoAsync"] = "Same",
                 ["returnPrimitiveAsync"] = "123",
                 ["returnArrayAsync"] = "first,second",
+                ["jsObjectReference.identity"] = "Invoked from JSObjectReference",
+                ["jsObjectReference.nested.add"] = "5",
+                ["addViaJSObjectReference"] = "5",
+                ["jsObjectReferenceModule"] = "Returned from module!",
                 ["syncGenericInstanceMethod"] = @"""Initial value""",
                 ["asyncGenericInstanceMethod"] = @"""Updated value 1""",
             };
@@ -93,6 +100,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
                 ["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
                 ["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
                 ["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
+                ["roundTripJSObjectReference"] = @"""successful""",
+                ["invokeDisposedJSObjectReferenceException"] = @"""JS object instance with ID",
                 ["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!",
                 ["ExceptionFromSyncMethod"] = "Function threw an exception!",
                 ["resultReturnDotNetObjectByRefSync"] = "1000",
@@ -100,6 +109,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
                 ["instanceMethodStringValueUpper"] = @"""MY STRING""",
                 ["instanceMethodIncomingByRef"] = "123",
                 ["instanceMethodOutgoingByRef"] = "1234",
+                ["jsInProcessObjectReference.identity"] = "Invoked from JSInProcessObjectReference",
                 ["stringValueUpperSync"] = "MY STRING",
                 ["testDtoNonSerializedValueSync"] = "99999",
                 ["testDtoSync"] = "Same",

+ 47 - 0
src/Components/test/testassets/BasicTestApp/FormsTest/CustomFieldCssClassProvider.cs

@@ -0,0 +1,47 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Components.Forms;
+
+namespace BasicTestApp.FormsTest
+{
+    // For E2E testing, this is a rough example of a field CSS class provider that looks for
+    // a custom attribute defining CSS class names. It isn't very efficient (it does reflection
+    // and allocates on every invocation) but is sufficient for testing purposes.
+    public class CustomFieldCssClassProvider : FieldCssClassProvider
+    {
+        public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
+        {
+            var cssClassName = base.GetFieldCssClass(editContext, fieldIdentifier);
+
+            // If we can find a [CustomValidationClassName], use it
+            var propertyInfo = fieldIdentifier.Model.GetType().GetProperty(fieldIdentifier.FieldName);
+            if (propertyInfo != null)
+            {
+                var customValidationClassName = (CustomValidationClassNameAttribute)propertyInfo
+                    .GetCustomAttributes(typeof(CustomValidationClassNameAttribute), true)
+                    .FirstOrDefault();
+                if (customValidationClassName != null)
+                {
+                    cssClassName = string.Join(' ', cssClassName.Split(' ').Select(token => token switch
+                    {
+                        "valid" => customValidationClassName.Valid ?? token,
+                        "invalid" => customValidationClassName.Invalid ?? token,
+                        _ => token,
+                    }));
+                }
+            }
+
+            return cssClassName;
+        }
+    }
+
+    [AttributeUsage(AttributeTargets.Property)]
+    public class CustomValidationClassNameAttribute : Attribute
+    {
+        public string Valid { get; set; }
+        public string Invalid { get; set; }
+    }
+}

+ 7 - 0
src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor

@@ -66,6 +66,9 @@
             </InputRadioGroup>
         </InputRadioGroup>
     </p>
+    <p class="socks">
+        Socks color: <InputText @bind-Value="person.SocksColor" />
+    </p>
     <p class="accepts-terms">
         Accepts terms: <InputCheckbox @bind-Value="person.AcceptsTerms" title="You have to check this" />
     </p>
@@ -98,6 +101,7 @@
     protected override void OnInitialized()
     {
         editContext = new EditContext(person);
+        editContext.SetFieldCssClassProvider(new CustomFieldCssClassProvider());
         customValidationMessageStore = new ValidationMessageStore(editContext);
     }
 
@@ -145,6 +149,9 @@
         [Required, EnumDataType(typeof(Country))]
         public Country? Country { get; set; } = null;
 
+        [Required, StringLength(10), CustomValidationClassName(Valid = "valid-socks", Invalid = "invalid-socks")]
+        public string SocksColor { get; set; }
+
         public string Username { get; set; }
     }
 

+ 33 - 0
src/Components/test/testassets/BasicTestApp/InteropComponent.razor

@@ -46,6 +46,8 @@
     <p id="@nameof(SyncExceptionFromAsyncMethod)">@SyncExceptionFromAsyncMethod?.Message</p>
     <h2>@nameof(AsyncExceptionFromAsyncMethod)</h2>
     <p id="@nameof(AsyncExceptionFromAsyncMethod)">@AsyncExceptionFromAsyncMethod?.Message</p>
+    <h2>@nameof(JSObjectReferenceInvokeNonFunctionException)</h2>
+    <p id="@nameof(JSObjectReferenceInvokeNonFunctionException)">@JSObjectReferenceInvokeNonFunctionException?.Message</p>
 </div>
 @if (DoneWithInterop)
 {
@@ -59,6 +61,7 @@
     public JSException ExceptionFromSyncMethod { get; set; }
     public JSException SyncExceptionFromAsyncMethod { get; set; }
     public JSException AsyncExceptionFromAsyncMethod { get; set; }
+    public JSException JSObjectReferenceInvokeNonFunctionException { get; set; }
 
     public IDictionary<string, object> ReceiveDotNetObjectByRefResult { get; set; } = new Dictionary<string, object>();
     public IDictionary<string, object> ReceiveDotNetObjectByRefAsyncResult { get; set; } = new Dictionary<string, object>();
@@ -134,6 +137,28 @@
             ReturnValues["returnArray"] = string.Join(",", ((IJSInProcessRuntime)JSRuntime).Invoke<Segment[]>("returnArray").Select(x => x.Source).ToArray());
         }
 
+        var jsObjectReference = await JSRuntime.InvokeAsync<JSObjectReference>("returnJSObjectReference");
+        ReturnValues["jsObjectReference.identity"] = await jsObjectReference.InvokeAsync<string>("identity", "Invoked from JSObjectReference");
+        ReturnValues["jsObjectReference.nested.add"] = (await jsObjectReference.InvokeAsync<int>("nested.add", 2, 3)).ToString();
+        ReturnValues["addViaJSObjectReference"] = (await JSRuntime.InvokeAsync<int>("addViaJSObjectReference", jsObjectReference, 2, 3)).ToString();
+
+        try
+        {
+            await jsObjectReference.InvokeAsync<object>("nonFunction");
+        }
+        catch (JSException e)
+        {
+            JSObjectReferenceInvokeNonFunctionException = e;
+        }
+
+        var module = await JSRuntime.InvokeAsync<JSObjectReference>("import", "./js/testmodule.js");
+        ReturnValues["jsObjectReferenceModule"] = await module.InvokeAsync<string>("identity", "Returned from module!");
+
+        if (shouldSupportSyncInterop)
+        {
+            InvokeInProcessJSInterop();
+        }
+
         Invocations = invocations;
         DoneWithInterop = true;
     }
@@ -163,6 +188,14 @@
         ReceiveDotNetObjectByRefResult["testDto"] = result.TestDto.Value == passDotNetObjectByRef ? "Same" : "Different";
     }
 
+    public void InvokeInProcessJSInterop()
+    {
+        var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
+
+        var jsInProcObjectReference = inProcRuntime.Invoke<JSInProcessObjectReference>("returnJSObjectReference");
+        ReturnValues["jsInProcessObjectReference.identity"] = jsInProcObjectReference.Invoke<string>("identity", "Invoked from JSInProcessObjectReference");
+    }
+
     public class PassDotNetObjectByRefArgs
     {
         public string StringValue { get; set; }

+ 41 - 0
src/Components/test/testassets/BasicTestApp/InteropTest/JavaScriptInterop.cs

@@ -414,6 +414,47 @@ namespace BasicTestApp.InteropTest
             return objectByRef.Value.GetNonSerializedValue();
         }
 
+        [JSInvokable]
+        public static JSObjectReference RoundTripJSObjectReference(JSObjectReference jsObjectReference)
+        {
+            return jsObjectReference;
+        }
+
+        [JSInvokable]
+        public static async Task<JSObjectReference> RoundTripJSObjectReferenceAsync(JSObjectReference jSObjectReference)
+        {
+            await Task.Yield();
+            return jSObjectReference;
+        }
+
+        [JSInvokable]
+        public static string InvokeDisposedJSObjectReferenceException(JSInProcessObjectReference jsObjectReference)
+        {
+            try
+            {
+                jsObjectReference.Invoke<object>("noop");
+                return "No exception thrown";
+            } 
+            catch (JSException e)
+            {
+                return e.Message;
+            }
+        }
+
+        [JSInvokable]
+        public static async Task<string> InvokeDisposedJSObjectReferenceExceptionAsync(JSObjectReference jsObjectReference)
+        {
+            try
+            {
+                await jsObjectReference.InvokeVoidAsync("noop");
+                return "No exception thrown";
+            } 
+            catch (JSException e)
+            {
+                return e.Message;
+            }
+        }
+
         [JSInvokable]
         public InstanceMethodOutput InstanceMethod(InstanceMethodInput input)
         {

+ 46 - 1
src/Components/test/testassets/BasicTestApp/wwwroot/js/jsinteroptests.js

@@ -30,6 +30,17 @@ async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetO
     var returnDotNetObjectByRefResult = DotNet.invokeMethod(assemblyName, 'ReturnDotNetObjectByRef');
     results['resultReturnDotNetObjectByRefSync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefResult['Some sync instance']);
 
+    var jsObjectReference = DotNet.createJSObjectReference({
+        prop: 'successful',
+        noop: function () { }
+    });
+
+    var returnedObject = DotNet.invokeMethod(assemblyName, 'RoundTripJSObjectReference', jsObjectReference);
+    results['roundTripJSObjectReference'] = returnedObject && returnedObject.prop;
+
+    DotNet.disposeJSObjectReference(jsObjectReference);
+    results['invokeDisposedJSObjectReferenceException'] = DotNet.invokeMethod(assemblyName, 'InvokeDisposedJSObjectReferenceException', jsObjectReference);
+
     var instanceMethodResult = instanceMethodsTarget.invokeMethod('InstanceMethod', {
       stringValue: 'My string',
       dtoByRef: dotNetObjectByRef
@@ -66,6 +77,17 @@ async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetO
   const returnDotNetObjectByRefAsync = await DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync');
   results['resultReturnDotNetObjectByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefAsync['Some async instance']);
 
+  var jsObjectReference = DotNet.createJSObjectReference({
+    prop: 'successful',
+    noop: function () { }
+  });
+
+  var returnedObject = await DotNet.invokeMethodAsync(assemblyName, 'RoundTripJSObjectReferenceAsync', jsObjectReference);
+  results['roundTripJSObjectReferenceAsync'] = returnedObject && returnedObject.prop;
+
+  DotNet.disposeJSObjectReference(jsObjectReference);
+  results['invokeDisposedJSObjectReferenceExceptionAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'InvokeDisposedJSObjectReferenceExceptionAsync', jsObjectReference);
+
   const instanceMethodAsync = await instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
     stringValue: 'My string',
     dtoByRef: dotNetObjectByRef
@@ -167,6 +189,8 @@ window.jsInteropTests = {
   asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException,
   returnPrimitive: returnPrimitive,
   returnPrimitiveAsync: returnPrimitiveAsync,
+  returnJSObjectReference: returnJSObjectReference,
+  addViaJSObjectReference: addViaJSObjectReference,
   receiveDotNetObjectByRef: receiveDotNetObjectByRef,
   receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync
 };
@@ -195,6 +219,27 @@ function returnArrayAsync() {
   });
 }
 
+function returnJSObjectReference() {
+  return {
+    identity: function (value) {
+      return value;
+    },
+    nonFunction: 123,
+    nested: {
+      add: function (a, b) {
+        return a + b;
+      }
+    },
+    dispose: function () {
+      DotNet.disposeJSObjectReference(this);
+    },
+  };
+}
+
+function addViaJSObjectReference(jsObjectReference, a, b) {
+  return jsObjectReference.nested.add(a, b);
+}
+
 function functionThrowsException() {
   throw new Error('Function threw an exception!');
 }
@@ -258,4 +303,4 @@ function receiveDotNetObjectByRefAsync(incomingData) {
       testDto: testDto
     };
   });
-}
+}

+ 3 - 0
src/Components/test/testassets/BasicTestApp/wwwroot/js/testmodule.js

@@ -0,0 +1,3 @@
+export function identity(value) {
+    return value;
+}

+ 4 - 1
src/Components/test/testassets/TestServer/Components.TestServer.csproj

@@ -22,10 +22,13 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\..\WebAssembly\DevServer\src\Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj" />
     <ProjectReference Include="..\BasicTestApp\BasicTestApp.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Include="..\..\..\WebAssembly\DevServer\src\Server\*.cs" />
+  </ItemGroup>
+
   <ItemGroup>
     <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
       <_Parameter1>Microsoft.AspNetCore.Testing.BasicTestApp.ContentRoot</_Parameter1>

+ 12 - 0
src/Components/test/testassets/TestServer/MultipleComponents.cs

@@ -32,6 +32,18 @@ namespace TestServer
                 app.UseDeveloperExceptionPage();
             }
 
+            app.Map("/Client/multiple-components", app =>
+            {
+                app.UseBlazorFrameworkFiles();
+                app.UseStaticFiles();
+                app.UseRouting();
+                app.UseEndpoints(endpoints =>
+                {
+                    endpoints.MapRazorPages();
+                    endpoints.MapFallbackToPage("/Client/MultipleComponents");
+                });
+            });
+
             app.Map("/multiple-components", app =>
             {
                 app.UseStaticFiles();

+ 29 - 0
src/Components/test/testassets/TestServer/Pages/Client/MultipleComponents.cshtml

@@ -0,0 +1,29 @@
+@page "/multiple-components"
+@using BasicTestApp.MultipleComponents;
+
+@{
+    Layout = "./MultipleComponentsLayout.cshtml";
+}
+
+
+@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.WebAssemblyPrerendered))
+@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.WebAssembly))
+<component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
+<component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
+<div id="container">
+    <p>Some content before</p>
+    <component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
+    <p>Some content between</p>
+    <component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" />
+    <p>Some content after</p>
+    <div id="nested-an-extra-level">
+        <p>Some content before</p>
+        <component type="typeof(GreeterComponent)" render-mode="WebAssembly" />
+        <component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" />
+        <p>Some content after</p>
+    </div>
+</div>
+<div id="container">
+    <component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Albert"' />
+    <component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Abraham"' />
+</div>

+ 45 - 0
src/Components/test/testassets/TestServer/Pages/Client/MultipleComponentsLayout.cshtml

@@ -0,0 +1,45 @@
+@using BasicTestApp.MultipleComponents;
+
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Multiple component entry points</title>
+    @* We need to make sure base is set to "/" so that the libraries load correctly *@
+    <base href="~/" />
+    @* This page is used to validate the ability to render multiple root components in a blazor webassembly application.
+    *@
+</head>
+<body>
+    <div id="test-container">
+        <component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Red fish"' />
+        <component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Blue fish"' />
+        @RenderBody()
+        <component type="typeof(GreeterComponent)" render-mode="WebAssembly" param-name='"Red fish"' />
+        <component type="typeof(GreeterComponent)" render-mode="WebAssemblyPrerendered" param-name='"Blue fish"' />
+    </div>
+
+    @*
+        So that E2E tests can make assertions about both the prerendered and
+        interactive states, we only load the .js file when told to.
+    *@
+    <hr />
+
+    <script>
+        // Used unconditionally on Program.cs, so needs to be defined here to avoid the test failing
+        function getCurrentUrl() {
+            return location.href;
+        }
+    </script>
+
+    <button id="load-boot-script" onclick="start()">Load boot script</button>
+
+    <script src="_framework/blazor.webassembly.js" autostart="false"></script>
+    <script>
+        function start() {
+            Blazor.start({
+                logLevel: 1 // LogLevel.Debug
+            });
+        }
+    </script>
+</body>
+</html>

+ 9 - 4
src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs

@@ -29,13 +29,18 @@ namespace Microsoft.AspNetCore.Hosting
         private AggregateException _hostingStartupErrors;
         private HostingStartupWebHostBuilder _hostingStartupWebHostBuilder;
 
-        public GenericWebHostBuilder(IHostBuilder builder)
+        public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
         {
             _builder = builder;
+            var configBuilder = new ConfigurationBuilder()
+                .AddInMemoryCollection();
 
-            _config = new ConfigurationBuilder()
-                .AddEnvironmentVariables(prefix: "ASPNETCORE_")
-                .Build();
+            if (!options.SuppressEnvironmentConfiguration)
+            {
+                configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_");
+            }
+
+            _config = configBuilder.Build();
 
             _builder.ConfigureHostConfiguration(config =>
             {

+ 32 - 1
src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs

@@ -1,3 +1,6 @@
+// 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.AspNetCore.Hosting;
 using Microsoft.Extensions.DependencyInjection;
@@ -6,9 +9,37 @@ namespace Microsoft.Extensions.Hosting
 {
     public static class GenericHostWebHostBuilderExtensions
     {
+        /// <summary>
+        /// Adds and configures an ASP.NET Core web application.
+        /// </summary>
         public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
         {
-            var webhostBuilder = new GenericWebHostBuilder(builder);
+            if (configure is null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            return builder.ConfigureWebHost(configure, _ => { });
+        }
+
+        /// <summary>
+        /// Adds and configures an ASP.NET Core web application.
+        /// </summary>
+        public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
+        {
+            if (configure is null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            if (configureWebHostBuilder is null)
+            {
+                throw new ArgumentNullException(nameof(configureWebHostBuilder));
+            }
+
+            var webHostBuilderOptions = new WebHostBuilderOptions();
+            configureWebHostBuilder(webHostBuilderOptions);
+            var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
             configure(webhostBuilder);
             builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
             return builder;

+ 17 - 0
src/Hosting/Hosting/src/WebHostBuilderOptions.cs

@@ -0,0 +1,17 @@
+// 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.
+
+namespace Microsoft.Extensions.Hosting
+{
+    /// <summary>
+    /// Builder options for use with ConfigureWebHost.
+    /// </summary>
+    public class WebHostBuilderOptions
+    {
+        /// <summary>
+        /// Indicates if "ASPNETCORE_" prefixed environment variables should be added to configuration.
+        /// They are added by default.
+        /// </summary>
+        public bool SuppressEnvironmentConfiguration { get; set; } = false;
+    }
+}

+ 1 - 1
src/Hosting/Hosting/test/Fakes/GenericWebHostBuilderWrapper.cs

@@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Hosting.Tests.Fakes
 
         internal GenericWebHostBuilderWrapper(HostBuilder hostBuilder)
         {
-            _builder = new GenericWebHostBuilder(hostBuilder);
+            _builder = new GenericWebHostBuilder(hostBuilder, new WebHostBuilderOptions());
             _hostBuilder = hostBuilder;
         }
 

+ 41 - 0
src/Hosting/Hosting/test/GenericWebHostBuilderTests.cs

@@ -0,0 +1,41 @@
+// 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.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Hosting
+{
+    // Most functionality is covered by WebHostBuilderTests for compat. Only GenericHost specific functionality is covered here.
+    public class GenericWebHostBuilderTests
+    {
+        [Fact]
+        public void ReadsAspNetCoreEnvironmentVariables()
+        {
+            var randomEnvKey = Guid.NewGuid().ToString();
+            Environment.SetEnvironmentVariable("ASPNETCORE_" + randomEnvKey, "true");
+            using var host = new HostBuilder()
+                .ConfigureWebHost(_ => { })
+                .Build();
+            var config = host.Services.GetRequiredService<IConfiguration>();
+            Assert.Equal("true", config[randomEnvKey]);
+            Environment.SetEnvironmentVariable("ASPNETCORE_" + randomEnvKey, null);
+        }
+
+        [Fact]
+        public void CanSuppressAspNetCoreEnvironmentVariables()
+        {
+            var randomEnvKey = Guid.NewGuid().ToString();
+            Environment.SetEnvironmentVariable("ASPNETCORE_" + randomEnvKey, "true");
+            using var host = new HostBuilder()
+                .ConfigureWebHost(_ => { }, webHostBulderOptions => { webHostBulderOptions.SuppressEnvironmentConfiguration  = true; })
+                .Build();
+            var config = host.Services.GetRequiredService<IConfiguration>();
+            Assert.Null(config[randomEnvKey]);
+            Environment.SetEnvironmentVariable("ASPNETCORE_" + randomEnvKey, null);
+        }
+    }
+}

+ 0 - 4
src/Hosting/Hosting/test/WebHostBuilderTests.cs

@@ -6,19 +6,15 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
-using System.Runtime.ExceptionServices;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Web;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting.Fakes;
 using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Hosting.Tests.Fakes;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Http.Features;
-using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;

+ 175 - 39
src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts

@@ -6,9 +6,68 @@ export module DotNet {
   export type JsonReviver = ((key: any, value: any) => any);
   const jsonRevivers: JsonReviver[] = [];
 
+  class JSObject {
+    _cachedFunctions: Map<string, Function>;
+
+    constructor(private _jsObject: any)
+    {
+      this._cachedFunctions = new Map<string, Function>();
+    }
+
+    public findFunction(identifier: string) {
+      const cachedFunction = this._cachedFunctions.get(identifier);
+
+      if (cachedFunction) {
+        return cachedFunction;
+      }
+
+      let result: any = this._jsObject;
+      let lastSegmentValue: any;
+
+      identifier.split('.').forEach(segment => {
+        if (segment in result) {
+          lastSegmentValue = result;
+          result = result[segment];
+        } else {
+          throw new Error(`Could not find '${identifier}' ('${segment}' was undefined).`);
+        }
+      });
+
+      if (result instanceof Function) {
+        result = result.bind(lastSegmentValue);
+        this._cachedFunctions.set(identifier, result);
+        return result;
+      } else {
+        throw new Error(`The value '${identifier}' is not a function.`);
+      }
+    }
+
+    public getWrappedObject() {
+      return this._jsObject;
+    }
+  }
+
+  const jsObjectIdKey = "__jsObjectId";
+
   const pendingAsyncCalls: { [id: number]: PendingAsyncCall<any> } = {};
-  const cachedJSFunctions: { [identifier: string]: Function } = {};
+  const windowJSObjectId = 0;
+  const cachedJSObjectsById: { [id: number]: JSObject } = {
+    [windowJSObjectId]: new JSObject(window),
+  };
+
+  cachedJSObjectsById[windowJSObjectId]._cachedFunctions.set('import', (url: any) => {
+    // In most cases developers will want to resolve dynamic imports relative to the base HREF.
+    // However since we're the one calling the import keyword, they would be resolved relative to
+    // this framework bundle URL. Fix this by providing an absolute URL.
+    if (typeof url === 'string' && url.startsWith('./')) {
+      url = document.baseURI + url.substr(2);
+    }
+
+    return import(/* webpackIgnore: true */ url);
+  });
+
   let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed"
+  let nextJsObjectId = 1; // Start at 1 because zero is reserved for "window"
 
   let dotNetDispatcher: DotNetCallDispatcher | null = null;
 
@@ -55,6 +114,58 @@ export module DotNet {
     return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
   }
 
+  /**
+   * Creates a JavaScript object reference that can be passed to .NET via interop calls.
+   *
+   * @param jsObject The JavaScript Object used to create the JavaScript object reference.
+   * @returns The JavaScript object reference (this will be the same instance as the given object).
+   * @throws Error if the given value is not an Object.
+   */
+  export function createJSObjectReference(jsObject: any): any {
+    if (jsObject && typeof jsObject === 'object') {
+      cachedJSObjectsById[nextJsObjectId] = new JSObject(jsObject);
+
+      const result = {
+        [jsObjectIdKey]: nextJsObjectId
+      };
+
+      nextJsObjectId++;
+
+      return result;
+    } else {
+      throw new Error(`Cannot create a JSObjectReference from the value '${jsObject}'.`);
+    }
+  }
+
+  /**
+   * Disposes the given JavaScript object reference.
+   *
+   * @param jsObjectReference The JavaScript Object reference.
+   */
+  export function disposeJSObjectReference(jsObjectReference: any): void {
+    const id = jsObjectReference && jsObjectReference[jsObjectIdKey];
+
+    if (typeof id === 'number') {
+      disposeJSObjectReferenceById(id);
+    }
+  }
+
+  /**
+   * Parses the given JSON string using revivers to restore args passed from .NET to JS.
+   * 
+   * @param json The JSON stirng to parse.
+   */
+  export function parseJsonWithRevivers(json: string): any {
+    return json ? JSON.parse(json, (key, initialValue) => {
+      // Invoke each reviver in order, passing the output from the previous reviver,
+      // so that each one gets a chance to transform the value
+      return jsonRevivers.reduce(
+        (latestValue, reviver) => reviver(key, latestValue),
+        initialValue
+      );
+    }) : null;
+  }
+
   function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T {
     const dispatcher = getRequiredDispatcher();
     if (dispatcher.invokeDotNetFromJS) {
@@ -114,6 +225,14 @@ export module DotNet {
     reject: (reason?: any) => void;
   }
 
+  /**
+   * Represents the type of result expected from a JS interop call.
+   */
+  export enum JSCallResultType {
+    Default = 0,
+    JSObjectReference = 1
+  }
+
   /**
    * Represents the ability to dispatch calls from JavaScript to a .NET runtime.
    */
@@ -158,19 +277,31 @@ export module DotNet {
      * Finds the JavaScript function matching the specified identifier.
      *
      * @param identifier Identifies the globally-reachable function to be returned.
+     * @param targetInstanceId The instance ID of the target JS object.
      * @returns A Function instance.
      */
     findJSFunction, // Note that this is used by the JS interop code inside Mono WebAssembly itself
 
+    /**
+     * Disposes the JavaScript object reference with the specified object ID.
+     *
+     * @param id The ID of the JavaScript object reference.
+     */
+    disposeJSObjectReferenceById,
+
     /**
      * Invokes the specified synchronous JavaScript function.
      *
      * @param identifier Identifies the globally-reachable function to invoke.
      * @param argsJson JSON representation of arguments to be passed to the function.
+     * @param resultType The type of result expected from the JS interop call.
+     * @param targetInstanceId The instance ID of the target JS object.
      * @returns JSON representation of the invocation result.
      */
-    invokeJSFromDotNet: (identifier: string, argsJson: string) => {
-      const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
+    invokeJSFromDotNet: (identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number) => {
+      const returnValue = findJSFunction(identifier, targetInstanceId).apply(null, parseJsonWithRevivers(argsJson));
+      const result = createJSCallResult(returnValue, resultType);
+
       return result === null || result === undefined
         ? null
         : JSON.stringify(result, argReplacer);
@@ -182,12 +313,14 @@ export module DotNet {
      * @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet.
      * @param identifier Identifies the globally-reachable function to invoke.
      * @param argsJson JSON representation of arguments to be passed to the function.
+     * @param resultType The type of result expected from the JS interop call.
+     * @param targetInstanceId The ID of the target JS object instance.
      */
-    beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string): void => {
+    beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string, resultType: JSCallResultType, targetInstanceId: number): void => {
       // Coerce synchronous functions into async ones, plus treat
       // synchronous exceptions the same as async ones
       const promise = new Promise<any>(resolve => {
-        const synchronousResultOrPromise = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
+        const synchronousResultOrPromise = findJSFunction(identifier, targetInstanceId).apply(null, parseJsonWithRevivers(argsJson));
         resolve(synchronousResultOrPromise);
       });
 
@@ -196,7 +329,7 @@ export module DotNet {
         // On completion, dispatch result back to .NET
         // Not using "await" because it codegens a lot of boilerplate
         promise.then(
-          result => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, true, JSON.stringify([asyncHandle, true, result], argReplacer)),
+          result => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, true, JSON.stringify([asyncHandle, true, createJSCallResult(result, resultType)], argReplacer)),
           error => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, false, JSON.stringify([asyncHandle, false, formatError(error)]))
         );
       }
@@ -214,17 +347,6 @@ export module DotNet {
     }
   }
 
-  function parseJsonWithRevivers(json: string): any {
-    return json ? JSON.parse(json, (key, initialValue) => {
-      // Invoke each reviver in order, passing the output from the previous reviver,
-      // so that each one gets a chance to transform the value
-      return jsonRevivers.reduce(
-        (latestValue, reviver) => reviver(key, latestValue),
-        initialValue
-      );
-    }) : null;
-  }
-
   function formatError(error: any): string {
     if (error instanceof Error) {
       return `${error.message}\n${error.stack}`;
@@ -233,33 +355,20 @@ export module DotNet {
     }
   }
 
-  function findJSFunction(identifier: string): Function {
-    if (Object.prototype.hasOwnProperty.call(cachedJSFunctions, identifier)) {
-      return cachedJSFunctions[identifier];
-    }
+  function findJSFunction(identifier: string, targetInstanceId: number): Function {
+    let targetInstance = cachedJSObjectsById[targetInstanceId];
 
-    let result: any = window;
-    let resultIdentifier = 'window';
-    let lastSegmentValue: any;
-    identifier.split('.').forEach(segment => {
-      if (segment in result) {
-        lastSegmentValue = result;
-        result = result[segment];
-        resultIdentifier += '.' + segment;
-      } else {
-        throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`);
-      }
-    });
-
-    if (result instanceof Function) {
-      result = result.bind(lastSegmentValue);
-      cachedJSFunctions[identifier] = result;
-      return result;
+    if (targetInstance) {
+      return targetInstance.findFunction(identifier);
     } else {
-      throw new Error(`The value '${resultIdentifier}' is not a function.`);
+      throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`);
     }
   }
 
+  function disposeJSObjectReferenceById(id: number) {
+    delete cachedJSObjectsById[id];
+  }
+
   class DotNetObject {
     constructor(private _id: number) {
     }
@@ -292,6 +401,33 @@ export module DotNet {
     return value;
   });
 
+  attachReviver(function reviveJSObjectReference(key: any, value: any) {
+    if (value && typeof value === 'object' && value.hasOwnProperty(jsObjectIdKey)) {
+      const id = value[jsObjectIdKey];
+      const jsObject = cachedJSObjectsById[id];
+
+      if (jsObject) {
+        return jsObject.getWrappedObject();
+      } else {
+        throw new Error(`JS object instance with ID ${id} does not exist (has it been disposed?).`);
+      }
+    }
+
+    // Unrecognized - let another reviver handle it
+    return value;
+  });
+
+  function createJSCallResult(returnValue: any, resultType: JSCallResultType) {
+    switch (resultType) {
+      case JSCallResultType.Default:
+        return returnValue;
+      case JSCallResultType.JSObjectReference:
+        return createJSObjectReference(returnValue);
+      default:
+        throw new Error(`Invalid JS call result type '${resultType}'.`);
+    }
+  }
+
   function argReplacer(key: string, value: any) {
     return value instanceof DotNetObject ? value.serializeAsArg() : value;
   }

+ 2 - 1
src/JSInterop/Microsoft.JSInterop.JS/src/tsconfig.json

@@ -8,7 +8,8 @@
         "lib": ["es2015", "dom", "es2015.promise"],
         "strict": true,
         "declaration": true,
-        "outDir": "dist"
+        "outDir": "dist",
+        "module": "ESNext",
     },
     "include": [
         "src/**/*.ts"

+ 59 - 0
src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSObjectReferenceJsonConverter.cs

@@ -0,0 +1,59 @@
+// 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.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.JSInterop.Infrastructure
+{
+    internal sealed class JSObjectReferenceJsonConverter<TJSObjectReference>
+        : JsonConverter<TJSObjectReference> where TJSObjectReference : JSObjectReference
+    {
+        private readonly Func<long, TJSObjectReference> _jsObjectReferenceFactory;
+
+        public JSObjectReferenceJsonConverter(Func<long, TJSObjectReference> jsObjectReferenceFactory)
+        {
+            _jsObjectReferenceFactory = jsObjectReferenceFactory;
+        }
+
+        public override TJSObjectReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            long id = -1;
+
+            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+            {
+                if (reader.TokenType == JsonTokenType.PropertyName)
+                {
+                    if (id == -1 && reader.ValueTextEquals(JSObjectReference.IdKey.EncodedUtf8Bytes))
+                    {
+                        reader.Read();
+                        id = reader.GetInt64();
+                    }
+                    else
+                    {
+                        throw new JsonException($"Unexcepted JSON property {reader.GetString()}.");
+                    }
+                }
+                else
+                {
+                    throw new JsonException($"Unexcepted JSON token {reader.TokenType}");
+                }
+            }
+
+            if (id == -1)
+            {
+                throw new JsonException($"Required property {JSObjectReference.IdKey} not found.");
+            }
+
+            return _jsObjectReferenceFactory(id);
+        }
+
+        public override void Write(Utf8JsonWriter writer, TJSObjectReference value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteNumber(JSObjectReference.IdKey, value.Id);
+            writer.WriteEndObject();
+        }
+    }
+}

+ 21 - 0
src/JSInterop/Microsoft.JSInterop/src/JSCallResultType.cs

@@ -0,0 +1,21 @@
+// 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.
+
+namespace Microsoft.JSInterop
+{
+    /// <summary>
+    /// Describes the type of result expected from a JS interop call.
+    /// </summary>
+    public enum JSCallResultType : int
+    {
+        /// <summary>
+        /// Indicates that the returned value is not treated in a special way.
+        /// </summary>
+        Default = 0,
+
+        /// <summary>
+        /// Indicates that the returned value is to be treated as a JS object reference.
+        /// </summary>
+        JSObjectReference = 1,
+    }
+}

+ 35 - 0
src/JSInterop/Microsoft.JSInterop/src/JSInProcessObjectReference.cs

@@ -0,0 +1,35 @@
+// 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.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.JSInterop
+{
+    /// <summary>
+    /// Represents a reference to a JavaScript object whose functions can be invoked synchronously.
+    /// </summary>
+    public class JSInProcessObjectReference : JSObjectReference
+    {
+        private readonly JSInProcessRuntime _jsRuntime;
+
+        internal JSInProcessObjectReference(JSInProcessRuntime jsRuntime, long id) : base(jsRuntime, id)
+        {
+            _jsRuntime = jsRuntime;
+        }
+
+        /// <summary>
+        /// Invokes the specified JavaScript function synchronously.
+        /// </summary>
+        /// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
+        /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
+        /// <param name="args">JSON-serializable arguments.</param>
+        /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
+        [return: MaybeNull]
+        public TValue Invoke<TValue>(string identifier, params object[] args)
+        {
+            ThrowIfDisposed();
+
+            return _jsRuntime.Invoke<TValue>(identifier, Id, args);
+        }
+    }
+}

+ 38 - 8
src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs

@@ -3,6 +3,7 @@
 
 using System.Diagnostics.CodeAnalysis;
 using System.Text.Json;
+using Microsoft.JSInterop.Infrastructure;
 
 namespace Microsoft.JSInterop
 {
@@ -12,16 +13,23 @@ namespace Microsoft.JSInterop
     public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime
     {
         /// <summary>
-        /// Invokes the specified JavaScript function synchronously.
+        /// Initializes a new instance of <see cref="JSInProcessRuntime"/>.
         /// </summary>
-        /// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
-        /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
-        /// <param name="args">JSON-serializable arguments.</param>
-        /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
+        protected JSInProcessRuntime()
+        {
+            JsonSerializerOptions.Converters.Add(new JSObjectReferenceJsonConverter<JSInProcessObjectReference>(
+                id => new JSInProcessObjectReference(this, id)));
+        }
+
         [return: MaybeNull]
-        public TValue Invoke<TValue>(string identifier, params object?[]? args)
+        internal TValue Invoke<TValue>(string identifier, long targetInstanceId, params object?[]? args)
         {
-            var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions));
+            var resultJson = InvokeJS(
+                identifier,
+                JsonSerializer.Serialize(args, JsonSerializerOptions),
+                ResultTypeFromGeneric<TValue>(),
+                targetInstanceId);
+
             if (resultJson is null)
             {
                 return default;
@@ -30,12 +38,34 @@ namespace Microsoft.JSInterop
             return JsonSerializer.Deserialize<TValue>(resultJson, JsonSerializerOptions);
         }
 
+        /// <summary>
+        /// Invokes the specified JavaScript function synchronously.
+        /// </summary>
+        /// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
+        /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
+        /// <param name="args">JSON-serializable arguments.</param>
+        /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
+        [return: MaybeNull]
+        public TValue Invoke<TValue>(string identifier, params object?[]? args)
+            => Invoke<TValue>(identifier, 0, args);
+
+        /// <summary>
+        /// Performs a synchronous function invocation.
+        /// </summary>
+        /// <param name="identifier">The identifier for the function to invoke.</param>
+        /// <param name="argsJson">A JSON representation of the arguments.</param>
+        /// <returns>A JSON representation of the result.</returns>
+        protected virtual string? InvokeJS(string identifier, string? argsJson)
+            => InvokeJS(identifier, argsJson, JSCallResultType.Default, 0);
+
         /// <summary>
         /// Performs a synchronous function invocation.
         /// </summary>
         /// <param name="identifier">The identifier for the function to invoke.</param>
         /// <param name="argsJson">A JSON representation of the arguments.</param>
+        /// <param name="resultType">The type of result expected from the invocation.</param>
+        /// <param name="targetInstanceId">The instance ID of the target JS object.</param>
         /// <returns>A JSON representation of the result.</returns>
-        protected abstract string? InvokeJS(string identifier, string? argsJson);
+        protected abstract string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId);
     }
 }

+ 103 - 0
src/JSInterop/Microsoft.JSInterop/src/JSObjectReference.cs

@@ -0,0 +1,103 @@
+// 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.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.JSInterop
+{
+    /// <summary>
+    /// Represents a reference to a JavaScript object.
+    /// </summary>
+    public class JSObjectReference : IAsyncDisposable
+    {
+        internal static readonly JsonEncodedText IdKey = JsonEncodedText.Encode("__jsObjectId");
+
+        private readonly JSRuntime _jsRuntime;
+
+        private bool _disposed;
+
+        internal long Id { get; }
+
+        internal JSObjectReference(JSRuntime jsRuntime, long id)
+        {
+            _jsRuntime = jsRuntime;
+
+            Id = id;
+        }
+
+        /// <summary>
+        /// Invokes the specified JavaScript function asynchronously.
+        /// </summary>
+        /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
+        /// <param name="args">JSON-serializable arguments.</param>
+        /// <returns>A <see cref="ValueTask"/> that represents the asynchronous invocation operation.</returns>
+        public async ValueTask InvokeVoidAsync(string identifier, params object[] args)
+        {
+            await InvokeAsync<object>(identifier, args);
+        }
+
+        /// <summary>
+        /// Invokes the specified JavaScript function asynchronously.
+        /// <para>
+        /// <see cref="JSRuntime"/> will apply timeouts to this operation based on the value configured in <see cref="JSRuntime.DefaultAsyncTimeout"/>. To dispatch a call with a different, or no timeout,
+        /// consider using <see cref="InvokeAsync{TValue}(string, CancellationToken, object[])" />.
+        /// </para>
+        /// </summary>
+        /// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
+        /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
+        /// <param name="args">JSON-serializable arguments.</param>
+        /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
+        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object[] args)
+        {
+            ThrowIfDisposed();
+
+            return _jsRuntime.InvokeAsync<TValue>(Id, identifier, args);
+        }
+
+        /// <summary>
+        /// Invokes the specified JavaScript function asynchronously.
+        /// </summary>
+        /// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
+        /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>someScope.someFunction</c> on the target instance.</param>
+        /// <param name="cancellationToken">
+        /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts
+        /// (<see cref="JSRuntime.DefaultAsyncTimeout"/>) from being applied.
+        /// </param>
+        /// <param name="args">JSON-serializable arguments.</param>
+        /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
+        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, params object[] args)
+        {
+            ThrowIfDisposed();
+
+            return _jsRuntime.InvokeAsync<TValue>(Id, identifier, cancellationToken, args);
+        }
+
+        /// <summary>
+        /// Disposes the <see cref="JSObjectReference"/>, freeing its resources and disabling it from further use.
+        /// </summary>
+        /// <returns>A <see cref="ValueTask"/> representing the completion of the operation.</returns>
+        public async ValueTask DisposeAsync()
+        {
+            if (!_disposed)
+            {
+                _disposed = true;
+
+                await _jsRuntime.InvokeVoidAsync("DotNet.jsCallDispatcher.disposeJSObjectReferenceById", Id);
+            }
+        }
+
+        /// <summary>
+        /// Throws an exception if this instance has been disposed.
+        /// </summary>
+        protected void ThrowIfDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+    }
+}

+ 49 - 14
src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs

@@ -37,6 +37,7 @@ namespace Microsoft.JSInterop
                 Converters =
                 {
                     new DotNetObjectReferenceJsonConverterFactory(this),
+                    new JSObjectReferenceJsonConverter<JSObjectReference>(id => new JSObjectReference(this, id)),
                 }
             };
         }
@@ -51,6 +52,17 @@ namespace Microsoft.JSInterop
         /// </summary>
         protected TimeSpan? DefaultAsyncTimeout { get; set; }
 
+        /// <summary>
+        /// Creates a <see cref="JSCallResultType"/> from the given generic type.
+        /// </summary>
+        /// <typeparam name="TResult">
+        /// The type of the result of the relevant JS interop call.
+        /// </typeparam>
+        protected static JSCallResultType ResultTypeFromGeneric<TResult>()
+            => typeof(TResult) == typeof(JSObjectReference) || typeof(TResult) == typeof(JSInProcessObjectReference) ?
+                JSCallResultType.JSObjectReference :
+                JSCallResultType.Default;
+
         /// <summary>
         /// Invokes the specified JavaScript function asynchronously.
         /// <para>
@@ -62,17 +74,8 @@ namespace Microsoft.JSInterop
         /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
         /// <param name="args">JSON-serializable arguments.</param>
         /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
-        public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
-        {
-            if (DefaultAsyncTimeout.HasValue)
-            {
-                using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value);
-                // We need to await here due to the using
-                return await InvokeAsync<TValue>(identifier, cts.Token, args);
-            }
-
-            return await InvokeAsync<TValue>(identifier, CancellationToken.None, args);
-        }
+        public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
+            => InvokeAsync<TValue>(0, identifier, args);
 
         /// <summary>
         /// Invokes the specified JavaScript function asynchronously.
@@ -81,11 +84,30 @@ namespace Microsoft.JSInterop
         /// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
         /// <param name="cancellationToken">
         /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts
-        /// (<see cref="JSRuntime.DefaultAsyncTimeout"/>) from being applied.
+        /// (<see cref="DefaultAsyncTimeout"/>) from being applied.
         /// </param>
         /// <param name="args">JSON-serializable arguments.</param>
         /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
         public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
+            => InvokeAsync<TValue>(0, identifier, cancellationToken, args);
+
+        internal async ValueTask<TValue> InvokeAsync<TValue>(long targetInstanceId, string identifier, object?[]? args)
+        {
+            if (DefaultAsyncTimeout.HasValue)
+            {
+                using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value);
+                // We need to await here due to the using
+                return await InvokeAsync<TValue>(targetInstanceId, identifier, cts.Token, args);
+            }
+
+            return await InvokeAsync<TValue>(targetInstanceId, identifier, CancellationToken.None, args);
+        }
+
+        internal ValueTask<TValue> InvokeAsync<TValue>(
+            long targetInstanceId,
+            string identifier,
+            CancellationToken cancellationToken,
+            object?[]? args)
         {
             var taskId = Interlocked.Increment(ref _nextPendingTaskId);
             var tcs = new TaskCompletionSource<TValue>(TaskContinuationOptions.RunContinuationsAsynchronously);
@@ -112,7 +134,9 @@ namespace Microsoft.JSInterop
                 var argsJson = args?.Any() == true ?
                     JsonSerializer.Serialize(args, JsonSerializerOptions) :
                     null;
-                BeginInvokeJS(taskId, identifier, argsJson);
+                var resultType = ResultTypeFromGeneric<TValue>();
+
+                BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId);
 
                 return new ValueTask<TValue>(tcs.Task);
             }
@@ -138,7 +162,18 @@ namespace Microsoft.JSInterop
         /// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
         /// <param name="identifier">The identifier for the function to invoke.</param>
         /// <param name="argsJson">A JSON representation of the arguments.</param>
-        protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson);
+        protected virtual void BeginInvokeJS(long taskId, string identifier, string? argsJson)
+            => BeginInvokeJS(taskId, identifier, argsJson, JSCallResultType.Default, 0);
+
+        /// <summary>
+        /// Begins an asynchronous function invocation.
+        /// </summary>
+        /// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
+        /// <param name="identifier">The identifier for the function to invoke.</param>
+        /// <param name="argsJson">A JSON representation of the arguments.</param>
+        /// <param name="resultType">The type of result expected from the invocation.</param>
+        /// <param name="targetInstanceId">The instance ID of the target JS object.</param>
+        protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId);
 
         /// <summary>
         /// Completes an async JS interop call from JavaScript to .NET

+ 2 - 2
src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs

@@ -885,7 +885,7 @@ namespace Microsoft.JSInterop.Infrastructure
             public string LastCompletionCallId { get; private set; }
             public DotNetInvocationResult LastCompletionResult { get; private set; }
 
-            protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
+            protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
             {
                 LastInvocationAsyncHandle = asyncHandle;
                 LastInvocationIdentifier = identifier;
@@ -894,7 +894,7 @@ namespace Microsoft.JSInterop.Infrastructure
                 _nextInvocationTcs = new TaskCompletionSource<object>();
             }
 
-            protected override string InvokeJS(string identifier, string argsJson)
+            protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
             {
                 LastInvocationAsyncHandle = default;
                 LastInvocationIdentifier = identifier;

+ 85 - 0
src/JSInterop/Microsoft.JSInterop/test/Infrastructure/JSObjectReferenceJsonConverterTest.cs

@@ -0,0 +1,85 @@
+// 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.Text.Json;
+using Xunit;
+
+namespace Microsoft.JSInterop.Infrastructure
+{
+    public class JSObjectReferenceJsonConverterTest
+    {
+        private readonly JSRuntime JSRuntime = new TestJSRuntime();
+        private JsonSerializerOptions JsonSerializerOptions => JSRuntime.JsonSerializerOptions;
+
+        [Fact]
+        public void Read_Throws_IfJsonIsMissingJSObjectIdProperty()
+        {
+            // Arrange
+            var json = "{}";
+
+            // Act & Assert
+            var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
+            Assert.Equal("Required property __jsObjectId not found.", ex.Message);
+        }
+
+        [Fact]
+        public void Read_Throws_IfJsonContainsUnknownContent()
+        {
+            // Arrange
+            var json = "{\"foo\":2}";
+
+            // Act & Assert
+            var ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
+            Assert.Equal("Unexcepted JSON property foo.", ex.Message);
+        }
+
+        [Fact]
+        public void Read_Throws_IfJsonIsIncomplete()
+        {
+            // Arrange
+            var json = $"{{\"__jsObjectId\":5";
+
+            // Act & Assert
+            var ex = Record.Exception(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
+            Assert.IsAssignableFrom<JsonException>(ex);
+        }
+
+        [Fact]
+        public void Read_Throws_IfJSObjectIdAppearsMultipleTimes()
+        {
+            // Arrange
+            var json = $"{{\"__jsObjectId\":3,\"__jsObjectId\":7}}";
+
+            // Act & Assert
+            var ex = Record.Exception(() => JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions));
+            Assert.IsAssignableFrom<JsonException>(ex);
+        }
+
+        [Fact]
+        public void Read_ReadsJson()
+        {
+            // Arrange
+            var expectedId = 3;
+            var json = $"{{\"__jsObjectId\":{expectedId}}}";
+
+            // Act
+            var deserialized = JsonSerializer.Deserialize<JSObjectReference>(json, JsonSerializerOptions)!;
+
+            // Assert
+            Assert.Equal(expectedId, deserialized?.Id);
+        }
+
+        [Fact]
+        public void Write_WritesValidJson()
+        {
+            // Arrange
+            var jsObjectRef = new JSObjectReference(JSRuntime, 7);
+
+            // Act
+            var json = JsonSerializer.Serialize(jsObjectRef, JsonSerializerOptions);
+
+            // Assert
+            Assert.Equal($"{{\"__jsObjectId\":{jsObjectRef.Id}}}", json);
+        }
+    }
+}

+ 2 - 2
src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs

@@ -99,7 +99,7 @@ namespace Microsoft.JSInterop
 
             public string? NextResultJson { get; set; }
 
-            protected override string? InvokeJS(string identifier, string? argsJson)
+            protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
             {
                 InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson });
                 return NextResultJson;
@@ -111,7 +111,7 @@ namespace Microsoft.JSInterop
                 public string? ArgsJson { get; set; }
             }
 
-            protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson)
+            protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
                 => throw new NotImplementedException("This test only covers sync calls");
 
             protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)

+ 105 - 0
src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs

@@ -0,0 +1,105 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.JSInterop.Infrastructure;
+using Xunit;
+
+namespace Microsoft.JSInterop.Tests
+{
+    public class JSObjectReferenceTest
+    {
+        [Fact]
+        public void JSObjectReference_InvokeAsync_CallsUnderlyingJSRuntimeInvokeAsync()
+        {
+            // Arrange
+            var jsRuntime = new TestJSRuntime();
+            var jsObject = new JSObjectReference(jsRuntime, 0);
+
+            // Act
+            _ = jsObject.InvokeAsync<object>("test", "arg1", "arg2");
+
+            // Assert
+            Assert.Equal(1, jsRuntime.BeginInvokeJSInvocationCount);
+        }
+
+        [Fact]
+        public void JSInProcessObjectReference_Invoke_CallsUnderlyingJSRuntimeInvoke()
+        {
+            // Arrange
+            var jsRuntime = new TestJSInProcessRuntime();
+            var jsObject = new JSInProcessObjectReference(jsRuntime, 0);
+
+            // Act
+            jsObject.Invoke<object>("test", "arg1", "arg2");
+
+            // Assert
+            Assert.Equal(1, jsRuntime.InvokeJSInvocationCount);
+        }
+
+        [Fact]
+        public async Task JSObjectReference_Dispose_DisallowsFurtherInteropCalls()
+        {
+            // Arrange
+            var jsRuntime = new TestJSRuntime();
+            var jsObject = new JSObjectReference(jsRuntime, 0);
+
+            // Act
+            _ = jsObject.DisposeAsync();
+
+            // Assert
+            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await jsObject.InvokeAsync<object>("test", "arg1", "arg2"));
+            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await jsObject.InvokeAsync<object>("test", CancellationToken.None, "arg1", "arg2"));
+        }
+
+        [Fact]
+        public void JSInProcessObjectReference_Dispose_DisallowsFurtherInteropCalls()
+        {
+            // Arrange
+            var jsRuntime = new TestJSInProcessRuntime();
+            var jsObject = new JSInProcessObjectReference(jsRuntime, 0);
+
+            // Act
+            _ = jsObject.DisposeAsync();
+
+            // Assert
+            Assert.Throws<ObjectDisposedException>(() => jsObject.Invoke<object>("test", "arg1", "arg2"));
+        }
+
+        class TestJSRuntime : JSRuntime
+        {
+            public int BeginInvokeJSInvocationCount { get; private set; }
+
+            protected override void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
+            {
+                BeginInvokeJSInvocationCount++;
+            }
+
+            protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
+            {
+            }
+        }
+
+        class TestJSInProcessRuntime : JSInProcessRuntime
+        {
+            public int InvokeJSInvocationCount { get; private set; }
+
+            protected override void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
+            {
+            }
+
+            protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
+            {
+                InvokeJSInvocationCount++;
+
+                return null;
+            }
+
+            protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
+            {
+            }
+        }
+    }
+}

+ 1 - 1
src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs

@@ -377,7 +377,7 @@ namespace Microsoft.JSInterop
                 });
             }
 
-            protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson)
+            protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
             {
                 BeginInvokeCalls.Add(new BeginInvokeAsyncArgs
                 {

+ 1 - 1
src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs

@@ -8,7 +8,7 @@ namespace Microsoft.JSInterop
 {
     internal class TestJSRuntime : JSRuntime
     {
-        protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson)
+        protected override void BeginInvokeJS(long asyncHandle, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
         {
             throw new NotImplementedException();
         }

+ 9 - 1
src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -42,6 +42,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             }
             catch (InvalidDataException ex)
             {
+                // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+                // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+                throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+            }
+            catch (IOException ex)
+            {
+                // ReadFormAsync can throw IOException if the client disconnects.
+                // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
                 throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
             }
 

+ 8 - 0
src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs

@@ -44,6 +44,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             }
             catch (InvalidDataException ex)
             {
+                // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+                // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+                throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+            }
+            catch (IOException ex)
+            {
+                // ReadFormAsync can throw IOException if the client disconnects.
+                // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
                 throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
             }
 

+ 20 - 1
src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs

@@ -3,7 +3,10 @@
 
 using System;
 using System.Globalization;
+using System.IO;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Core;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding
 {
@@ -34,7 +37,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
         {
             var request = context.ActionContext.HttpContext.Request;
 
-            var formCollection = await request.ReadFormAsync();
+            IFormCollection formCollection;
+            try
+            {
+                formCollection = await request.ReadFormAsync();
+            }
+            catch (InvalidDataException ex)
+            {
+                // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+                // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+                throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+            }
+            catch (IOException ex)
+            {
+                // ReadFormAsync can throw IOException if the client disconnects.
+                // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+                throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+            }
 
             var valueProvider = new JQueryFormValueProvider(
                 BindingSource.Form,

+ 22 - 0
src/Mvc/Mvc.Core/test/ModelBinding/CompositeValueProviderTest.cs

@@ -5,7 +5,10 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.Primitives;
 using Moq;
 using Xunit;
@@ -45,6 +48,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             return new CompositeValueProvider() { emptyValueProvider, valueProvider };
         }
 
+        [Fact]
+        public async Task TryCreateAsync_AddsModelStateError_WhenValueProviderFactoryThrowsValueProviderException()
+        {
+            // Arrange
+            var factory = new Mock<IValueProviderFactory>();
+            factory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>())).ThrowsAsync(new ValueProviderException("Some error"));
+            var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor(), new ModelStateDictionary());
+
+            // Act
+            var (success, result) = await CompositeValueProvider.TryCreateAsync(actionContext, new[] { factory.Object });
+
+            // Assert
+            Assert.False(success);
+            var modelState = actionContext.ModelState;
+            Assert.False(modelState.IsValid);
+            var entry = Assert.Single(modelState);
+            Assert.Empty(entry.Key);
+        }
+
         [Fact]
         public void GetKeysFromPrefixAsync_ReturnsResultFromFirstValueProviderThatReturnsValues()
         {

+ 57 - 1
src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs

@@ -1,13 +1,16 @@
-// 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;
 using System.Collections.Generic;
 using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.Primitives;
+using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -60,6 +63,59 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
                 v => Assert.IsType<FormFileValueProvider>(v));
         }
 
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidDataException()
+        {
+            // Arrange
+            var exception = new InvalidDataException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new FormFileValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<ValueProviderException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidOperationException()
+        {
+            // Arrange
+            var exception = new IOException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new FormFileValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<ValueProviderException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsOriginalException_IfReadingFormThrows()
+        {
+            // Arrange
+            var exception = new TimeZoneNotFoundException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new FormFileValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex);
+        }
+
+        private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
+        {
+            var context = new Mock<HttpContext>();
+            context.Setup(c => c.Request.ContentType).Returns("application/x-www-form-urlencoded");
+            context.Setup(c => c.Request.HasFormContentType).Returns(true);
+            context.Setup(c => c.Request.ReadFormAsync(It.IsAny<CancellationToken>())).ThrowsAsync(exception);
+            var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor());
+            var valueProviderContext = new ValueProviderFactoryContext(actionContext);
+            return valueProviderContext;
+        }
+
         private static ValueProviderFactoryContext CreateContext(string contentType)
         {
             var context = new DefaultHttpContext();

+ 57 - 0
src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderFactoryTest.cs

@@ -1,13 +1,17 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.Primitives;
+using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
@@ -47,6 +51,59 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
             Assert.Equal(CultureInfo.CurrentCulture, valueProvider.Culture);
         }
 
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidDataException()
+        {
+            // Arrange
+            var exception = new InvalidDataException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new FormValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<ValueProviderException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidOperationException()
+        {
+            // Arrange
+            var exception = new IOException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new FormValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<ValueProviderException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsOriginalException_IfReadingFormThrows()
+        {
+            // Arrange
+            var exception = new TimeZoneNotFoundException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new FormValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex);
+        }
+
+        private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
+        {
+            var context = new Mock<HttpContext>();
+            context.Setup(c => c.Request.ContentType).Returns("application/x-www-form-urlencoded");
+            context.Setup(c => c.Request.HasFormContentType).Returns(true);
+            context.Setup(c => c.Request.ReadFormAsync(It.IsAny<CancellationToken>())).ThrowsAsync(exception);
+            var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor());
+            var valueProviderContext = new ValueProviderFactoryContext(actionContext);
+            return valueProviderContext;
+        }
+
         private static ValueProviderFactoryContext CreateContext(string contentType)
         {
             var context = new DefaultHttpContext();

+ 57 - 0
src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderFactoryTest.cs

@@ -1,13 +1,17 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.Primitives;
+using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
@@ -132,6 +136,59 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
             Assert.Equal(CultureInfo.CurrentCulture, jqueryFormValueProvider.Culture);
         }
 
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidDataException()
+        {
+            // Arrange
+            var exception = new InvalidDataException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new JQueryFormValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<ValueProviderException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidOperationException()
+        {
+            // Arrange
+            var exception = new IOException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new JQueryFormValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<ValueProviderException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex.InnerException);
+        }
+
+        [Fact]
+        public async Task GetValueProviderAsync_ThrowsOriginalException_IfReadingFormThrows()
+        {
+            // Arrange
+            var exception = new TimeZoneNotFoundException();
+            var valueProviderContext = CreateThrowingContext(exception);
+
+            var factory = new JQueryFormValueProviderFactory();
+
+            // Act & Assert
+            var ex = await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => factory.CreateValueProviderAsync(valueProviderContext));
+            Assert.Same(exception, ex);
+        }
+
+        private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
+        {
+            var context = new Mock<HttpContext>();
+            context.Setup(c => c.Request.ContentType).Returns("application/x-www-form-urlencoded");
+            context.Setup(c => c.Request.HasFormContentType).Returns(true);
+            context.Setup(c => c.Request.ReadFormAsync(It.IsAny<CancellationToken>())).ThrowsAsync(exception);
+            var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor());
+            var valueProviderContext = new ValueProviderFactoryContext(actionContext);
+            return valueProviderContext;
+        }
+
         private static ValueProviderFactoryContext CreateContext(string contentType, Dictionary<string, StringValues> formValues)
         {
             var context = new DefaultHttpContext();

+ 3 - 1
src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -66,6 +66,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
                     case RenderMode.Server:
                     case RenderMode.ServerPrerendered:
                     case RenderMode.Static:
+                    case RenderMode.WebAssembly:
+                    case RenderMode.WebAssemblyPrerendered:
                         _renderMode = value;
                         break;
 

+ 75 - 0
src/Mvc/Mvc.ViewFeatures/src/ClientComponentSerializer.cs

@@ -0,0 +1,75 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.AspNetCore.Components;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
+{
+    // See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
+    internal class WebAssemblyComponentSerializer
+    {
+        public WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered)
+        {
+            var assembly = type.Assembly.GetName().Name;
+            var typeFullName = type.FullName;
+            var (definitions, values) = ComponentParameter.FromParameterView(parameters);
+
+            // We need to serialize and Base64 encode parameters separately since they can contain arbitrary data that might
+            // cause the HTML comment to be invalid (like if you serialize a string that contains two consecutive dashes "--").
+            var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
+            var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
+
+            return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues) :
+                WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues);
+        }
+
+        internal IEnumerable<string> GetPreamble(WebAssemblyComponentMarker record)
+        {
+            var serializedStartRecord = JsonSerializer.Serialize(
+                record,
+                WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
+
+            if (record.PrerenderId != null)
+            {
+                return PrerenderedStart(serializedStartRecord);
+            }
+            else
+            {
+                return NonPrerenderedSequence(serializedStartRecord);
+            }
+
+            static IEnumerable<string> PrerenderedStart(string startRecord)
+            {
+                yield return "<!--Blazor:";
+                yield return startRecord;
+                yield return "-->";
+            }
+
+            static IEnumerable<string> NonPrerenderedSequence(string record)
+            {
+                yield return "<!--Blazor:";
+                yield return record;
+                yield return "-->";
+            }
+        }
+
+        internal IEnumerable<string> GetEpilogue(WebAssemblyComponentMarker record)
+        {
+            var serializedStartRecord = JsonSerializer.Serialize(
+                record.GetEndRecord(),
+                WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
+
+            return PrerenderEnd(serializedStartRecord);
+
+            static IEnumerable<string> PrerenderEnd(string endRecord)
+            {
+                yield return "<!--Blazor:";
+                yield return endRecord;
+                yield return "-->";
+            }
+        }
+    }
+}

+ 3 - 0
src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs

@@ -177,6 +177,9 @@ namespace Microsoft.Extensions.DependencyInjection
             // Component services for Blazor server-side interop
             services.TryAddSingleton<ServerComponentSerializer>();
 
+            // Component services for Blazor webassembly interop
+            services.TryAddSingleton<WebAssemblyComponentSerializer>();
+
             //
             // View Components
             //

+ 3 - 1
src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Description>
@@ -31,7 +31,9 @@
   </ItemGroup>
 
   <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" />
     <Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" />
+    <Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" />
     <Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" />
     <Compile Include="$(SharedSourceRoot)Components\ServerComponent.cs" />
     <Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />

+ 2 - 0
src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt

@@ -77,6 +77,8 @@ Microsoft.AspNetCore.Mvc.Rendering.RenderMode
 Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Server = 2 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
 Microsoft.AspNetCore.Mvc.Rendering.RenderMode.ServerPrerendered = 3 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
 Microsoft.AspNetCore.Mvc.Rendering.RenderMode.Static = 1 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
+Microsoft.AspNetCore.Mvc.Rendering.RenderMode.WebAssembly = 4 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
+Microsoft.AspNetCore.Mvc.Rendering.RenderMode.WebAssemblyPrerendered = 5 -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
 Microsoft.AspNetCore.Mvc.Rendering.SelectList
 Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup
 Microsoft.AspNetCore.Mvc.Rendering.SelectListGroup.Disabled.get -> bool

+ 32 - 3
src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -15,13 +15,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
         private static readonly object ComponentSequenceKey = new object();
         private readonly StaticComponentRenderer _staticComponentRenderer;
         private readonly ServerComponentSerializer _serverComponentSerializer;
+        private readonly WebAssemblyComponentSerializer _WebAssemblyComponentSerializer;
 
         public ComponentRenderer(
             StaticComponentRenderer staticComponentRenderer,
-            ServerComponentSerializer serverComponentSerializer)
+            ServerComponentSerializer serverComponentSerializer,
+            WebAssemblyComponentSerializer WebAssemblyComponentSerializer)
         {
             _staticComponentRenderer = staticComponentRenderer;
             _serverComponentSerializer = serverComponentSerializer;
+            _WebAssemblyComponentSerializer = WebAssemblyComponentSerializer;
         }
 
         public async Task<IHtmlContent> RenderComponentAsync(
@@ -55,6 +58,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
                 RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
                 RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
                 RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
+                RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(context, componentType, parameterView),
+                RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(context, componentType, parameterView),
                 _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
             };
         }
@@ -99,12 +104,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
                 _serverComponentSerializer.GetEpilogue(currentInvocation));
         }
 
+        private async Task<IHtmlContent> PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
+        {
+            var currentInvocation = _WebAssemblyComponentSerializer.SerializeInvocation(
+                type,
+                parametersCollection,
+                prerendered: true);
+
+            var result = await _staticComponentRenderer.PrerenderComponentAsync(
+                parametersCollection,
+                context,
+                type);
+
+            return new ComponentHtmlContent(
+                _WebAssemblyComponentSerializer.GetPreamble(currentInvocation),
+                result,
+                _WebAssemblyComponentSerializer.GetEpilogue(currentInvocation));
+        }
+
         private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
         {
-            var serviceProvider = context.RequestServices;
             var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
 
             return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
         }
+
+        private IHtmlContent NonPrerenderedWebAssemblyComponent(HttpContext context, Type type, ParameterView parametersCollection)
+        {
+            var currentInvocation = _WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false);
+
+            return new ComponentHtmlContent(_WebAssemblyComponentSerializer.GetPreamble(currentInvocation));
+        }
     }
 }

Some files were not shown because too many files changed in this diff