From c9af79a3e36f3ad72d64f5e2d0e819d877e2243b Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 19 Apr 2024 09:34:38 -0700 Subject: [PATCH] [Blazor] Use JSON source generator during WebAssembly startup (#54956) --- .../JsonConverterFactoryTypeInfoResolver.cs | 45 +++++++++++++++ .../IInternalWebJSInProcessRuntime.cs | 20 +++++++ ...Microsoft.AspNetCore.Components.Web.csproj | 1 + .../Web/src/PublicAPI.Unshipped.txt | 2 + src/Components/Web/src/WebRenderer.cs | 55 +++++++++++++++++-- ...icationServiceCollectionExtensionsTests.cs | 32 +++++------ .../src/Hosting/WebAssemblyHost.cs | 2 +- .../src/Hosting/WebAssemblyHostBuilder.cs | 48 ++++++++-------- .../WebAssemblyJsonSerializerContext.cs | 21 +++++++ ...bAssemblyComponentParameterDeserializer.cs | 6 +- .../src/Rendering/WebAssemblyRenderer.cs | 14 ++--- .../Services/DefaultWebAssemblyJSRuntime.cs | 14 +++-- .../src/Services/IInternalJSImportMethods.cs | 4 ++ .../src/Services/InternalJSImportMethods.cs | 12 ++++ .../Hosting/WebAssemblyHostBuilderTest.cs | 24 ++++---- .../test/Hosting/WebAssemblyHostTest.cs | 8 +-- .../test/TestInternalJSImportMethods.cs | 4 ++ .../src/PublicAPI.Unshipped.txt | 2 +- .../PrerenderComponentApplicationStore.cs | 14 +++-- 19 files changed, 238 insertions(+), 90 deletions(-) create mode 100644 src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs create mode 100644 src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs create mode 100644 src/Components/WebAssembly/WebAssembly/src/Infrastructure/WebAssemblyJsonSerializerContext.cs diff --git a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs new file mode 100644 index 000000000000..32d9973a0fd8 --- /dev/null +++ b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.AspNetCore.Components; + +// For custom converters that don't rely on serializing an object graph, +// we can resolve the incoming type's JsonTypeInfo directly from the converter. +// This skips extra work to collect metadata for the type that won't be used. +internal sealed class JsonConverterFactoryTypeInfoResolver : IJsonTypeInfoResolver +{ + public static readonly JsonConverterFactoryTypeInfoResolver Instance = new(); + + private JsonConverterFactoryTypeInfoResolver() + { + } + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (type != typeof(T)) + { + return null; + } + + foreach (var converter in options.Converters) + { + if (converter is not JsonConverterFactory factory || !factory.CanConvert(type)) + { + continue; + } + + if (factory.CreateConverter(type, options) is not { } converterToUse) + { + continue; + } + + return JsonMetadataServices.CreateValueInfo(options, converterToUse); + } + + return null; + } +} diff --git a/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs b/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs new file mode 100644 index 000000000000..dc3383f45ced --- /dev/null +++ b/src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.JSInterop; + +namespace Microsoft.AspNetCore.Components.Web.Internal; + +/// +/// For internal framework use only. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IInternalWebJSInProcessRuntime +{ + /// + /// For internal framework use only. + /// + string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId); +} diff --git a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj index b3ec9165085a..d312f15ace7a 100644 --- a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj +++ b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 4befff3c6426..1a79e385d309 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime +Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime.InvokeJS(string! identifier, string? argsJson, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> string! Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.get -> bool Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.set -> void diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 840369c559c7..becd887453d0 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -3,8 +3,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; +using Microsoft.AspNetCore.Components.Web.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; @@ -41,12 +43,7 @@ public WebRenderer( // Supply a DotNetObjectReference to JS that it can use to call us back for events etc. jsComponentInterop.AttachToRenderer(this); var jsRuntime = serviceProvider.GetRequiredService(); - jsRuntime.InvokeVoidAsync( - "Blazor._internal.attachWebRendererInterop", - _rendererId, - _interopMethodsReference, - jsComponentInterop.Configuration.JSComponentParametersByIdentifier, - jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); + AttachWebRendererInterop(jsRuntime, jsonOptions, jsComponentInterop); } /// @@ -103,6 +100,44 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + private void AttachWebRendererInterop(IJSRuntime jsRuntime, JsonSerializerOptions jsonOptions, JSComponentInterop jsComponentInterop) + { + const string JSMethodIdentifier = "Blazor._internal.attachWebRendererInterop"; + + // These arguments should be kept in sync with WebRendererSerializerContext + object[] args = [ + _rendererId, + _interopMethodsReference, + jsComponentInterop.Configuration.JSComponentParametersByIdentifier, + jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer, + ]; + + if (jsRuntime is IInternalWebJSInProcessRuntime inProcessRuntime) + { + // Fast path for WebAssembly: Rather than using the JSRuntime to serialize + // parameters, we utilize the source-generated WebRendererSerializerContext + // for a faster JsonTypeInfo resolution. + + // We resolve a JsonTypeInfo for DotNetObjectReference from + // the JS runtime's JsonConverters. This is because adding DotNetObjectReference as + // a supported type in the JsonSerializerContext generates unnecessary code to produce + // JsonTypeInfo for all the types referenced by both DotNetObjectReference and its + // generic type argument. + + var newJsonOptions = new JsonSerializerOptions(jsonOptions); + newJsonOptions.TypeInfoResolverChain.Clear(); + newJsonOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default); + newJsonOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver>.Instance); + var argsJson = JsonSerializer.Serialize(args, newJsonOptions); + inProcessRuntime.InvokeJS(JSMethodIdentifier, argsJson, JSCallResultType.JSVoidResult, 0); + } + else + { + jsRuntime.InvokeVoidAsync(JSMethodIdentifier, args).Preserve(); + } + } + /// /// A collection of JS invokable methods that the JS-side code can use when it needs to /// make calls in the context of a particular renderer. This object is never exposed to @@ -145,3 +180,11 @@ public void RemoveRootComponent(int componentId) => _jsComponentInterop.RemoveRootComponent(componentId); } } + +// This should be kept in sync with the argument types in the call to +// 'Blazor._internal.attachWebRendererInterop' +[JsonSerializable(typeof(object[]))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary>))] +internal sealed partial class WebRendererSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs index e20acfac4fab..25776f22f8ea 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs @@ -12,12 +12,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; public class WebAssemblyAuthenticationServiceCollectionExtensionsTests { - private static readonly JsonSerializerOptions JsonOptions = new(); - [Fact] public void CanResolve_AccessTokenProvider() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -27,7 +25,7 @@ public void CanResolve_AccessTokenProvider() [Fact] public void CanResolve_IRemoteAuthenticationService() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -37,7 +35,7 @@ public void CanResolve_IRemoteAuthenticationService() [Fact] public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -71,7 +69,7 @@ public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied() [Fact] public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; builder.Services.AddApiAuthorization(options => { @@ -98,7 +96,7 @@ public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce() [Fact] public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; builder.Services.AddApiAuthorization(options => calls++); @@ -124,7 +122,7 @@ public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration() [Fact] public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -147,7 +145,7 @@ public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfigurati [Fact] public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; builder.Services.AddApiAuthorization(options => calls++); @@ -173,7 +171,7 @@ public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfigurat [Fact] public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -196,7 +194,7 @@ public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpC [Fact] public void ApiAuthorizationOptions_DefaultsCanBeOverriden() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddApiAuthorization(options => { options.AuthenticationPaths.LogInPath = "a"; @@ -247,7 +245,7 @@ public void ApiAuthorizationOptions_DefaultsCanBeOverriden() [Fact] public void OidcOptions_ConfigurationDefaultsGetApplied() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.Replace(ServiceDescriptor.Singleton()); builder.Services.AddOidcAuthentication(options => { }); var host = builder.Build(); @@ -286,7 +284,7 @@ public void OidcOptions_ConfigurationDefaultsGetApplied() [Fact] public void OidcOptions_DefaultsCanBeOverriden() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddOidcAuthentication(options => { options.AuthenticationPaths.LogInPath = "a"; @@ -348,7 +346,7 @@ public void OidcOptions_DefaultsCanBeOverriden() [Fact] public void AddOidc_ConfigurationGetsCalledOnce() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; builder.Services.AddOidcAuthentication(options => calls++); @@ -365,7 +363,7 @@ public void AddOidc_ConfigurationGetsCalledOnce() [Fact] public void AddOidc_CustomState_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; builder.Services.AddOidcAuthentication(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture)); @@ -387,7 +385,7 @@ public void AddOidc_CustomState_SetsUpConfiguration() [Fact] public void AddOidc_CustomStateAndAccount_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; builder.Services.AddOidcAuthentication(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture)); @@ -409,7 +407,7 @@ public void AddOidc_CustomStateAndAccount_SetsUpConfiguration() [Fact] public void OidcProviderOptionsAndDependencies_NotResolvedFromRootScope() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var calls = 0; diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 7823857eedd3..01e51c2920f0 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -227,6 +227,6 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo operation.Descriptor!.Parameters)); } - WebAssemblyRenderer.NotifyEndUpdateRootComponents(operationBatch.BatchId); + renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 95deabeaa789..47f7305e8b49 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; -using System.Text.Json; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.RenderTree; @@ -27,7 +26,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; /// public sealed class WebAssemblyHostBuilder { - private readonly JsonSerializerOptions _jsonOptions; + private readonly IInternalJSImportMethods _jsMethods; private Func _createServiceProvider; private RootComponentTypeCache? _rootComponentCache; private string? _persistedState; @@ -48,9 +47,7 @@ public static WebAssemblyHostBuilder CreateDefault(string[]? args = default) { // We don't use the args for anything right now, but we want to accept them // here so that it shows up this way in the project templates. - var builder = new WebAssemblyHostBuilder( - InternalJSImportMethods.Instance, - DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions()); + var builder = new WebAssemblyHostBuilder(InternalJSImportMethods.Instance); WebAssemblyCultureProvider.Initialize(); @@ -64,14 +61,12 @@ public static WebAssemblyHostBuilder CreateDefault(string[]? args = default) /// /// Creates an instance of with the minimal configuration. /// - internal WebAssemblyHostBuilder( - IInternalJSImportMethods jsMethods, - JsonSerializerOptions jsonOptions) + internal WebAssemblyHostBuilder(IInternalJSImportMethods jsMethods) { // Private right now because we don't have much reason to expose it. This can be exposed // in the future if we want to give people a choice between CreateDefault and something // less opinionated. - _jsonOptions = jsonOptions; + _jsMethods = jsMethods; Configuration = new WebAssemblyHostConfiguration(); RootComponents = new RootComponentMappingCollection(); Services = new ServiceCollection(); @@ -86,12 +81,12 @@ internal WebAssemblyHostBuilder( InitializeWebAssemblyRenderer(); // Retrieve required attributes from JSRuntimeInvoker - InitializeNavigationManager(jsMethods); - InitializeRegisteredRootComponents(jsMethods); - InitializePersistedState(jsMethods); + InitializeNavigationManager(); + InitializeRegisteredRootComponents(); + InitializePersistedState(); InitializeDefaultServices(); - var hostEnvironment = InitializeEnvironment(jsMethods); + var hostEnvironment = InitializeEnvironment(); HostEnvironment = hostEnvironment; _createServiceProvider = () => @@ -117,9 +112,9 @@ private static void InitializeRoutingAppContextSwitch(Assembly assembly) } [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")] - private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods) + private void InitializeRegisteredRootComponents() { - var componentsCount = jsMethods.RegisteredComponents_GetRegisteredComponentsCount(); + var componentsCount = _jsMethods.RegisteredComponents_GetRegisteredComponentsCount(); if (componentsCount == 0) { return; @@ -128,10 +123,10 @@ private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMetho var registeredComponents = new ComponentMarker[componentsCount]; for (var i = 0; i < componentsCount; i++) { - var assembly = jsMethods.RegisteredComponents_GetAssembly(i); - var typeName = jsMethods.RegisteredComponents_GetTypeName(i); - var serializedParameterDefinitions = jsMethods.RegisteredComponents_GetParameterDefinitions(i); - var serializedParameterValues = jsMethods.RegisteredComponents_GetParameterValues(i); + var assembly = _jsMethods.RegisteredComponents_GetAssembly(i); + var typeName = _jsMethods.RegisteredComponents_GetTypeName(i); + var serializedParameterDefinitions = _jsMethods.RegisteredComponents_GetParameterDefinitions(i); + var serializedParameterValues = _jsMethods.RegisteredComponents_GetParameterValues(i); registeredComponents[i] = ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, false, null); registeredComponents[i].WriteWebAssemblyData( assembly, @@ -161,22 +156,22 @@ private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMetho } } - private void InitializePersistedState(IInternalJSImportMethods jsMethods) + private void InitializePersistedState() { - _persistedState = jsMethods.GetPersistedState(); + _persistedState = _jsMethods.GetPersistedState(); } - private static void InitializeNavigationManager(IInternalJSImportMethods jsMethods) + private void InitializeNavigationManager() { - var baseUri = jsMethods.NavigationManager_GetBaseUri(); - var uri = jsMethods.NavigationManager_GetLocationHref(); + var baseUri = _jsMethods.NavigationManager_GetBaseUri(); + var uri = _jsMethods.NavigationManager_GetLocationHref(); WebAssemblyNavigationManager.Instance = new WebAssemblyNavigationManager(baseUri, uri); } - private WebAssemblyHostEnvironment InitializeEnvironment(IInternalJSImportMethods jsMethods) + private WebAssemblyHostEnvironment InitializeEnvironment() { - var applicationEnvironment = jsMethods.GetApplicationEnvironment(); + var applicationEnvironment = _jsMethods.GetApplicationEnvironment(); var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment, WebAssemblyNavigationManager.Instance.BaseUri); Services.AddSingleton(hostEnvironment); @@ -305,6 +300,7 @@ internal void InitializeDefaultServices() Services.AddSingleton(WebAssemblyNavigationManager.Instance); Services.AddSingleton(WebAssemblyNavigationInterception.Instance); Services.AddSingleton(WebAssemblyScrollToLocationHash.Instance); + Services.AddSingleton(_jsMethods); Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance)); Services.AddSingleton(_ => _rootComponentCache ?? new()); Services.AddSingleton(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/WebAssemblyJsonSerializerContext.cs b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/WebAssemblyJsonSerializerContext.cs new file mode 100644 index 000000000000..fdb41e372d3b --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/WebAssemblyJsonSerializerContext.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; + +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + +// Required for WebAssemblyComponentParameterDeserializer +[JsonSerializable(typeof(ComponentParameter[]))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(IList))] + +// Required for DefaultWebAssemblyJSRuntime +[JsonSerializable(typeof(RootComponentOperationBatch))] +internal sealed partial class WebAssemblyJsonSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs index a00694cbcd12..0c4884243585 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -75,15 +76,14 @@ public ParameterView DeserializeParameters(IList parametersD [DynamicDependency(JsonSerialized, typeof(ComponentParameter))] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The correct members will be preserved by the above DynamicDependency.")] - // This should use JSON source generation public static ComponentParameter[] GetParameterDefinitions(string parametersDefinitions) { - return JsonSerializer.Deserialize(parametersDefinitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!; + return JsonSerializer.Deserialize(parametersDefinitions, WebAssemblyJsonSerializerContext.Default.ComponentParameterArray)!; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to preserve component parameter types.")] public static IList GetParameterValues(string parameterValues) { - return JsonSerializer.Deserialize>(parameterValues, WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!; + return JsonSerializer.Deserialize(parameterValues, WebAssemblyJsonSerializerContext.Default.IListObject)!; } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index e0447385ba68..a2297cb2f8b7 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -9,8 +9,8 @@ using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Services; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.JSInterop; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering; @@ -23,11 +23,13 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer { private readonly ILogger _logger; private readonly Dispatcher _dispatcher; + private readonly IInternalJSImportMethods _jsMethods; public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop) { _logger = loggerFactory.CreateLogger(); + _jsMethods = serviceProvider.GetRequiredService(); // if SynchronizationContext.Current is null, it means we are on the single-threaded runtime _dispatcher = WebAssemblyDispatcher._mainSynchronizationContext == null @@ -70,9 +72,9 @@ private void OnUpdateRootComponents(RootComponentOperationBatch batch) NotifyEndUpdateRootComponents(batch.BatchId); } - public static void NotifyEndUpdateRootComponents(long batchId) + public void NotifyEndUpdateRootComponents(long batchId) { - DefaultWebAssemblyJSRuntime.Instance.InvokeVoid("Blazor._internal.endUpdateRootComponents", batchId); + _jsMethods.EndUpdateRootComponents(batchId); } public override Dispatcher Dispatcher => _dispatcher; @@ -87,11 +89,7 @@ public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type compo protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector) { - DefaultWebAssemblyJSRuntime.Instance.InvokeVoid( - "Blazor._internal.attachRootComponentToElement", - domElementSelector, - componentId, - RendererId); + _jsMethods.AttachRootComponentToElement(domElementSelector, componentId, RendererId); } /// diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 31e7c1cbe61c..6d0ce0b78110 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -6,7 +6,9 @@ using System.Runtime.InteropServices.JavaScript; using System.Runtime.Versioning; using System.Text.Json; +using Microsoft.AspNetCore.Components.Web.Internal; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; using Microsoft.JSInterop.WebAssembly; @@ -14,10 +16,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services; -internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime +internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime, IInternalWebJSInProcessRuntime { + public static readonly DefaultWebAssemblyJSRuntime Instance = new(); + private readonly RootComponentTypeCache _rootComponentCache = new(); - internal static readonly DefaultWebAssemblyJSRuntime Instance = new(); public ElementReferenceContext ElementReferenceContext { get; } @@ -110,9 +113,9 @@ public static void UpdateRootComponentsCore(string operationsJson) [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The correct members will be preserved by the above DynamicDependency")] internal static RootComponentOperationBatch DeserializeOperations(string operationsJson) { - var deserialized = JsonSerializer.Deserialize( + var deserialized = JsonSerializer.Deserialize( operationsJson, - WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!; + WebAssemblyJsonSerializerContext.Default.RootComponentOperationBatch)!; for (var i = 0; i < deserialized.Operations.Length; i++) { @@ -162,4 +165,7 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference { return TransmitDataStreamToJS.TransmitStreamAsync(this, "Blazor._internal.receiveWebAssemblyDotNetDataStream", streamId, dotNetStreamReference); } + + string IInternalWebJSInProcessRuntime.InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + => InvokeJS(identifier, argsJson, resultType, targetInstanceId); } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/IInternalJSImportMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Services/IInternalJSImportMethods.cs index e406454b1b20..83f14d1bd2b1 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/IInternalJSImportMethods.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/IInternalJSImportMethods.cs @@ -9,6 +9,10 @@ internal interface IInternalJSImportMethods string GetApplicationEnvironment(); + void AttachRootComponentToElement(string domElementSelector, int componentId, int rendererId); + + void EndUpdateRootComponents(long batchId); + void NavigationManager_EnableNavigationInterception(int rendererId); void NavigationManager_ScrollToElement(string id); diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/InternalJSImportMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Services/InternalJSImportMethods.cs index 8006401cac65..811a78dbc652 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/InternalJSImportMethods.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/InternalJSImportMethods.cs @@ -27,6 +27,12 @@ public static async Task GetInitialComponentUpdate( public string GetApplicationEnvironment() => GetApplicationEnvironmentCore(); + public void AttachRootComponentToElement(string domElementSelector, int componentId, int rendererId) + => AttachRootComponentToElementCore(domElementSelector, componentId, rendererId); + + public void EndUpdateRootComponents(long batchId) + => EndUpdateRootComponentsCore(batchId); + public void NavigationManager_EnableNavigationInterception(int rendererId) => NavigationManager_EnableNavigationInterceptionCore(rendererId); @@ -66,6 +72,12 @@ public string RegisteredComponents_GetParameterValues(int id) [JSImport("Blazor._internal.getApplicationEnvironment", "blazor-internal")] private static partial string GetApplicationEnvironmentCore(); + [JSImport("Blazor._internal.attachRootComponentToElement", "blazor-internal")] + private static partial void AttachRootComponentToElementCore(string domElementSelector, int componentId, int rendererId); + + [JSImport("Blazor._internal.endUpdateRootComponents", "blazor-internal")] + private static partial void EndUpdateRootComponentsCore([JSMarshalAs] long batchId); + [JSImport(BrowserNavigationManagerInterop.EnableNavigationInterception, "blazor-internal")] private static partial void NavigationManager_EnableNavigationInterceptionCore(int rendererId); diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs index 0e6f259e7c72..2dec1e6bc51b 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -14,13 +14,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; public class WebAssemblyHostBuilderTest { - private static readonly JsonSerializerOptions JsonOptions = new(); - [Fact] public void Build_AllowsConfiguringConfiguration() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Configuration.AddInMemoryCollection(new[] { @@ -38,7 +36,7 @@ public void Build_AllowsConfiguringConfiguration() public void Build_AllowsConfiguringServices() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); // This test also verifies that we create a scope. builder.Services.AddScoped(); @@ -54,7 +52,7 @@ public void Build_AllowsConfiguringServices() public void Build_AllowsConfiguringContainer() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddScoped(); var factory = new MyFakeServiceProviderFactory(); @@ -72,7 +70,7 @@ public void Build_AllowsConfiguringContainer() public void Build_AllowsConfiguringContainer_WithDelegate() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddScoped(); @@ -95,7 +93,7 @@ public void Build_AllowsConfiguringContainer_WithDelegate() public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -112,7 +110,7 @@ public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation public void Build_InProduction_ConfiguresWithServiceProviderWithScopeValidation() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -129,7 +127,7 @@ public void Build_InProduction_ConfiguresWithServiceProviderWithScopeValidation( public void Builder_InDevelopment_SetsHostEnvironmentProperty() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); // Assert Assert.NotNull(builder.HostEnvironment); @@ -140,7 +138,7 @@ public void Builder_InDevelopment_SetsHostEnvironmentProperty() public void Builder_CreatesNavigationManager() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); // Act var host = builder.Build(); @@ -190,7 +188,7 @@ public IServiceProvider CreateServiceProvider(MyFakeDIBuilderThing containerBuil public void Build_AddsConfigurationToServices() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Configuration.AddInMemoryCollection(new[] { @@ -225,7 +223,7 @@ private static IReadOnlyList DefaultServiceTypes public void Constructor_AddsDefaultServices() { // Arrange & Act - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); foreach (var type in DefaultServiceTypes) { @@ -237,7 +235,7 @@ public void Constructor_AddsDefaultServices() public void Builder_SupportsConfiguringLogging() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); var provider = new Mock(); // Act diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs index 587d3b626b31..552ca8272707 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs @@ -12,15 +12,13 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; public class WebAssemblyHostTest { - private static readonly JsonSerializerOptions JsonOptions = new(); - // This won't happen in the product code, but we need to be able to safely call RunAsync // to be able to test a few of the other details. [Fact] public async Task RunAsync_CanExitBasedOnCancellationToken() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddSingleton(Mock.Of()); var host = builder.Build(); var cultureProvider = new TestSatelliteResourcesLoader(); @@ -40,7 +38,7 @@ public async Task RunAsync_CanExitBasedOnCancellationToken() public async Task RunAsync_CallingTwiceCausesException() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddSingleton(Mock.Of()); var host = builder.Build(); var cultureProvider = new TestSatelliteResourcesLoader(); @@ -62,7 +60,7 @@ public async Task RunAsync_CallingTwiceCausesException() public async Task DisposeAsync_CanDisposeAfterCallingRunAsync() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); builder.Services.AddSingleton(Mock.Of()); builder.Services.AddSingleton(); var host = builder.Build(); diff --git a/src/Components/WebAssembly/WebAssembly/test/TestInternalJSImportMethods.cs b/src/Components/WebAssembly/WebAssembly/test/TestInternalJSImportMethods.cs index 505e4335f280..74e1d6b676ac 100644 --- a/src/Components/WebAssembly/WebAssembly/test/TestInternalJSImportMethods.cs +++ b/src/Components/WebAssembly/WebAssembly/test/TestInternalJSImportMethods.cs @@ -20,6 +20,10 @@ public string GetApplicationEnvironment() public string GetPersistedState() => null; + public void AttachRootComponentToElement(string domElementSelector, int componentId, int rendererId) { } + + public void EndUpdateRootComponents(long batchId) { } + public void NavigationManager_EnableNavigationInterception(int rendererId) { } public void NavigationManager_ScrollToElement(string id) { } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index d7a3755ff3c5..3759e9ad0478 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -8,4 +8,4 @@ *REMOVED*Microsoft.JSInterop.IJSUnmarshalledRuntime.InvokeUnmarshalled(string! identifier, T0 arg0, T1 arg1, T2 arg2) -> TResult *REMOVED*Microsoft.JSInterop.IJSUnmarshalledRuntime.InvokeUnmarshalled(string! identifier, T0 arg0, T1 arg1) -> TResult *REMOVED*Microsoft.JSInterop.IJSUnmarshalledRuntime.InvokeUnmarshalled(string! identifier, T0 arg0) -> TResult -*REMOVED*Microsoft.JSInterop.IJSUnmarshalledRuntime.InvokeUnmarshalled(string! identifier) -> TResult \ No newline at end of file +*REMOVED*Microsoft.JSInterop.IJSUnmarshalledRuntime.InvokeUnmarshalled(string! identifier) -> TResult diff --git a/src/Shared/Components/PrerenderComponentApplicationStore.cs b/src/Shared/Components/PrerenderComponentApplicationStore.cs index 2c76d4034a1a..8c66acead0cc 100644 --- a/src/Shared/Components/PrerenderComponentApplicationStore.cs +++ b/src/Shared/Components/PrerenderComponentApplicationStore.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.AspNetCore.Components; @@ -29,12 +30,10 @@ public PrerenderComponentApplicationStore(string existingState) [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Simple deserialize of primitive types.")] protected void DeserializeState(byte[] existingState) { - var state = JsonSerializer.Deserialize>(existingState); - if (state == null) - { - throw new ArgumentException("Could not deserialize state correctly", nameof(existingState)); - } - + var state = JsonSerializer.Deserialize( + existingState, + PrerenderComponentApplicationStoreSerializerContext.Default.DictionaryStringByteArray) + ?? throw new ArgumentException("Could not deserialize state correctly", nameof(existingState)); ExistingState = state; } @@ -73,3 +72,6 @@ public Task PersistStateAsync(IReadOnlyDictionary state) public virtual bool SupportsRenderMode(IComponentRenderMode renderMode) => renderMode is null || renderMode is InteractiveWebAssemblyRenderMode || renderMode is InteractiveAutoRenderMode; } + +[JsonSerializable(typeof(Dictionary), GenerationMode = JsonSourceGenerationMode.Serialization)] +internal sealed partial class PrerenderComponentApplicationStoreSerializerContext : JsonSerializerContext;