From a42088e2e7937f014c8aaf88ff2e8558ee57bde0 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 28 Mar 2024 16:35:33 -0700 Subject: [PATCH 01/20] Start using JSON source generator --- .../src/PersistentComponentState.cs | 26 +++++++++ .../Components/src/PublicAPI.Unshipped.txt | 1 + .../src/DefaultAntiforgeryStateProvider.cs | 15 +++++- ...Microsoft.AspNetCore.Components.Web.csproj | 2 + src/Components/Web/src/WebRenderer.cs | 37 ++++++++++--- .../src/Hosting/WebAssemblyHost.cs | 2 +- .../src/Hosting/WebAssemblyHostBuilder.cs | 37 +++++++------ ...bAssemblyComponentParameterDeserializer.cs | 20 +++++-- .../src/Rendering/WebAssemblyRenderer.cs | 20 ++++--- .../Services/DefaultWebAssemblyJSRuntime.cs | 16 +++++- .../src/Services/IInternalJSImportMethods.cs | 4 ++ .../src/Services/InternalJSImportMethods.cs | 12 +++++ .../test/TestInternalJSImportMethods.cs | 4 ++ .../BasicTestApp/BasicTestApp.csproj | 3 ++ .../RazorComponents/App.razor | 5 ++ .../Components.WasmMinimal.csproj | 3 ++ .../src/IJSInProcessRuntime.cs | 3 ++ .../Microsoft.JSInterop/src/IJSRuntime.cs | 13 +++++ .../src/JSInProcessRuntimeExtensions.cs | 3 ++ .../Microsoft.JSInterop/src/JSRuntime.cs | 54 ++++++++++++++++--- .../src/JSRuntimeExtensions.cs | 16 ++++++ .../src/PublicAPI.Unshipped.txt | 5 +- .../PrerenderComponentApplicationStore.cs | 20 ++++--- 23 files changed, 265 insertions(+), 56 deletions(-) diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index bc193dd77e5f..0469e9d9fabd 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -114,6 +115,31 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call } } + /// + /// TODO + /// + /// + /// + /// + /// + /// + public bool TryTakeFromJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, JsonTypeInfo jsonTypeInfo, [MaybeNullWhen(false)] out TValue? instance) + { + ArgumentNullException.ThrowIfNull(key); + + if (TryTake(key, out var data)) + { + var reader = new Utf8JsonReader(data); + instance = JsonSerializer.Deserialize(ref reader, jsonTypeInfo)!; + return true; + } + else + { + instance = default; + return false; + } + } + private bool TryTake(string key, out byte[]? value) { ArgumentNullException.ThrowIfNull(key); diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..31e78573d8a4 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(string! key, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, out TValue? instance) -> bool diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 6a3d926a73a2..5cedb7702b3f 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.AspNetCore.Components.Forms; -internal class DefaultAntiforgeryStateProvider : AntiforgeryStateProvider, IDisposable +internal partial class DefaultAntiforgeryStateProvider : AntiforgeryStateProvider, IDisposable { private const string PersistenceKey = $"__internal__{nameof(AntiforgeryRequestToken)}"; private readonly PersistingComponentStateSubscription _subscription; @@ -28,7 +30,10 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) return Task.CompletedTask; }, RenderMode.InteractiveAuto); - state.TryTakeFromJson(PersistenceKey, out _currentToken); + state.TryTakeFromJson( + PersistenceKey, + DefaultAntiforgeryStateProviderSerializerContext.Default.AntiforgeryRequestToken, + out _currentToken); } /// @@ -36,4 +41,10 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) /// public void Dispose() => _subscription.Dispose(); + + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(AntiforgeryRequestToken))] + internal partial class DefaultAntiforgeryStateProviderSerializerContext : JsonSerializerContext + { + } } diff --git a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj index b3ec9165085a..2b337152af34 100644 --- a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj +++ b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj @@ -7,6 +7,8 @@ true Microsoft.AspNetCore.Components enable + + $(NoWarn);SYSLIB0020 true false diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 840369c559c7..8e7d9a821032 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -3,6 +3,7 @@ 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.Extensions.DependencyInjection; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree; /// /// A that attaches its components to a browser DOM. /// -public abstract class WebRenderer : Renderer +public abstract partial class WebRenderer : Renderer { private readonly DotNetObjectReference _interopMethodsReference; private readonly int _rendererId; @@ -41,12 +42,25 @@ 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(); + + async Task AttachWebRendererInterop() + { + try + { + await jsRuntime.InvokeVoidAsync( + "Blazor._internal.attachWebRendererInterop", + WebRendererSerializerContext.Default, + _rendererId, + _interopMethodsReference, + jsComponentInterop.Configuration.JSComponentParametersByIdentifier, + jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + } + } } /// @@ -144,4 +158,13 @@ public void SetRootComponentParameters(int componentId, int parameterCount, Json public void RemoveRootComponent(int componentId) => _jsComponentInterop.RemoveRootComponent(componentId); } + + [JsonSerializable(typeof(object[]))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(DotNetObjectReference))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary>))] + internal partial class WebRendererSerializerContext : JsonSerializerContext + { + } } 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..810b49ebb713 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -28,6 +28,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; @@ -72,6 +73,7 @@ internal WebAssemblyHostBuilder( // 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 +88,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 +119,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 +130,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 +163,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 +307,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/Prerendering/WebAssemblyComponentParameterDeserializer.cs b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs index a00694cbcd12..4f930706439f 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -3,12 +3,18 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; -internal sealed class WebAssemblyComponentParameterDeserializer +internal sealed partial class WebAssemblyComponentParameterDeserializer { + private static readonly JsonSerializerOptions _jsonSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) + { + TypeInfoResolver = WebAssemblyComponentParameterDeserializerSerializerContext.Default, + }; + private readonly ComponentParametersTypeCache _parametersCache; public WebAssemblyComponentParameterDeserializer( @@ -75,15 +81,21 @@ 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, _jsonSerializerOptions)!; } [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, _jsonSerializerOptions)!; + } + + [JsonSerializable(typeof(ComponentParameter[]))] + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(IList))] + internal partial class WebAssemblyComponentParameterDeserializerSerializerContext : JsonSerializerContext + { } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index e0447385ba68..de0c33cc536c 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,10 @@ 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); + //DefaultWebAssemblyJSRuntime.Instance.InvokeVoid("Blazor._internal.endUpdateRootComponents", batchId); + _jsMethods.EndUpdateRootComponents(batchId); } public override Dispatcher Dispatcher => _dispatcher; @@ -87,11 +90,12 @@ 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); + //DefaultWebAssemblyJSRuntime.Instance.InvokeVoid( + // "Blazor._internal.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..db43f5254a95 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices.JavaScript; using System.Runtime.Versioning; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; @@ -16,8 +17,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services; internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime { + private static readonly JsonSerializerOptions _rootComponentSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) + { + TypeInfoResolver = DefaultWebAssemblyJSRuntimeSerializerContext.Default, + }; + + public static readonly DefaultWebAssemblyJSRuntime Instance = new(); + private readonly RootComponentTypeCache _rootComponentCache = new(); - internal static readonly DefaultWebAssemblyJSRuntime Instance = new(); public ElementReferenceContext ElementReferenceContext { get; } @@ -112,7 +119,7 @@ internal static RootComponentOperationBatch DeserializeOperations(string operati { var deserialized = JsonSerializer.Deserialize( operationsJson, - WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!; + _rootComponentSerializerOptions)!; for (var i = 0; i < deserialized.Operations.Length; i++) { @@ -162,4 +169,9 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference { return TransmitDataStreamToJS.TransmitStreamAsync(this, "Blazor._internal.receiveWebAssemblyDotNetDataStream", streamId, dotNetStreamReference); } + + [JsonSerializable(typeof(RootComponentOperationBatch))] + internal partial class DefaultWebAssemblyJSRuntimeSerializerContext : JsonSerializerContext + { + } } 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/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/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index a336a1090ced..460961b85479 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -11,6 +11,9 @@ true + + browser; + true diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index befd36e52a53..08b02cad20ee 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -70,6 +70,11 @@ }); } }, + configureRuntime: (builder) => { + builder.withConfig({ + browserProfilerOptions: {}, + }); + }, }, }).then(() => { const startedParagraph = document.createElement('p'); diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index 3b5fd66e8188..188f62b80e6b 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -5,6 +5,9 @@ enable enable WasmMinimal + + browser; + true diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs index 82a49f14acc2..356f96f30902 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs @@ -11,6 +11,9 @@ namespace Microsoft.JSInterop; /// public interface IJSInProcessRuntime : IJSRuntime { + // TODO: Remove as many references to this as possible and use the high-performance + // WASM interop APIs. + /// /// Invokes the specified JavaScript function synchronously. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 0313bf613470..491a3ddfbd1e 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -24,6 +25,18 @@ public interface IJSRuntime /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args); + /// + /// TODO + /// + /// + /// + /// + /// + /// + /// + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerContext jsonSerializerContext, object?[]? args) + => throw new InvalidOperationException("Not implemented"); + /// /// Invokes the specified JavaScript function asynchronously. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs index 40bbbc8ba158..73cd52d1b4fd 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs @@ -11,6 +11,9 @@ namespace Microsoft.JSInterop; /// public static class JSInProcessRuntimeExtensions { + // TODO: Remove as many references to this as possible and use the high-performance + // WASM interop APIs. + /// /// Invokes the specified JavaScript function synchronously. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 3cbd024d2755..4a9b27bc515f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -68,6 +69,10 @@ protected JSRuntime() public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); + /// + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerContext jsonSerializerContext, object?[]? args) + => InvokeAsync(0, identifier, jsonSerializerContext, args); + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -82,22 +87,26 @@ protected JSRuntime() public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => InvokeAsync(0, identifier, cancellationToken, args); - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, JsonSerializerContext? jsonSerializerContext, object?[]? args) { if (DefaultAsyncTimeout.HasValue) { using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value); // We need to await here due to the using - return await InvokeAsync(targetInstanceId, identifier, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, jsonSerializerContext, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, jsonSerializerContext, CancellationToken.None, args); } + internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) + => InvokeAsync(targetInstanceId, identifier, jsonSerializerContext: null, args); + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, + JsonSerializerContext? jsonSerializerContext, CancellationToken cancellationToken, object?[]? args) { @@ -123,9 +132,23 @@ protected JSRuntime() return new ValueTask(tcs.Task); } - var argsJson = args is not null && args.Length != 0 ? - JsonSerializer.Serialize(args, JsonSerializerOptions) : - null; + string? argsJson = null; + + if (args is not null && args.Length != 0) + { + var options = JsonSerializerOptions; + + if (jsonSerializerContext is not null) + { + options = new(JsonSerializerOptions) + { + TypeInfoResolver = jsonSerializerContext, + }; + } + + argsJson = JsonSerializer.Serialize(args, options); + } + var resultType = JSCallResultTypeHelper.FromGeneric(); BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); @@ -139,6 +162,13 @@ protected JSRuntime() } } + internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( + long targetInstanceId, + string identifier, + CancellationToken cancellationToken, + object?[]? args) + => InvokeAsync(targetInstanceId, identifier, jsonSerializerContext: null, cancellationToken, args); + private void CleanupTasksAndRegistrations(long taskId) { _pendingTasks.TryRemove(taskId, out _); @@ -244,7 +274,17 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe { var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); - var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); + object? result; + if (resultType == typeof(IJSVoidResult)) + { + result = null; + } + else + { + // TODO: Consider tracking a JsonSerializerContext if provided, and pass it back here. + result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); + } + ByteArraysToBeRevived.Clear(); TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 5a4202ed15da..aab500ea026b 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -26,6 +27,21 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, args); } + /// + /// TODO + /// + /// + /// + /// + /// + /// + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerContext jsonSerializerContext, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + await jsRuntime.InvokeAsync(identifier, jsonSerializerContext, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index d7a3755ff3c5..0e972dcf3abc 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -8,4 +8,7 @@ *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 +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file diff --git a/src/Shared/Components/PrerenderComponentApplicationStore.cs b/src/Shared/Components/PrerenderComponentApplicationStore.cs index 2c76d4034a1a..0a72a2add25e 100644 --- a/src/Shared/Components/PrerenderComponentApplicationStore.cs +++ b/src/Shared/Components/PrerenderComponentApplicationStore.cs @@ -3,12 +3,14 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.AspNetCore.Components; #pragma warning disable CA1852 // Seal internal types -internal class PrerenderComponentApplicationStore : IPersistentComponentStateStore +internal partial class PrerenderComponentApplicationStore : IPersistentComponentStateStore #pragma warning restore CA1852 // Seal internal types { private bool _stateIsPersisted; @@ -29,12 +31,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; } @@ -72,4 +72,10 @@ public Task PersistStateAsync(IReadOnlyDictionary state) public virtual bool SupportsRenderMode(IComponentRenderMode renderMode) => renderMode is null || renderMode is InteractiveWebAssemblyRenderMode || renderMode is InteractiveAutoRenderMode; + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(Dictionary))] + internal partial class PrerenderComponentApplicationStoreSerializerContext : JsonSerializerContext + { + } } From dfb6f931e7a9d731c03b8058716e4fed8f2b6b26 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 4 Apr 2024 15:37:24 -0700 Subject: [PATCH 02/20] Updates and cleanup --- .../src/PersistentComponentState.cs | 14 +- .../src/DefaultAntiforgeryStateProvider.cs | 12 +- src/Components/Web/src/WebRenderer.cs | 42 +--- ...bAssemblyComponentParameterDeserializer.cs | 14 +- .../src/Rendering/WebAssemblyRenderer.cs | 6 - .../Services/DefaultWebAssemblyJSRuntime.cs | 8 +- .../BasicTestApp/BasicTestApp.csproj | 3 - .../BasicTestApp/wwwroot/index.html | 3 + .../RazorComponents/App.razor | 5 - .../Components.WasmMinimal.csproj | 3 - .../src/IJSInProcessRuntime.cs | 3 - .../Microsoft.JSInterop/src/IJSInteropTask.cs | 17 ++ .../Microsoft.JSInterop/src/IJSRuntime.cs | 38 ++- .../src/Infrastructure/TaskGenericsUtil.cs | 56 +---- .../src/JSInProcessRuntimeExtensions.cs | 3 - .../Microsoft.JSInterop/src/JSInteropTask.cs | 60 +++++ .../Microsoft.JSInterop/src/JSRuntime.cs | 226 +++++++++++------- .../src/JSRuntimeExtensions.cs | 125 ++++++++-- .../src/PublicAPI.Unshipped.txt | 13 +- .../PrerenderComponentApplicationStore.cs | 13 +- 20 files changed, 409 insertions(+), 255 deletions(-) create mode 100644 src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index 0469e9d9fabd..4b6fbf8961fa 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -116,14 +116,14 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call } /// - /// TODO + /// Tries to retrieve the persisted state as JSON with the given and deserializes it into an + /// instance of type . /// - /// - /// - /// - /// - /// - public bool TryTakeFromJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, JsonTypeInfo jsonTypeInfo, [MaybeNullWhen(false)] out TValue? instance) + /// The key used to persist the instance. + /// The to use when deserializing from JSON. + /// The persisted instance. + /// true if the state was found; false otherwise. + public bool TryTakeFromJson(string key, JsonTypeInfo jsonTypeInfo, [MaybeNullWhen(false)] out TValue? instance) { ArgumentNullException.ThrowIfNull(key); diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 5cedb7702b3f..ee209b4626af 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.AspNetCore.Components.Forms; -internal partial class DefaultAntiforgeryStateProvider : AntiforgeryStateProvider, IDisposable +internal class DefaultAntiforgeryStateProvider : AntiforgeryStateProvider, IDisposable { private const string PersistenceKey = $"__internal__{nameof(AntiforgeryRequestToken)}"; private readonly PersistingComponentStateSubscription _subscription; @@ -41,10 +40,7 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) /// public void Dispose() => _subscription.Dispose(); - - [JsonSourceGenerationOptions] - [JsonSerializable(typeof(AntiforgeryRequestToken))] - internal partial class DefaultAntiforgeryStateProviderSerializerContext : JsonSerializerContext - { - } } + +[JsonSerializable(typeof(AntiforgeryRequestToken))] +internal sealed partial class DefaultAntiforgeryStateProviderSerializerContext : JsonSerializerContext; diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 8e7d9a821032..52c162752825 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree; /// /// A that attaches its components to a browser DOM. /// -public abstract partial class WebRenderer : Renderer +public abstract class WebRenderer : Renderer { private readonly DotNetObjectReference _interopMethodsReference; private readonly int _rendererId; @@ -42,25 +42,13 @@ 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(); - _ = AttachWebRendererInterop(); - - async Task AttachWebRendererInterop() - { - try - { - await jsRuntime.InvokeVoidAsync( - "Blazor._internal.attachWebRendererInterop", - WebRendererSerializerContext.Default, - _rendererId, - _interopMethodsReference, - jsComponentInterop.Configuration.JSComponentParametersByIdentifier, - jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); - } - catch (Exception e) - { - Console.Error.WriteLine(e); - } - } + jsRuntime.InvokeVoidAsync( + "Blazor._internal.attachWebRendererInterop", + WebRendererSerializerContext.Default, + _rendererId, + _interopMethodsReference, + jsComponentInterop.Configuration.JSComponentParametersByIdentifier, + jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); } /// @@ -158,13 +146,9 @@ public void SetRootComponentParameters(int componentId, int parameterCount, Json public void RemoveRootComponent(int componentId) => _jsComponentInterop.RemoveRootComponent(componentId); } - - [JsonSerializable(typeof(object[]))] - [JsonSerializable(typeof(int))] - [JsonSerializable(typeof(DotNetObjectReference))] - [JsonSerializable(typeof(Dictionary))] - [JsonSerializable(typeof(Dictionary>))] - internal partial class WebRendererSerializerContext : JsonSerializerContext - { - } } + +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary>))] +internal sealed partial class WebRendererSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs index 4f930706439f..08bd539f9984 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; -internal sealed partial class WebAssemblyComponentParameterDeserializer +internal sealed class WebAssemblyComponentParameterDeserializer { private static readonly JsonSerializerOptions _jsonSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) { @@ -91,11 +91,9 @@ public static IList GetParameterValues(string parameterValues) { return JsonSerializer.Deserialize>(parameterValues, _jsonSerializerOptions)!; } - - [JsonSerializable(typeof(ComponentParameter[]))] - [JsonSerializable(typeof(JsonElement))] - [JsonSerializable(typeof(IList))] - internal partial class WebAssemblyComponentParameterDeserializerSerializerContext : JsonSerializerContext - { - } } + +[JsonSerializable(typeof(ComponentParameter[]))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(IList))] +internal sealed partial class WebAssemblyComponentParameterDeserializerSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index de0c33cc536c..a2297cb2f8b7 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -74,7 +74,6 @@ private void OnUpdateRootComponents(RootComponentOperationBatch batch) public void NotifyEndUpdateRootComponents(long batchId) { - //DefaultWebAssemblyJSRuntime.Instance.InvokeVoid("Blazor._internal.endUpdateRootComponents", batchId); _jsMethods.EndUpdateRootComponents(batchId); } @@ -91,11 +90,6 @@ public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type compo protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector) { _jsMethods.AttachRootComponentToElement(domElementSelector, componentId, RendererId); - //DefaultWebAssemblyJSRuntime.Instance.InvokeVoid( - // "Blazor._internal.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 db43f5254a95..3446e35a76ba 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -169,9 +169,7 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference { return TransmitDataStreamToJS.TransmitStreamAsync(this, "Blazor._internal.receiveWebAssemblyDotNetDataStream", streamId, dotNetStreamReference); } - - [JsonSerializable(typeof(RootComponentOperationBatch))] - internal partial class DefaultWebAssemblyJSRuntimeSerializerContext : JsonSerializerContext - { - } } + +[JsonSerializable(typeof(RootComponentOperationBatch))] +internal sealed partial class DefaultWebAssemblyJSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index 460961b85479..a336a1090ced 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -11,9 +11,6 @@ true - - browser; - true diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html index 9506d48c434e..9d52f5356cdb 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html @@ -66,6 +66,9 @@ Blazor.start({ configureRuntime: dotnet => { dotnet.withEnvironmentVariable("CONFIGURE_RUNTIME", "true"); + dotnet.withConfig({ + browserProfilerOptions: {}, + }); } }); })(); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 08b02cad20ee..befd36e52a53 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -70,11 +70,6 @@ }); } }, - configureRuntime: (builder) => { - builder.withConfig({ - browserProfilerOptions: {}, - }); - }, }, }).then(() => { const startedParagraph = document.createElement('p'); diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index 188f62b80e6b..3b5fd66e8188 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -5,9 +5,6 @@ enable enable WasmMinimal - - browser; - true diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs index 356f96f30902..82a49f14acc2 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs @@ -11,9 +11,6 @@ namespace Microsoft.JSInterop; /// public interface IJSInProcessRuntime : IJSRuntime { - // TODO: Remove as many references to this as possible and use the high-performance - // WASM interop APIs. - /// /// Invokes the specified JavaScript function synchronously. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs new file mode 100644 index 000000000000..e13ba2d3cadd --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs @@ -0,0 +1,17 @@ +// 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; + +namespace Microsoft.JSInterop; + +internal interface IJSInteropTask : IDisposable +{ + public Type ResultType { get; } + + public JsonSerializerOptions? DeserializeOptions { get; set; } + + void SetResult(object? result); + + void SetException(Exception exception); +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 491a3ddfbd1e..1484c983d79b 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -26,16 +26,19 @@ public interface IJSRuntime ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args); /// - /// TODO + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, + /// consider using . + /// /// - /// - /// - /// - /// - /// - /// - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerContext jsonSerializerContext, object?[]? args) - => throw new InvalidOperationException("Not implemented"); + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) + => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); /// /// Invokes the specified JavaScript function asynchronously. @@ -49,4 +52,19 @@ public interface IJSRuntime /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args); + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) + => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs index a03d87536db1..7cfe3964dfe7 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs @@ -2,26 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.Globalization; namespace Microsoft.JSInterop.Infrastructure; internal static class TaskGenericsUtil { - private static readonly ConcurrentDictionary _cachedResultGetters - = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary _cachedResultSetters - = new ConcurrentDictionary(); - - public static void SetTaskCompletionSourceResult(object taskCompletionSource, object? result) - => CreateResultSetter(taskCompletionSource).SetResult(taskCompletionSource, result); - - public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception) - => CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception); - - public static Type GetTaskCompletionSourceResultType(object taskCompletionSource) - => CreateResultSetter(taskCompletionSource).ResultType; + private static readonly ConcurrentDictionary _cachedResultGetters = []; public static object? GetTaskResult(Task task) { @@ -52,13 +38,6 @@ public static Type GetTaskCompletionSourceResultType(object taskCompletionSource : null; } - interface ITcsResultSetter - { - Type ResultType { get; } - void SetResult(object taskCompletionSource, object? result); - void SetException(object taskCompletionSource, Exception exception); - } - private interface ITaskResultGetter { object? GetResult(Task task); @@ -77,37 +56,4 @@ private sealed class VoidTaskResultGetter : ITaskResultGetter return null; } } - - private sealed class TcsResultSetter : ITcsResultSetter - { - public Type ResultType => typeof(T); - - public void SetResult(object tcs, object? result) - { - var typedTcs = (TaskCompletionSource)tcs; - - // If necessary, attempt a cast - var typedResult = result is T resultT - ? resultT - : (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture)!; - - typedTcs.SetResult(typedResult!); - } - - public void SetException(object tcs, Exception exception) - { - var typedTcs = (TaskCompletionSource)tcs; - typedTcs.SetException(exception); - } - } - - private static ITcsResultSetter CreateResultSetter(object taskCompletionSource) - { - return _cachedResultSetters.GetOrAdd(taskCompletionSource.GetType(), tcsType => - { - var resultType = tcsType.GetGenericArguments()[0]; - return (ITcsResultSetter)Activator.CreateInstance( - typeof(TcsResultSetter<>).MakeGenericType(resultType))!; - }); - } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs index 73cd52d1b4fd..40bbbc8ba158 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs @@ -11,9 +11,6 @@ namespace Microsoft.JSInterop; /// public static class JSInProcessRuntimeExtensions { - // TODO: Remove as many references to this as possible and use the high-performance - // WASM interop APIs. - /// /// Invokes the specified JavaScript function synchronously. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs new file mode 100644 index 000000000000..7271a4a10568 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; + +namespace Microsoft.JSInterop; + +internal sealed class JSInteropTask : IJSInteropTask +{ + private readonly TaskCompletionSource _tcs; + private readonly CancellationTokenRegistration _cancellationTokenRegistration; + private readonly Action? _onCanceled; + + public JsonSerializerOptions? DeserializeOptions { get; set; } + + public Task Task => _tcs.Task; + + public Type ResultType => typeof(TResult); + + public JSInteropTask(CancellationToken cancellationToken, Action? onCanceled = null) + { + _tcs = new TaskCompletionSource(); + _onCanceled = onCanceled; + + if (cancellationToken.CanBeCanceled) + { + _cancellationTokenRegistration = cancellationToken.Register(Cancel); + } + } + + public void SetResult(object? result) + { + if (result is not TResult typedResult) + { + typedResult = (TResult)Convert.ChangeType(result, typeof(TResult), CultureInfo.InvariantCulture)!; + } + + _tcs.SetResult(typedResult); + _cancellationTokenRegistration.Dispose(); + } + + public void SetException(Exception exception) + { + _tcs.SetException(exception); + _cancellationTokenRegistration.Dispose(); + } + + private void Cancel() + { + _cancellationTokenRegistration.Dispose(); + _tcs.TrySetCanceled(); + _onCanceled?.Invoke(); + } + + public void Dispose() + { + _cancellationTokenRegistration.Dispose(); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 4a9b27bc515f..5f92498840e6 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -4,11 +4,18 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection.Metadata; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; +[assembly: MetadataUpdateHandler(typeof(JSRuntime.MetadataUpdateHandler))] + namespace Microsoft.JSInterop; /// @@ -18,9 +25,14 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable { private long _nextObjectReferenceId; // Initial value of 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" - private readonly ConcurrentDictionary _pendingTasks = new(); + private readonly ConcurrentDictionary _pendingTasks = new(); private readonly ConcurrentDictionary _trackedRefsById = new(); - private readonly ConcurrentDictionary _cancellationRegistrations = new(); + + // We expect JSON type info resolvers to be long-lived objects in most cases. This is because they'll + // typically be generated by the JSON source generator and referenced via generated static properties. + // Therefore, we shouldn't need to worry about type info resolvers not getting GC'd due to referencing + // them here. + private readonly ConcurrentDictionary _cachedSerializerOptions = []; internal readonly ArrayBuilder ByteArraysToBeRevived = new(); @@ -41,8 +53,13 @@ protected JSRuntime() new JSStreamReferenceJsonConverter(this), new DotNetStreamReferenceJsonConverter(this), new ByteArrayJsonConverter(this), - } + }, }; + + if (MetadataUpdater.IsSupported) + { + TrackJSRuntimeInstance(this); + } } /// @@ -55,111 +72,102 @@ protected JSRuntime() /// protected TimeSpan? DefaultAsyncTimeout { get; set; } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different, or no timeout, - /// consider using . - /// - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. + /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerContext jsonSerializerContext, object?[]? args) - => InvokeAsync(0, identifier, jsonSerializerContext, args); + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) + => InvokeAsync(0, identifier, resolver, args); - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. + /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => InvokeAsync(0, identifier, cancellationToken, args); - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, JsonSerializerContext? jsonSerializerContext, object?[]? args) + /// + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) + => InvokeAsync(0, identifier, resolver, cancellationToken, args); + + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, IJsonTypeInfoResolver? resolver, object?[]? args) { if (DefaultAsyncTimeout.HasValue) { using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value); // We need to await here due to the using - return await InvokeAsync(targetInstanceId, identifier, jsonSerializerContext, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, resolver, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, jsonSerializerContext, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, resolver, CancellationToken.None, args); } internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) - => InvokeAsync(targetInstanceId, identifier, jsonSerializerContext: null, args); + => InvokeAsync(targetInstanceId, identifier, resolver: null, args); [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, - JsonSerializerContext? jsonSerializerContext, + IJsonTypeInfoResolver? resolver, CancellationToken cancellationToken, object?[]? args) { - var taskId = Interlocked.Increment(ref _nextPendingTaskId); - var tcs = new TaskCompletionSource(); - if (cancellationToken.CanBeCanceled) + if (cancellationToken.IsCancellationRequested) { - _cancellationRegistrations[taskId] = cancellationToken.Register(() => - { - tcs.TrySetCanceled(cancellationToken); - CleanupTasksAndRegistrations(taskId); - }); + return ValueTask.FromCanceled(cancellationToken); } - _pendingTasks[taskId] = tcs; - try + var taskId = Interlocked.Increment(ref _nextPendingTaskId); + var interopTask = new JSInteropTask(cancellationToken, onCanceled: () => { - if (cancellationToken.IsCancellationRequested) - { - tcs.TrySetCanceled(cancellationToken); - CleanupTasksAndRegistrations(taskId); + _pendingTasks.TryRemove(taskId, out _); + }); - return new ValueTask(tcs.Task); - } - - string? argsJson = null; + _pendingTasks[taskId] = interopTask; - if (args is not null && args.Length != 0) + try + { + var resultType = JSCallResultTypeHelper.FromGeneric(); + var jsonSerializerOptions = GetJsonSerializerOptionsForResolver(resolver); + var argsJson = args switch { - var options = JsonSerializerOptions; + null or { Length: 0 } => null, + _ => JsonSerializer.Serialize(args, jsonSerializerOptions), + }; - if (jsonSerializerContext is not null) - { - options = new(JsonSerializerOptions) - { - TypeInfoResolver = jsonSerializerContext, - }; - } - - argsJson = JsonSerializer.Serialize(args, options); - } - - var resultType = JSCallResultTypeHelper.FromGeneric(); + interopTask.DeserializeOptions = jsonSerializerOptions; BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); - return new ValueTask(tcs.Task); + return new ValueTask(interopTask.Task); } catch { - CleanupTasksAndRegistrations(taskId); + _pendingTasks.TryRemove(taskId, out _); + interopTask.Dispose(); throw; } + + JsonSerializerOptions GetJsonSerializerOptionsForResolver(IJsonTypeInfoResolver? resolver) + { + if (resolver is null) + { + return JsonSerializerOptions; + } + + if (!_cachedSerializerOptions.TryGetValue(resolver, out var options)) + { + options = new(JsonSerializerOptions) + { + TypeInfoResolver = JsonTypeInfoResolver.Combine( + resolver, + JSRuntimeSerializerContext.Default, + FallbackTypeInfoResolver.Instance), + }; + _cachedSerializerOptions[resolver] = options; + } + + return options; + } } internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( @@ -167,16 +175,7 @@ protected JSRuntime() string identifier, CancellationToken cancellationToken, object?[]? args) - => InvokeAsync(targetInstanceId, identifier, jsonSerializerContext: null, cancellationToken, args); - - private void CleanupTasksAndRegistrations(long taskId) - { - _pendingTasks.TryRemove(taskId, out _); - if (_cancellationRegistrations.TryRemove(taskId, out var registration)) - { - registration.Dispose(); - } - } + => InvokeAsync(targetInstanceId, identifier, resolver: null, cancellationToken, args); /// /// Begins an asynchronous function invocation. @@ -259,20 +258,18 @@ protected internal virtual Task ReadJSDataAsStreamAsync(IJSStreamReferen [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync.")] internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader) { - if (!_pendingTasks.TryRemove(taskId, out var tcs)) + if (!_pendingTasks.TryRemove(taskId, out var interopTask)) { // We should simply return if we can't find an id for the invocation. // This likely means that the method that initiated the call defined a timeout and stopped waiting. return false; } - CleanupTasksAndRegistrations(taskId); - try { if (succeeded) { - var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); + var resultType = interopTask.ResultType; object? result; if (resultType == typeof(IJSVoidResult)) @@ -281,17 +278,16 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe } else { - // TODO: Consider tracking a JsonSerializerContext if provided, and pass it back here. - result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); + result = JsonSerializer.Deserialize(ref jsonReader, resultType, interopTask.DeserializeOptions); } ByteArraysToBeRevived.Clear(); - TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); + interopTask.SetResult(result); } else { var exceptionText = jsonReader.GetString() ?? string.Empty; - TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText)); + interopTask.SetException(new JSException(exceptionText)); } return true; @@ -299,9 +295,13 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe catch (Exception exception) { var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details."; - TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception)); + interopTask.SetException(new JSException(message, exception)); return false; } + finally + { + interopTask.Dispose(); + } } /// @@ -372,8 +372,64 @@ internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) /// The ID of the . internal void ReleaseObjectReference(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); + /// + /// Clears all caches associated with this instance. + /// + internal void ClearCaches() + { + _cachedSerializerOptions.Clear(); + } + /// /// Dispose the JSRuntime. /// public void Dispose() => ByteArraysToBeRevived.Dispose(); + + private static void TrackJSRuntimeInstance(JSRuntime jsRuntime) + => TrackedJSRuntimeInstances.All.Add(jsRuntime, null); + + internal static class TrackedJSRuntimeInstances + { + // Tracks all live JSRuntime instances. All instances add themselves to this table in their + // constructor when hot reload is enabled. + public static readonly ConditionalWeakTable All = []; + } + + internal static class MetadataUpdateHandler + { + public static void ClearCache(Type[]? _) + { + foreach (var (jsRuntime, _) in TrackedJSRuntimeInstances.All) + { + jsRuntime.ClearCaches(); + } + } + } +} + +[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync")] +internal sealed class FallbackTypeInfoResolver : IJsonTypeInfoResolver +{ + private static readonly DefaultJsonTypeInfoResolver _defaultJsonTypeInfoResolver = new(); + + public static readonly FallbackTypeInfoResolver Instance = new(); + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (options.Converters.Any(c => c.CanConvert(type))) + { + // TODO: We should allow types with custom converters to be serialized without + // having to generate metadata for them. + // Question: Why do we even need a JsonTypeInfo if the type is going to be serialized + // with a custom converter anyway? We shouldn't need to perform any reflection here. + // Is it possible to generate a "minimal" JsonTypeInfo that just points to the correct + // converter? + return _defaultJsonTypeInfoResolver.GetTypeInfo(type, options); + } + + return null; + } } + +[JsonSerializable(typeof(object[]))] // JS interop argument lists are always object arrays +internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index aab500ea026b..71b7d2f7ea52 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -28,18 +28,92 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string } /// - /// TODO + /// Invokes the specified JavaScript function asynchronously. /// - /// - /// - /// - /// - /// - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerContext jsonSerializerContext, params object?[]? args) + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization. + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - await jsRuntime.InvokeAsync(identifier, jsonSerializerContext, args); + await jsRuntime.InvokeAsync(identifier, resolver, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); } /// @@ -61,6 +135,26 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return jsRuntime.InvokeAsync(identifier, args); } + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, + /// consider using . + /// + /// + /// The . + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + return jsRuntime.InvokeAsync(identifier, resolver, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -83,19 +177,21 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// Invokes the specified JavaScript function asynchronously. /// + /// The JSON-serializable return type. /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. /// /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts /// () from being applied. /// /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object?[]? args) + /// An instance of obtained by JSON-deserializing the return value. + public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + return jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); } /// @@ -121,16 +217,17 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. /// The duration after which to cancel the async operation. Overrides default timeouts (). /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object?[]? args) + public static async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + return await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 0e972dcf3abc..1a79f6cd0b27 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -9,6 +9,13 @@ *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 -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext, object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file diff --git a/src/Shared/Components/PrerenderComponentApplicationStore.cs b/src/Shared/Components/PrerenderComponentApplicationStore.cs index 0a72a2add25e..195c42f8833b 100644 --- a/src/Shared/Components/PrerenderComponentApplicationStore.cs +++ b/src/Shared/Components/PrerenderComponentApplicationStore.cs @@ -4,13 +4,12 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.AspNetCore.Components; #pragma warning disable CA1852 // Seal internal types -internal partial class PrerenderComponentApplicationStore : IPersistentComponentStateStore +internal class PrerenderComponentApplicationStore : IPersistentComponentStateStore #pragma warning restore CA1852 // Seal internal types { private bool _stateIsPersisted; @@ -72,10 +71,8 @@ public Task PersistStateAsync(IReadOnlyDictionary state) public virtual bool SupportsRenderMode(IComponentRenderMode renderMode) => renderMode is null || renderMode is InteractiveWebAssemblyRenderMode || renderMode is InteractiveAutoRenderMode; - - [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] - [JsonSerializable(typeof(Dictionary))] - internal partial class PrerenderComponentApplicationStoreSerializerContext : JsonSerializerContext - { - } } + +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] +[JsonSerializable(typeof(Dictionary))] +internal sealed partial class PrerenderComponentApplicationStoreSerializerContext : JsonSerializerContext; From 11c518c72e95993b2b2a3d55ea22dd60ad8e9289 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 4 Apr 2024 16:04:21 -0700 Subject: [PATCH 03/20] Undo changes in index.html --- src/Components/test/testassets/BasicTestApp/wwwroot/index.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html index 9d52f5356cdb..9506d48c434e 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html @@ -66,9 +66,6 @@ Blazor.start({ configureRuntime: dotnet => { dotnet.withEnvironmentVariable("CONFIGURE_RUNTIME", "true"); - dotnet.withConfig({ - browserProfilerOptions: {}, - }); } }); })(); From 0eb1454c15977e35042dddbe0c1e512cbcb1bd10 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 5 Apr 2024 17:17:42 -0700 Subject: [PATCH 04/20] Fix Antiforgery --- .../Microsoft.AspNetCore.Components.csproj | 1 + .../src/PersistentComponentState.cs | 9 ++- .../Components/src/PublicAPI.Unshipped.txt | 2 +- .../src/DefaultAntiforgeryStateProvider.cs | 2 +- .../Shared/src/JsonSerializerOptionsCache.cs | 75 +++++++++++++++++++ .../Microsoft.JSInterop/src/JSRuntime.cs | 75 ++----------------- .../src/Microsoft.JSInterop.csproj | 1 + .../PrerenderComponentApplicationStore.cs | 3 +- 8 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 src/Components/Shared/src/JsonSerializerOptionsCache.cs diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 9ebdbfc3778f..ba20808b6a79 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index 4b6fbf8961fa..ff6de37b09ec 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -17,6 +17,7 @@ public class PersistentComponentState private readonly IDictionary _currentState; private readonly List _registeredCallbacks; + private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache = new(JsonSerializerOptionsProvider.Options); internal PersistentComponentState( IDictionary currentState, @@ -120,17 +121,19 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call /// instance of type . /// /// The key used to persist the instance. - /// The to use when deserializing from JSON. + /// The to use when deserializing from JSON. /// The persisted instance. /// true if the state was found; false otherwise. - public bool TryTakeFromJson(string key, JsonTypeInfo jsonTypeInfo, [MaybeNullWhen(false)] out TValue? instance) + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public bool TryTakeFromJson(string key, IJsonTypeInfoResolver resolver, [MaybeNullWhen(false)] out TValue? instance) { ArgumentNullException.ThrowIfNull(key); if (TryTake(key, out var data)) { var reader = new Utf8JsonReader(data); - instance = JsonSerializer.Deserialize(ref reader, jsonTypeInfo)!; + var options = _jsonSerializerOptionsCache.GetOrAdd(resolver); + instance = JsonSerializer.Deserialize(ref reader, options)!; return true; } else diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 31e78573d8a4..e111812b4a50 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ #nullable enable -Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(string! key, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, out TValue? instance) -> bool +Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(string! key, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, out TValue? instance) -> bool diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index ee209b4626af..9c12d3b42458 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -31,7 +31,7 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) state.TryTakeFromJson( PersistenceKey, - DefaultAntiforgeryStateProviderSerializerContext.Default.AntiforgeryRequestToken, + DefaultAntiforgeryStateProviderSerializerContext.Default, out _currentToken); } diff --git a/src/Components/Shared/src/JsonSerializerOptionsCache.cs b/src/Components/Shared/src/JsonSerializerOptionsCache.cs new file mode 100644 index 000000000000..b3a8f6b3eddd --- /dev/null +++ b/src/Components/Shared/src/JsonSerializerOptionsCache.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Reflection.Metadata; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +[assembly: MetadataUpdateHandler(typeof(JsonSerializerOptionsCache.MetadataUpdateHandler))] + +internal sealed class JsonSerializerOptionsCache +{ + private readonly JsonSerializerOptions _baseOptions; + + // We expect JSON type info resolvers to be long-lived objects in most cases. This is because they'll + // typically be generated by the JSON source generator and referenced via generated static properties. + // Therefore, we shouldn't need to worry about type info resolvers not getting GC'd due to referencing + // them here. + private readonly ConcurrentDictionary _cachedSerializerOptions = []; + + public JsonSerializerOptionsCache(JsonSerializerOptions baseOptions) + { + _baseOptions = baseOptions; + + if (MetadataUpdater.IsSupported) + { + TrackInstance(this); + } + } + + public JsonSerializerOptions GetOrAdd( + IJsonTypeInfoResolver? resolver, + Func? valueFactory = null) + { + if (resolver is null) + { + return _baseOptions; + } + + return _cachedSerializerOptions.GetOrAdd(resolver, static (resolver, args) => + { + if (args.valueFactory is not null) + { + resolver = args.valueFactory(resolver); + } + + return new JsonSerializerOptions(args.cache._baseOptions) + { + TypeInfoResolver = resolver, + }; + }, (cache: this, valueFactory)); + } + + private static void TrackInstance(JsonSerializerOptionsCache instance) + => TrackedJsonSerializerOptionsCaches.All.Add(instance, null); + + internal static class TrackedJsonSerializerOptionsCaches + { + // Tracks all live JSRuntime instances. All instances add themselves to this table in their + // constructor when hot reload is enabled. + public static readonly ConditionalWeakTable All = []; + } + + internal static class MetadataUpdateHandler + { + public static void ClearCache(Type[]? _) + { + foreach (var (cache, _) in TrackedJsonSerializerOptionsCaches.All) + { + cache._cachedSerializerOptions.Clear(); + } + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 5f92498840e6..eb3ac1359b26 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -5,17 +5,12 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection.Metadata; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; -[assembly: MetadataUpdateHandler(typeof(JSRuntime.MetadataUpdateHandler))] - namespace Microsoft.JSInterop; /// @@ -27,12 +22,7 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" private readonly ConcurrentDictionary _pendingTasks = new(); private readonly ConcurrentDictionary _trackedRefsById = new(); - - // We expect JSON type info resolvers to be long-lived objects in most cases. This is because they'll - // typically be generated by the JSON source generator and referenced via generated static properties. - // Therefore, we shouldn't need to worry about type info resolvers not getting GC'd due to referencing - // them here. - private readonly ConcurrentDictionary _cachedSerializerOptions = []; + private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache; internal readonly ArrayBuilder ByteArraysToBeRevived = new(); @@ -56,10 +46,7 @@ protected JSRuntime() }, }; - if (MetadataUpdater.IsSupported) - { - TrackJSRuntimeInstance(this); - } + _jsonSerializerOptionsCache = new(JsonSerializerOptions); } /// @@ -127,7 +114,12 @@ protected JSRuntime() try { var resultType = JSCallResultTypeHelper.FromGeneric(); - var jsonSerializerOptions = GetJsonSerializerOptionsForResolver(resolver); + var jsonSerializerOptions = _jsonSerializerOptionsCache.GetOrAdd(resolver, static resolver => + JsonTypeInfoResolver.Combine( + resolver, + JSRuntimeSerializerContext.Default, + FallbackTypeInfoResolver.Instance)); + var argsJson = args switch { null or { Length: 0 } => null, @@ -146,28 +138,6 @@ protected JSRuntime() interopTask.Dispose(); throw; } - - JsonSerializerOptions GetJsonSerializerOptionsForResolver(IJsonTypeInfoResolver? resolver) - { - if (resolver is null) - { - return JsonSerializerOptions; - } - - if (!_cachedSerializerOptions.TryGetValue(resolver, out var options)) - { - options = new(JsonSerializerOptions) - { - TypeInfoResolver = JsonTypeInfoResolver.Combine( - resolver, - JSRuntimeSerializerContext.Default, - FallbackTypeInfoResolver.Instance), - }; - _cachedSerializerOptions[resolver] = options; - } - - return options; - } } internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( @@ -372,39 +342,10 @@ internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) /// The ID of the . internal void ReleaseObjectReference(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); - /// - /// Clears all caches associated with this instance. - /// - internal void ClearCaches() - { - _cachedSerializerOptions.Clear(); - } - /// /// Dispose the JSRuntime. /// public void Dispose() => ByteArraysToBeRevived.Dispose(); - - private static void TrackJSRuntimeInstance(JSRuntime jsRuntime) - => TrackedJSRuntimeInstances.All.Add(jsRuntime, null); - - internal static class TrackedJSRuntimeInstances - { - // Tracks all live JSRuntime instances. All instances add themselves to this table in their - // constructor when hot reload is enabled. - public static readonly ConditionalWeakTable All = []; - } - - internal static class MetadataUpdateHandler - { - public static void ClearCache(Type[]? _) - { - foreach (var (jsRuntime, _) in TrackedJSRuntimeInstances.All) - { - jsRuntime.ClearCaches(); - } - } - } } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync")] diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj index baebaf34e3c8..8092928fcb03 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Shared/Components/PrerenderComponentApplicationStore.cs b/src/Shared/Components/PrerenderComponentApplicationStore.cs index 195c42f8833b..8c66acead0cc 100644 --- a/src/Shared/Components/PrerenderComponentApplicationStore.cs +++ b/src/Shared/Components/PrerenderComponentApplicationStore.cs @@ -73,6 +73,5 @@ public virtual bool SupportsRenderMode(IComponentRenderMode renderMode) => renderMode is null || renderMode is InteractiveWebAssemblyRenderMode || renderMode is InteractiveAutoRenderMode; } -[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] -[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary), GenerationMode = JsonSourceGenerationMode.Serialization)] internal sealed partial class PrerenderComponentApplicationStoreSerializerContext : JsonSerializerContext; From 24335bc75afcab491801d1e3218bc40ef5756d2c Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 12 Apr 2024 16:55:37 -0700 Subject: [PATCH 05/20] Trying a confiugration-based approach --- .../ComponentStatePersistenceManager.cs | 21 +++- src/Components/Components/src/JsonOptions.cs | 17 +++ .../src/JsonServiceCollectionExtensions.cs | 24 ++++ .../Microsoft.AspNetCore.Components.csproj | 2 +- .../src/PersistentComponentState.cs | 39 +----- .../Components/src/PublicAPI.Unshipped.txt | 7 +- .../Lifetime/ComponentApplicationStateTest.cs | 18 +-- .../Server/src/Circuits/RemoteJSRuntime.cs | 8 ++ .../ComponentServiceCollectionExtensions.cs | 5 + ...rosoft.AspNetCore.Components.Server.csproj | 6 +- .../ProtectedBrowserStorage.cs | 13 +- .../ProtectedLocalStorage.cs | 26 +++- .../ProtectedSessionStorage.cs | 26 +++- .../Server/src/PublicAPI.Unshipped.txt | 2 + .../Server/test/Circuits/CircuitHostTest.cs | 2 +- .../test/Circuits/RemoteJSDataStreamTest.cs | 27 ++-- .../test/Circuits/RemoteJSRuntimeTest.cs | 13 +- .../test/Circuits/RemoteRendererTest.cs | 2 +- .../Server/test/Circuits/TestCircuitHost.cs | 6 +- .../test/ProtectedBrowserStorageTest.cs | 2 +- ...yJsonOptionsServiceCollectionExtensions.cs | 19 +++ .../src/DefaultAntiforgeryStateProvider.cs | 5 +- .../src/DefaultJsonSerializerOptions.cs | 27 ++++ .../JSRuntimeSerializerContext.cs | 9 ++ .../JsonConverterFactoryTypeInfoResolver.cs | 41 ++++++ .../src/JsonSerializerOptionsProvider.cs | 15 --- .../JsonOptionsServiceCollectionExtensions.cs | 28 +++++ .../Web/src/PublicAPI.Unshipped.txt | 2 + src/Components/Web/src/WebRenderer.cs | 6 +- .../WebEventData/ChangeEventArgsReaderTest.cs | 2 +- .../ClipboardEventArgsReaderTest.cs | 2 +- .../WebEventData/DragEventArgsReaderTest.cs | 2 +- .../WebEventData/ErrorEventArgsReaderTest.cs | 2 +- .../WebEventData/FocusEventArgsReaderTest.cs | 2 +- .../KeyboardEventArgsReaderTest.cs | 2 +- .../WebEventData/MouseEventArgsReaderTest.cs | 2 +- .../PointerEventArgsReaderTest.cs | 2 +- .../ProgressEventArgsReaderTest.cs | 2 +- .../WebEventData/TouchEventArgsReaderTest.cs | 2 +- .../WebEventDescriptorReaderTest.cs | 2 +- .../WebEventData/WheelEventArgsReaderTest.cs | 2 +- .../src/Hosting/WebAssemblyHost.cs | 4 +- .../src/Hosting/WebAssemblyHostBuilder.cs | 33 +++-- ...t.AspNetCore.Components.WebAssembly.csproj | 4 +- .../src/Rendering/WebAssemblyRenderer.cs | 9 +- .../Services/DefaultWebAssemblyJSRuntime.cs | 34 +++-- .../WebView/WebView/src/IpcCommon.cs | 4 +- ...osoft.AspNetCore.Components.WebView.csproj | 2 +- .../BasicTestApp/BasicTestApp.csproj | 7 +- .../test/testassets/BasicTestApp/Program.cs | 13 +- .../Microsoft.JSInterop/src/IJSInteropTask.cs | 4 - .../Microsoft.JSInterop/src/IJSRuntime.cs | 31 ----- .../Microsoft.JSInterop/src/JSInteropTask.cs | 3 - .../Microsoft.JSInterop/src/JSRuntime.cs | 118 +++++++----------- .../src/JSRuntimeExtensions.cs | 113 ----------------- .../src/PublicAPI.Unshipped.txt | 10 -- 56 files changed, 467 insertions(+), 364 deletions(-) create mode 100644 src/Components/Components/src/JsonOptions.cs create mode 100644 src/Components/Components/src/JsonServiceCollectionExtensions.cs create mode 100644 src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs create mode 100644 src/Components/Shared/src/DefaultJsonSerializerOptions.cs create mode 100644 src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs create mode 100644 src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs delete mode 100644 src/Components/Shared/src/JsonSerializerOptionsProvider.cs create mode 100644 src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs diff --git a/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs index e1b4fdf605ec..7b81bc474ef1 100644 --- a/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs +++ b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs @@ -1,8 +1,10 @@ // 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 Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.Infrastructure; @@ -21,8 +23,25 @@ public class ComponentStatePersistenceManager /// Initializes a new instance of . /// public ComponentStatePersistenceManager(ILogger logger) + : this(DefaultJsonSerializerOptions.Instance, logger) { - State = new PersistentComponentState(_currentState, _registeredCallbacks); + } + + /// + /// Initializes a new instance of . + /// + public ComponentStatePersistenceManager( + IOptions jsonOptions, + ILogger logger) + : this(jsonOptions.Value.SerializerOptions, logger) + { + } + + private ComponentStatePersistenceManager( + JsonSerializerOptions jsonSerializerOptions, + ILogger logger) + { + State = new PersistentComponentState(jsonSerializerOptions, _currentState, _registeredCallbacks); _logger = logger; } diff --git a/src/Components/Components/src/JsonOptions.cs b/src/Components/Components/src/JsonOptions.cs new file mode 100644 index 000000000000..d8b0ac51090b --- /dev/null +++ b/src/Components/Components/src/JsonOptions.cs @@ -0,0 +1,17 @@ +// 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; + +namespace Microsoft.AspNetCore.Components; + +/// +/// Options to configure JSON serialization settings for components. +/// +public sealed class JsonOptions +{ + /// + /// Gets the . + /// + public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultJsonSerializerOptions.Instance); +} diff --git a/src/Components/Components/src/JsonServiceCollectionExtensions.cs b/src/Components/Components/src/JsonServiceCollectionExtensions.cs new file mode 100644 index 000000000000..29345a1cbf87 --- /dev/null +++ b/src/Components/Components/src/JsonServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for configuring JSON options for components. +/// +public static class JsonServiceCollectionExtensions +{ + /// + /// Configures options used for serializing JSON in components functionality. + /// + /// The to configure options on. + /// The to configure the . + /// The modified . + public static IServiceCollection ConfigureComponentsJsonOptions(this IServiceCollection services, Action configureOptions) + { + services.Configure(configureOptions); + return services; + } +} diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index ba20808b6a79..3a548c346f60 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -15,8 +15,8 @@ - + diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index ff6de37b09ec..d285144ba200 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -15,14 +14,15 @@ public class PersistentComponentState { private IDictionary? _existingState; private readonly IDictionary _currentState; - + private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly List _registeredCallbacks; - private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache = new(JsonSerializerOptionsProvider.Options); internal PersistentComponentState( - IDictionary currentState, + JsonSerializerOptions jsonSerializerOptions, + IDictionary currentState, List pauseCallbacks) { + _jsonSerializerOptions = jsonSerializerOptions; _currentState = currentState; _registeredCallbacks = pauseCallbacks; } @@ -86,7 +86,7 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call throw new ArgumentException($"There is already a persisted object under the same key '{key}'"); } - _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options)); + _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, _jsonSerializerOptions)); } /// @@ -106,34 +106,7 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call if (TryTake(key, out var data)) { var reader = new Utf8JsonReader(data); - instance = JsonSerializer.Deserialize(ref reader, JsonSerializerOptionsProvider.Options)!; - return true; - } - else - { - instance = default; - return false; - } - } - - /// - /// Tries to retrieve the persisted state as JSON with the given and deserializes it into an - /// instance of type . - /// - /// The key used to persist the instance. - /// The to use when deserializing from JSON. - /// The persisted instance. - /// true if the state was found; false otherwise. - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] - public bool TryTakeFromJson(string key, IJsonTypeInfoResolver resolver, [MaybeNullWhen(false)] out TValue? instance) - { - ArgumentNullException.ThrowIfNull(key); - - if (TryTake(key, out var data)) - { - var reader = new Utf8JsonReader(data); - var options = _jsonSerializerOptionsCache.GetOrAdd(resolver); - instance = JsonSerializer.Deserialize(ref reader, options)!; + instance = JsonSerializer.Deserialize(ref reader, _jsonSerializerOptions)!; return true; } else diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index e111812b4a50..1d8ad6476c94 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,2 +1,7 @@ #nullable enable -Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(string! key, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, out TValue? instance) -> bool +Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Options.IOptions! jsonOptions, Microsoft.Extensions.Logging.ILogger! logger) -> void +Microsoft.AspNetCore.Components.JsonOptions +Microsoft.AspNetCore.Components.JsonOptions.JsonOptions() -> void +Microsoft.AspNetCore.Components.JsonOptions.SerializerOptions.get -> System.Text.Json.JsonSerializerOptions! +Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions.ConfigureComponentsJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs b/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs index bed42d42dfbf..0d4ba5efa728 100644 --- a/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs +++ b/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs @@ -11,7 +11,7 @@ public class ComponentApplicationStateTest public void InitializeExistingState_SetupsState() { // Arrange - var applicationState = new PersistentComponentState(new Dictionary(), new List()); + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); var existingState = new Dictionary { ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 }) @@ -29,7 +29,7 @@ public void InitializeExistingState_SetupsState() public void InitializeExistingState_ThrowsIfAlreadyInitialized() { // Arrange - var applicationState = new PersistentComponentState(new Dictionary(), new List()); + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); var existingState = new Dictionary { ["MyState"] = new byte[] { 1, 2, 3, 4 } @@ -45,7 +45,7 @@ public void InitializeExistingState_ThrowsIfAlreadyInitialized() public void TryRetrieveState_ReturnsStateWhenItExists() { // Arrange - var applicationState = new PersistentComponentState(new Dictionary(), new List()); + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); var existingState = new Dictionary { ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 }) @@ -65,7 +65,7 @@ public void PersistState_SavesDataToTheStoreAsync() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(currentState, new List()) + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) { PersistingState = true }; @@ -84,7 +84,7 @@ public void PersistState_ThrowsForDuplicateKeys() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(currentState, new List()) + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) { PersistingState = true }; @@ -101,7 +101,7 @@ public void PersistAsJson_SerializesTheDataToJsonAsync() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(currentState, new List()) + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) { PersistingState = true }; @@ -120,7 +120,7 @@ public void PersistAsJson_NullValueAsync() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(currentState, new List()) + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) { PersistingState = true }; @@ -140,7 +140,7 @@ public void TryRetrieveFromJson_DeserializesTheDataFromJson() var myState = new byte[] { 1, 2, 3, 4 }; var serialized = JsonSerializer.SerializeToUtf8Bytes(myState); var existingState = new Dictionary() { ["MyState"] = serialized }; - var applicationState = new PersistentComponentState(new Dictionary(), new List()); + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); applicationState.InitializeExistingState(existingState); @@ -158,7 +158,7 @@ public void TryRetrieveFromJson_NullValue() // Arrange var serialized = JsonSerializer.SerializeToUtf8Bytes(null); var existingState = new Dictionary() { ["MyState"] = serialized }; - var applicationState = new PersistentComponentState(new Dictionary(), new List()); + var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); applicationState.InitializeExistingState(existingState); diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 34c76baada6f..0eeef0680ffa 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -40,6 +40,7 @@ internal partial class RemoteJSRuntime : JSRuntime public RemoteJSRuntime( IOptions circuitOptions, IOptions> componentHubOptions, + IOptions jsonOptions, ILogger logger) { _options = circuitOptions.Value; @@ -48,6 +49,13 @@ public RemoteJSRuntime( DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout; ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); + + JsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver.Instance); + + if (jsonOptions.Value.SerializerOptions is { TypeInfoResolver: { } typeInfoResolver }) + { + JsonSerializerOptions.TypeInfoResolverChain.Add(typeInfoResolver); + } } public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index bea90b2ec054..b2c48b7cb5a5 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -87,6 +88,10 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJavaScriptInitializersConfiguration>()); + // Configure JSON serializer options + services.ConfigureComponentsWebJsonOptions(); + services.ConfigureDefaultAntiforgeryJsonOptions(); + if (configure != null) { services.Configure(configure); diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index 260e60ac5efb..c82f21277938 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -59,11 +59,15 @@ + + + + + - diff --git a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs index 1903de5feb83..d71ec2a608e4 100644 --- a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs +++ b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs @@ -16,6 +16,7 @@ public abstract class ProtectedBrowserStorage private readonly string _storeName; private readonly IJSRuntime _jsRuntime; private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly ConcurrentDictionary _cachedDataProtectorsByPurpose = new ConcurrentDictionary(StringComparer.Ordinal); @@ -25,7 +26,12 @@ private readonly ConcurrentDictionary _cachedDataProtect /// The name of the store in which the data should be stored. /// The . /// The . - private protected ProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) + /// The . + private protected ProtectedBrowserStorage( + string storeName, + IJSRuntime jsRuntime, + IDataProtectionProvider dataProtectionProvider, + JsonSerializerOptions? jsonSerializerOptions) { // Performing data protection on the client would give users a false sense of security, so we'll prevent this. if (OperatingSystem.IsBrowser()) @@ -38,6 +44,7 @@ private protected ProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime _storeName = storeName; _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); _dataProtectionProvider = dataProtectionProvider ?? throw new ArgumentNullException(nameof(dataProtectionProvider)); + _jsonSerializerOptions = jsonSerializerOptions ?? DefaultJsonSerializerOptions.Instance; } /// @@ -122,7 +129,7 @@ public ValueTask DeleteAsync(string key) private string Protect(string purpose, object value) { - var json = JsonSerializer.Serialize(value, options: JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.Serialize(value, options: _jsonSerializerOptions); var protector = GetOrCreateCachedProtector(purpose); return protector.Protect(json); @@ -133,7 +140,7 @@ private TValue Unprotect(string purpose, string protectedJson) var protector = GetOrCreateCachedProtector(purpose); var json = protector.Unprotect(protectedJson); - return JsonSerializer.Deserialize(json, options: JsonSerializerOptionsProvider.Options)!; + return JsonSerializer.Deserialize(json, options: _jsonSerializerOptions)!; } private ValueTask SetProtectedJsonAsync(string key, string protectedJson) diff --git a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs index eca79fde0c09..67d8ed803929 100644 --- a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs +++ b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs @@ -1,7 +1,9 @@ // 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 Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @@ -23,7 +25,29 @@ public sealed class ProtectedLocalStorage : ProtectedBrowserStorage /// The . /// The . public ProtectedLocalStorage(IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) - : base("localStorage", jsRuntime, dataProtectionProvider) + : this(jsRuntime, dataProtectionProvider, jsonSerializerOptions: null) + { + } + + /// + /// Constructs an instance of . + /// + /// The . + /// The . + /// The . + public ProtectedLocalStorage( + IJSRuntime jsRuntime, + IDataProtectionProvider dataProtectionProvider, + IOptions jsonOptions) + : this(jsRuntime, dataProtectionProvider, jsonOptions.Value.SerializerOptions) + { + } + + private ProtectedLocalStorage( + IJSRuntime jsRuntime, + IDataProtectionProvider dataProtectionProvider, + JsonSerializerOptions? jsonSerializerOptions) + : base("localStorage", jsRuntime, dataProtectionProvider, jsonSerializerOptions) { } } diff --git a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs index 4a6d395931aa..af213952c462 100644 --- a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs +++ b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs @@ -1,7 +1,9 @@ // 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 Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @@ -23,7 +25,29 @@ public sealed class ProtectedSessionStorage : ProtectedBrowserStorage /// The . /// The . public ProtectedSessionStorage(IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) - : base("sessionStorage", jsRuntime, dataProtectionProvider) + : this(jsRuntime, dataProtectionProvider, jsonSerializerOptions: null) + { + } + + /// + /// Constructs an instance of . + /// + /// The . + /// The . + /// The . + public ProtectedSessionStorage( + IJSRuntime jsRuntime, + IDataProtectionProvider dataProtectionProvider, + IOptions jsonOptions) + : this(jsRuntime, dataProtectionProvider, jsonOptions.Value.SerializerOptions) + { + } + + private ProtectedSessionStorage( + IJSRuntime jsRuntime, + IDataProtectionProvider dataProtectionProvider, + JsonSerializerOptions? jsonSerializerOptions) + : base("sessionStorage", jsRuntime, dataProtectionProvider, jsonSerializerOptions) { } } diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 9d240138aaac..ebffe4283669 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedLocalStorage.ProtectedLocalStorage(Microsoft.JSInterop.IJSRuntime! jsRuntime, Microsoft.AspNetCore.DataProtection.IDataProtectionProvider! dataProtectionProvider, Microsoft.Extensions.Options.IOptions! jsonOptions) -> void +Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedSessionStorage.ProtectedSessionStorage(Microsoft.JSInterop.IJSRuntime! jsRuntime, Microsoft.AspNetCore.DataProtection.IDataProtectionProvider! dataProtectionProvider, Microsoft.Extensions.Options.IOptions! jsonOptions) -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebSocketAcceptContext.get -> System.Func? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebSocketAcceptContext.set -> void diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index b1d2e5fe0a4d..9f9993cbc6b2 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -726,7 +726,7 @@ protected override void Dispose(bool disposing) } private static RemoteJSRuntime CreateJSRuntime(CircuitOptions options) - => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions()), null); + => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), null); } private class DispatcherComponent : ComponentBase, IDisposable diff --git a/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs b/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs index dac28c3acea4..ad254df37ea7 100644 --- a/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs +++ b/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits; public class RemoteJSDataStreamTest { - private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); [Fact] public async Task CreateRemoteJSDataStreamAsync_CreatesStream() @@ -45,7 +45,7 @@ public async Task ReceiveData_DoesNotFindStream() public async Task ReceiveData_SuccessReadsBackStream() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[100]; @@ -73,7 +73,7 @@ public async Task ReceiveData_SuccessReadsBackStream() public async Task ReceiveData_SuccessReadsBackPipeReader() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[100]; @@ -101,7 +101,7 @@ public async Task ReceiveData_SuccessReadsBackPipeReader() public async Task ReceiveData_WithError() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); @@ -119,7 +119,7 @@ public async Task ReceiveData_WithError() public async Task ReceiveData_WithZeroLengthChunk() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = Array.Empty(); @@ -138,7 +138,7 @@ public async Task ReceiveData_WithZeroLengthChunk() public async Task ReceiveData_WithLargerChunksThanPermitted() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[50_000]; // more than the 32k maximum chunk size @@ -157,7 +157,7 @@ public async Task ReceiveData_WithLargerChunksThanPermitted() public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var jsStreamReference = Mock.Of(); var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); @@ -177,7 +177,7 @@ public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining() public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDisconnect() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); var jsStreamReference = Mock.Of(); var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); @@ -202,7 +202,7 @@ public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed() { // Arrange var unhandledExceptionRaisedTask = new TaskCompletionSource(); - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); jsRuntime.UnhandledException += (_, ex) => { Assert.Equal("Did not receive any data in the allotted time.", ex.Message); @@ -243,7 +243,7 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed() { // Arrange var unhandledExceptionRaisedTask = new TaskCompletionSource(); - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); jsRuntime.UnhandledException += (_, ex) => { Assert.Equal("Did not receive any data in the allotted time.", ex.Message); @@ -299,7 +299,12 @@ private static long GetStreamId(RemoteJSDataStream stream, RemoteJSRuntime runti class TestRemoteJSRuntime : RemoteJSRuntime, IJSRuntime { - public TestRemoteJSRuntime(IOptions circuitOptions, IOptions> hubOptions, ILogger logger) : base(circuitOptions, hubOptions, logger) + public TestRemoteJSRuntime( + IOptions circuitOptions, + IOptions> hubOptions, + IOptions jsonOptions, + ILogger logger) + : base(circuitOptions, hubOptions, jsonOptions, logger) { } diff --git a/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs b/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs index 9b5673d5c973..5ac490ab42dc 100644 --- a/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs +++ b/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs @@ -101,13 +101,22 @@ private static TestRemoteJSRuntime CreateTestRemoteJSRuntime(long? componentHubM { var componentHubOptions = Options.Create(new HubOptions()); componentHubOptions.Value.MaximumReceiveMessageSize = componentHubMaximumIncomingBytes; - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), componentHubOptions, Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime( + Options.Create(new CircuitOptions()), + componentHubOptions, + Options.Create(new JsonOptions()), + Mock.Of>()); return jsRuntime; } class TestRemoteJSRuntime : RemoteJSRuntime, IJSRuntime { - public TestRemoteJSRuntime(IOptions circuitOptions, IOptions> hubOptions, ILogger logger) : base(circuitOptions, hubOptions, logger) + public TestRemoteJSRuntime( + IOptions circuitOptions, + IOptions> hubOptions, + IOptions jsonOptions, + ILogger logger) + : base(circuitOptions, hubOptions, jsonOptions, logger) { } diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index 9ca641979610..896dfb5827dc 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -721,7 +721,7 @@ protected override void AttachRootComponentToBrowser(int componentId, string dom } private static RemoteJSRuntime CreateJSRuntime(CircuitOptions options) - => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions()), null); + => new(Options.Create(options), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), null); } private class TestComponent : IComponent, IHandleAfterRender diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index c0b09cb45189..ec5b4929ec15 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -28,7 +28,11 @@ public static CircuitHost Create( { serviceScope = serviceScope ?? new AsyncServiceScope(Mock.Of()); clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of(), Guid.NewGuid().ToString()); - var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var jsRuntime = new RemoteJSRuntime( + Options.Create(new CircuitOptions()), + Options.Create(new HubOptions()), + Options.Create(new JsonOptions()), + Mock.Of>()); var navigationManager = new RemoteNavigationManager(Mock.Of>()); var serviceProvider = new Mock(); serviceProvider diff --git a/src/Components/Server/test/ProtectedBrowserStorageTest.cs b/src/Components/Server/test/ProtectedBrowserStorageTest.cs index 89da1b351874..159bf71c9901 100644 --- a/src/Components/Server/test/ProtectedBrowserStorageTest.cs +++ b/src/Components/Server/test/ProtectedBrowserStorageTest.cs @@ -370,7 +370,7 @@ public ValueTask InvokeAsync(string identifier, object[] args) class TestProtectedBrowserStorage : ProtectedBrowserStorage { public TestProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) - : base(storeName, jsRuntime, dataProtectionProvider) + : base(storeName, jsRuntime, dataProtectionProvider, jsonSerializerOptions: null) { } } diff --git a/src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs b/src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs new file mode 100644 index 000000000000..11366d032702 --- /dev/null +++ b/src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.Forms; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class DefaultAntiforgeryJsonOptionsServiceCollectionExtensions +{ + public static IServiceCollection ConfigureDefaultAntiforgeryJsonOptions(this IServiceCollection services) + { + services.ConfigureComponentsJsonOptions(static options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, DefaultAntiforgeryStateProviderJsonSerializerContext.Default); + }); + + return services; + } +} diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 9c12d3b42458..8460fdb687fc 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -29,9 +29,10 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) return Task.CompletedTask; }, RenderMode.InteractiveAuto); + // The argument type should be kept in sync with + // DefaultAntiforgeryStateProviderJsonSerializerContext state.TryTakeFromJson( PersistenceKey, - DefaultAntiforgeryStateProviderSerializerContext.Default, out _currentToken); } @@ -43,4 +44,4 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) } [JsonSerializable(typeof(AntiforgeryRequestToken))] -internal sealed partial class DefaultAntiforgeryStateProviderSerializerContext : JsonSerializerContext; +internal sealed partial class DefaultAntiforgeryStateProviderJsonSerializerContext : JsonSerializerContext; diff --git a/src/Components/Shared/src/DefaultJsonSerializerOptions.cs b/src/Components/Shared/src/DefaultJsonSerializerOptions.cs new file mode 100644 index 000000000000..e5eb0f0b3789 --- /dev/null +++ b/src/Components/Shared/src/DefaultJsonSerializerOptions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.AspNetCore.Components; + +internal static class DefaultJsonSerializerOptions +{ + public static readonly JsonSerializerOptions Instance = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeInfoResolver() : JsonTypeInfoResolver.Combine(), + }; + + static DefaultJsonSerializerOptions() + { + Instance.MakeReadOnly(); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This method only gets called when reflection is enabled for JsonSerializer")] + static DefaultJsonTypeInfoResolver CreateDefaultTypeInfoResolver() + => new(); +} diff --git a/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs b/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs new file mode 100644 index 000000000000..ce90bdd316af --- /dev/null +++ b/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs @@ -0,0 +1,9 @@ +// 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.Serialization; + +namespace Microsoft.AspNetCore.Components; + +[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] // JS interop argument lists are always object arrays +internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs new file mode 100644 index 000000000000..ae6b4fa707ef --- /dev/null +++ b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.AspNetCore.Components; + +internal sealed class JsonConverterFactoryTypeInfoResolver : IJsonTypeInfoResolver +{ + private static readonly MethodInfo _createValueInfoMethod = ((Delegate)JsonMetadataServices.CreateValueInfo).Method.GetGenericMethodDefinition(); + + public static readonly JsonConverterFactoryTypeInfoResolver Instance = new(); + + [SuppressMessage("Trimming", "IL2060", Justification = "We expect the incoming type to have already been correctly preserved")] + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + foreach (var converter in options.Converters) + { + if (converter is not JsonConverterFactory factory || !factory.CanConvert(type)) + { + continue; + } + + var converterToUse = factory.CreateConverter(type, options); + var createValueInfo = _createValueInfoMethod.MakeGenericMethod(type); + + if (createValueInfo.Invoke(null, [options, converterToUse]) is not JsonTypeInfo jsonTypeInfo) + { + throw new InvalidOperationException($"Unable to create a {nameof(JsonTypeInfo)} for the type {type.FullName}"); + } + + return jsonTypeInfo; + } + + return null; + } +} diff --git a/src/Components/Shared/src/JsonSerializerOptionsProvider.cs b/src/Components/Shared/src/JsonSerializerOptionsProvider.cs deleted file mode 100644 index b8b60805feed..000000000000 --- a/src/Components/Shared/src/JsonSerializerOptionsProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Components; - -internal static class JsonSerializerOptionsProvider -{ - public static readonly JsonSerializerOptions Options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - }; -} diff --git a/src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs b/src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs new file mode 100644 index 000000000000..332bd786d01e --- /dev/null +++ b/src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Components.Web.Infrastructure; + +/// +/// Extension methods for configuring web-specific JSON options for components. +/// +public static class JsonOptionsServiceCollectionExtensions +{ + /// + /// Configures options used for serializing JSON in web-specific components functionality. + /// + /// The to configure options on. + /// The modified . + public static IServiceCollection ConfigureComponentsWebJsonOptions(this IServiceCollection services) + { + services.ConfigureComponentsJsonOptions(static options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, WebRendererJsonSerializerContext.Default); + }); + + return services; + } +} diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 4befff3c6426..704f0eca5edb 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.Infrastructure.JsonOptionsServiceCollectionExtensions Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.get -> bool Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.set -> void +static Microsoft.AspNetCore.Components.Web.Infrastructure.JsonOptionsServiceCollectionExtensions.ConfigureComponentsWebJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 52c162752825..5ee21f2becf8 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -42,9 +42,11 @@ 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(); + + // The arguments passed to the following invocation should be kept in sync + // with WebRendererJsonSerializerContext. jsRuntime.InvokeVoidAsync( "Blazor._internal.attachWebRendererInterop", - WebRendererSerializerContext.Default, _rendererId, _interopMethodsReference, jsComponentInterop.Configuration.JSComponentParametersByIdentifier, @@ -151,4 +153,4 @@ public void RemoveRootComponent(int componentId) [JsonSerializable(typeof(int))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary>))] -internal sealed partial class WebRendererSerializerContext : JsonSerializerContext; +internal sealed partial class WebRendererJsonSerializerContext : JsonSerializerContext; diff --git a/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs index 72feafe3cecf..786fa87c22b1 100644 --- a/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs @@ -62,7 +62,7 @@ public void Read_WithStringValue() private static JsonElement GetJsonElement(ChangeEventArgs args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs index 0c60d32ca3b5..70032b269e0e 100644 --- a/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs @@ -26,7 +26,7 @@ public void Read_Works() private static JsonElement GetJsonElement(ClipboardEventArgs args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs index d5bf85c7df1e..65618e39a8cb 100644 --- a/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs @@ -80,7 +80,7 @@ private void AssertEqual(DataTransferItem[] expected, DataTransferItem[] actual) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs index 607329646a18..7fee1ada8a58 100644 --- a/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs @@ -35,7 +35,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs index 19836e297d51..cc1f93ef9b9d 100644 --- a/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs @@ -27,7 +27,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs index 8cbafaf5a0c5..5aab91e7a99d 100644 --- a/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs @@ -45,7 +45,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs index d0e72b84145c..17f5e783b9cf 100644 --- a/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs @@ -65,7 +65,7 @@ internal static void AssertEqual(MouseEventArgs expected, MouseEventArgs actual) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs index 2c8caf0a575a..5e4d56f22ad4 100644 --- a/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs @@ -57,7 +57,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs index a3a7b4e48955..98921111dce0 100644 --- a/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs @@ -32,7 +32,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs index ef500609de1f..c29358bf0524 100644 --- a/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs @@ -110,7 +110,7 @@ private void AssertEqual(TouchPoint expected, TouchPoint actual) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs b/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs index b3b3d6d4be4e..7ae7ba6f8f88 100644 --- a/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs +++ b/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs @@ -64,7 +64,7 @@ public void Read_WithBoolValue_Works(bool value) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs index 9ef83c742bde..304abcf76006 100644 --- a/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs @@ -50,7 +50,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); + var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 01e51c2920f0..ca453378aa2d 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -147,8 +148,9 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl using (cancellationToken.Register(() => tcs.TrySetResult())) { var loggerFactory = Services.GetRequiredService(); + var jsonOptions = Services.GetRequiredService>(); var jsComponentInterop = new JSComponentInterop(_rootComponents.JSComponents); - _renderer = new WebAssemblyRenderer(Services, loggerFactory, jsComponentInterop); + _renderer = new WebAssemblyRenderer(Services, loggerFactory, jsonOptions, jsComponentInterop); WebAssemblyNavigationManager.Instance.CreateLogger(loggerFactory); diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 810b49ebb713..f308c60a410c 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -4,12 +4,12 @@ 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; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Rendering; using Microsoft.AspNetCore.Components.WebAssembly.Services; @@ -17,6 +17,7 @@ using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -27,7 +28,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; /// public sealed class WebAssemblyHostBuilder { - private readonly JsonSerializerOptions _jsonOptions; private readonly IInternalJSImportMethods _jsMethods; private Func _createServiceProvider; private RootComponentTypeCache? _rootComponentCache; @@ -49,9 +49,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(); @@ -65,14 +63,11 @@ 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(); @@ -298,6 +293,11 @@ public WebAssemblyHost Build() var services = _createServiceProvider(); var scope = services.GetRequiredService().CreateAsyncScope(); + // Provide JsonOptions to the JS runtime as quickly as possible to ensure that + // JSON options are configured before any JS interop calls occur. + var jsonOptions = scope.ServiceProvider.GetRequiredService>(); + DefaultWebAssemblyJSRuntime.Instance.SetJsonOptions(jsonOptions); + return new WebAssemblyHost(this, services, scope, _persistedState); } @@ -310,7 +310,12 @@ internal void InitializeDefaultServices() Services.AddSingleton(_jsMethods); Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance)); Services.AddSingleton(_ => _rootComponentCache ?? new()); - Services.AddSingleton(); + Services.AddSingleton(static sp => + { + var jsonOptions = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); + return new(jsonOptions, logger); + }); Services.AddSingleton(sp => sp.GetRequiredService().State); Services.AddSingleton(); Services.AddSingleton(); @@ -319,5 +324,13 @@ internal void InitializeDefaultServices() builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance)); }); Services.AddSupplyValueFromQueryProvider(); + + // Configure JSON serializer options + Services.ConfigureComponentsJsonOptions(jsonOptions => + { + jsonOptions.SerializerOptions.TypeInfoResolverChain.Insert(0, DefaultWebAssemblyJSRuntimeSerializerContext.Default); + }); + Services.ConfigureComponentsWebJsonOptions(); + Services.ConfigureDefaultAntiforgeryJsonOptions(); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj index 896817768457..054b6d73ed0d 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj +++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj @@ -27,7 +27,6 @@ - @@ -37,12 +36,15 @@ + + + diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index a2297cb2f8b7..3a09ae07a040 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering; @@ -25,8 +26,12 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer private readonly Dispatcher _dispatcher; private readonly IInternalJSImportMethods _jsMethods; - public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) - : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop) + public WebAssemblyRenderer( + IServiceProvider serviceProvider, + ILoggerFactory loggerFactory, + IOptions jsonOptions, + JSComponentInterop jsComponentInterop) + : base(serviceProvider, loggerFactory, jsonOptions.Value.SerializerOptions, jsComponentInterop) { _logger = loggerFactory.CreateLogger(); _jsMethods = serviceProvider.GetRequiredService(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 3446e35a76ba..e37e71737989 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; using Microsoft.JSInterop.WebAssembly; @@ -17,15 +18,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services; internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime { - private static readonly JsonSerializerOptions _rootComponentSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) - { - TypeInfoResolver = DefaultWebAssemblyJSRuntimeSerializerContext.Default, - }; - public static readonly DefaultWebAssemblyJSRuntime Instance = new(); private readonly RootComponentTypeCache _rootComponentCache = new(); + private JsonSerializerOptions? _rootComponentSerializerOptions; + public ElementReferenceContext ElementReferenceContext { get; } public event Action? OnUpdateRootComponents; @@ -39,9 +37,24 @@ private DefaultWebAssemblyJSRuntime() { ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); + JsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver.Instance); } - public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; + public void SetJsonOptions(IOptions jsonOptions) + { + if (JsonSerializerOptions.IsReadOnly) + { + throw new InvalidOperationException( + "JSON options must be provided to the JS runtime before the it gets used."); + } + + _rootComponentSerializerOptions = jsonOptions.Value.SerializerOptions; + + if (jsonOptions.Value.SerializerOptions is { TypeInfoResolver: { } typeInfoResolver }) + { + JsonSerializerOptions.TypeInfoResolverChain.Add(typeInfoResolver); + } + } [JSExport] [SupportedOSPlatform("browser")] @@ -119,7 +132,8 @@ internal static RootComponentOperationBatch DeserializeOperations(string operati { var deserialized = JsonSerializer.Deserialize( operationsJson, - _rootComponentSerializerOptions)!; + Instance._rootComponentSerializerOptions ?? + DefaultWebAssemblyJSRuntimeSerializerContext.Default.Options)!; for (var i = 0; i < deserialized.Operations.Length; i++) { @@ -143,7 +157,7 @@ internal static RootComponentOperationBatch DeserializeOperations(string operati return deserialized; } - static WebRootComponentParameters DeserializeComponentParameters(ComponentMarker marker) + private static WebRootComponentParameters DeserializeComponentParameters(ComponentMarker marker) { var definitions = WebAssemblyComponentParameterDeserializer.GetParameterDefinitions(marker.ParameterDefinitions!); var values = WebAssemblyComponentParameterDeserializer.GetParameterValues(marker.ParameterValues!); @@ -171,5 +185,9 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference } } +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(RootComponentOperationBatch))] internal sealed partial class DefaultWebAssemblyJSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebView/WebView/src/IpcCommon.cs b/src/Components/WebView/WebView/src/IpcCommon.cs index e81b62ac8a34..6176bc3dd63b 100644 --- a/src/Components/WebView/WebView/src/IpcCommon.cs +++ b/src/Components/WebView/WebView/src/IpcCommon.cs @@ -31,7 +31,7 @@ private static string Serialize(string messageType, object[] args) // Note we do NOT need the JSRuntime specific JsonSerializerOptions as the args needing special handling // (JS/DotNetObjectReference & Byte Arrays) have already been serialized earlier in the JSRuntime. // We run the serialization here to add the `messageType`. - return $"{_ipcMessagePrefix}{JsonSerializer.Serialize(messageTypeAndArgs, JsonSerializerOptionsProvider.Options)}"; + return $"{_ipcMessagePrefix}{JsonSerializer.Serialize(messageTypeAndArgs, DefaultJsonSerializerOptions.Instance)}"; } private static bool TryDeserialize(string message, out T messageType, out ArraySegment args) @@ -41,7 +41,7 @@ private static bool TryDeserialize(string message, out T messageType, out Arr if (message != null && message.StartsWith(_ipcMessagePrefix, StringComparison.Ordinal)) { var messageAfterPrefix = message.AsSpan(_ipcMessagePrefix.Length); - var parsed = (JsonElement[])JsonSerializer.Deserialize(messageAfterPrefix, typeof(JsonElement[]), JsonSerializerOptionsProvider.Options); + var parsed = (JsonElement[])JsonSerializer.Deserialize(messageAfterPrefix, typeof(JsonElement[]), DefaultJsonSerializerOptions.Instance); messageType = (T)Enum.Parse(typeof(T), parsed[0].GetString()); args = new ArraySegment(parsed, 1, parsed.Length - 1); return true; diff --git a/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj b/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj index c669218e36f3..067a9e878d5b 100644 --- a/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj +++ b/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj @@ -24,13 +24,13 @@ - + diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index a336a1090ced..ee7023243c0f 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -11,6 +11,8 @@ true + + true @@ -50,10 +52,7 @@ - + diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 4b147896e621..3f88e2defa5a 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net.Http; +using System.Runtime.InteropServices.JavaScript; using System.Web; using BasicTestApp.AuthTest; using BasicTestApp.PropertyInjection; @@ -16,7 +17,7 @@ namespace BasicTestApp; -public class Program +public partial class Program { public static async Task Main(string[] args) { @@ -82,10 +83,10 @@ private static void ConfigureCulture(WebAssemblyHost host) CultureInfo.DefaultThreadCurrentUICulture = culture; } - // Supports E2E tests in StartupErrorNotificationTest + //Supports E2E tests in StartupErrorNotificationTest private static async Task SimulateErrorsIfNeededForTest() { - var currentUrl = DefaultWebAssemblyJSRuntime.Instance.Invoke("getCurrentUrl"); + var currentUrl = JSFunctions.GetCurrentUrl(); if (currentUrl.Contains("error=sync")) { throw new InvalidTimeZoneException("This is a synchronous startup exception"); @@ -98,4 +99,10 @@ private static async Task SimulateErrorsIfNeededForTest() throw new InvalidTimeZoneException("This is an asynchronous startup exception"); } } + + private static partial class JSFunctions + { + [JSImport("globalThis.getCurrentUrl")] + public static partial string GetCurrentUrl(); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs index e13ba2d3cadd..ea03292f91a1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs @@ -1,16 +1,12 @@ // 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; - namespace Microsoft.JSInterop; internal interface IJSInteropTask : IDisposable { public Type ResultType { get; } - public JsonSerializerOptions? DeserializeOptions { get; set; } - void SetResult(object? result); void SetException(Exception exception); diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 1484c983d79b..0313bf613470 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -25,21 +24,6 @@ public interface IJSRuntime /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args); - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, - /// consider using . - /// - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) - => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -52,19 +36,4 @@ public interface IJSRuntime /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args); - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) - => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs index 7271a4a10568..253224889941 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Text.Json; namespace Microsoft.JSInterop; @@ -12,8 +11,6 @@ internal sealed class JSInteropTask : IJSInteropTask private readonly CancellationTokenRegistration _cancellationTokenRegistration; private readonly Action? _onCanceled; - public JsonSerializerOptions? DeserializeOptions { get; set; } - public Task Task => _tcs.Task; public Type ResultType => typeof(TResult); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index eb3ac1359b26..65ffc60d2836 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -4,10 +4,8 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -22,7 +20,6 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" private readonly ConcurrentDictionary _pendingTasks = new(); private readonly ConcurrentDictionary _trackedRefsById = new(); - private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache; internal readonly ArrayBuilder ByteArraysToBeRevived = new(); @@ -31,22 +28,20 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable /// protected JSRuntime() { - JsonSerializerOptions = new JsonSerializerOptions + JsonSerializerOptions = new() { MaxDepth = 32, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, Converters = - { - new DotNetObjectReferenceJsonConverterFactory(this), - new JSObjectReferenceJsonConverter(this), - new JSStreamReferenceJsonConverter(this), - new DotNetStreamReferenceJsonConverter(this), - new ByteArrayJsonConverter(this), - }, + { + new DotNetObjectReferenceJsonConverterFactory(this), + new JSObjectReferenceJsonConverter(this), + new JSStreamReferenceJsonConverter(this), + new DotNetStreamReferenceJsonConverter(this), + new ByteArrayJsonConverter(this), + }, }; - - _jsonSerializerOptionsCache = new(JsonSerializerOptions); } /// @@ -63,38 +58,26 @@ protected JSRuntime() public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); - /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) - => InvokeAsync(0, identifier, resolver, args); - /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => InvokeAsync(0, identifier, cancellationToken, args); - /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) - => InvokeAsync(0, identifier, resolver, cancellationToken, args); - - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, IJsonTypeInfoResolver? resolver, object?[]? args) + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] 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(targetInstanceId, identifier, resolver, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, resolver, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, CancellationToken.None, args); } - internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) - => InvokeAsync(targetInstanceId, identifier, resolver: null, args); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, - IJsonTypeInfoResolver? resolver, CancellationToken cancellationToken, object?[]? args) { @@ -114,20 +97,13 @@ protected JSRuntime() try { var resultType = JSCallResultTypeHelper.FromGeneric(); - var jsonSerializerOptions = _jsonSerializerOptionsCache.GetOrAdd(resolver, static resolver => - JsonTypeInfoResolver.Combine( - resolver, - JSRuntimeSerializerContext.Default, - FallbackTypeInfoResolver.Instance)); var argsJson = args switch { null or { Length: 0 } => null, - _ => JsonSerializer.Serialize(args, jsonSerializerOptions), + _ => SerializeArgs(args), }; - interopTask.DeserializeOptions = jsonSerializerOptions; - BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); return new ValueTask(interopTask.Task); @@ -138,14 +114,39 @@ protected JSRuntime() interopTask.Dispose(); throw; } - } - internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( - long targetInstanceId, - string identifier, - CancellationToken cancellationToken, - object?[]? args) - => InvokeAsync(targetInstanceId, identifier, resolver: null, cancellationToken, args); + string SerializeArgs(object?[] args) + { + Debug.Assert(args.Length > 0); + + var builder = new StringBuilder(); + builder.Append('['); + + WriteArg(args[0]); + + for (var i = 1; i < args.Length; i++) + { + builder.Append(','); + WriteArg(args[i]); + } + + builder.Append(']'); + return builder.ToString(); + + void WriteArg(object? arg) + { + if (arg is null) + { + builder.Append("null"); + } + else + { + var argJson = JsonSerializer.Serialize(arg, arg.GetType(), JsonSerializerOptions); + builder.Append(argJson); + } + } + } + } /// /// Begins an asynchronous function invocation. @@ -248,7 +249,7 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe } else { - result = JsonSerializer.Deserialize(ref jsonReader, resultType, interopTask.DeserializeOptions); + result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); } ByteArraysToBeRevived.Clear(); @@ -347,30 +348,3 @@ internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) /// public void Dispose() => ByteArraysToBeRevived.Dispose(); } - -[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync")] -internal sealed class FallbackTypeInfoResolver : IJsonTypeInfoResolver -{ - private static readonly DefaultJsonTypeInfoResolver _defaultJsonTypeInfoResolver = new(); - - public static readonly FallbackTypeInfoResolver Instance = new(); - - public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) - { - if (options.Converters.Any(c => c.CanConvert(type))) - { - // TODO: We should allow types with custom converters to be serialized without - // having to generate metadata for them. - // Question: Why do we even need a JsonTypeInfo if the type is going to be serialized - // with a custom converter anyway? We shouldn't need to perform any reflection here. - // Is it possible to generate a "minimal" JsonTypeInfo that just points to the correct - // converter? - return _defaultJsonTypeInfoResolver.GetTypeInfo(type, options); - } - - return null; - } -} - -[JsonSerializable(typeof(object[]))] // JS interop argument lists are always object arrays -internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 71b7d2f7ea52..7cb7cd934374 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -27,21 +26,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - await jsRuntime.InvokeAsync(identifier, resolver, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -60,25 +44,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -97,25 +62,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. - /// The duration after which to cancel the async operation. Overrides default timeouts (). - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); - var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - - await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -135,26 +81,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return jsRuntime.InvokeAsync(identifier, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, - /// consider using . - /// - /// - /// The . - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - return jsRuntime.InvokeAsync(identifier, resolver, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -174,26 +100,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return jsRuntime.InvokeAsync(identifier, cancellationToken, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The JSON-serializable return type. - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - return jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -211,23 +117,4 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// The duration after which to cancel the async operation. Overrides default timeouts (). - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); - var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - - return await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); - } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 1a79f6cd0b27..3759e9ad0478 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -9,13 +9,3 @@ *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 -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file From 54f2b82c15f7b6c46c1d36b5fc9dee2fe94d269b Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 15 Apr 2024 10:29:31 -0700 Subject: [PATCH 06/20] Fix build errors --- .../JsonConverterFactoryTypeInfoResolver.cs | 2 +- ...icationServiceCollectionExtensionsTests.cs | 33 +++++++++---------- .../Hosting/WebAssemblyHostBuilderTest.cs | 25 +++++++------- .../test/Hosting/WebAssemblyHostTest.cs | 9 ++--- .../RazorComponents/App.razor | 5 +++ .../Components.WasmMinimal.csproj | 3 ++ 6 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs index ae6b4fa707ef..f0451666b27a 100644 --- a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs +++ b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs @@ -15,7 +15,7 @@ internal sealed class JsonConverterFactoryTypeInfoResolver : IJsonTypeInfoResolv public static readonly JsonConverterFactoryTypeInfoResolver Instance = new(); - [SuppressMessage("Trimming", "IL2060", Justification = "We expect the incoming type to have already been correctly preserved")] + [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "We expect the incoming type to have already been correctly preserved")] public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) { foreach (var converter in options.Converters) diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs index e20acfac4fab..c22d7745ed05 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Text.Json; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -12,12 +11,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 +24,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 +34,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 +68,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 +95,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 +121,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 +144,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 +170,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 +193,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 +244,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 +283,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 +345,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 +362,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 +384,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 +406,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/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs index 0e6f259e7c72..12b2602cd9d3 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; -using System.Text.Json; using Microsoft.AspNetCore.Components.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -14,13 +13,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 +35,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 +51,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 +69,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 +92,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 +109,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 +126,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 +137,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 +187,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 +222,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 +234,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..6d68c3fe307f 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Text.Json; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; @@ -12,15 +11,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 +37,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 +59,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/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index befd36e52a53..cb50751d3186 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -70,6 +70,11 @@ }); } }, + configureRuntime: (builder) => { + builder.withConfig({ + browserProfilerOptions: {}, + }); + } }, }).then(() => { const startedParagraph = document.createElement('p'); diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index 3b5fd66e8188..5e7934989691 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -5,6 +5,9 @@ enable enable WasmMinimal + + browser; + true From 361c91c8c44d3bdae49b34e84d45fff1219f9942 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 15 Apr 2024 11:07:15 -0700 Subject: [PATCH 07/20] Cleanup --- .../Microsoft.AspNetCore.Components.csproj | 1 - .../JSRuntimeSerializerContext.cs | 3 +- .../JsonConverterFactoryTypeInfoResolver.cs | 3 + .../Shared/src/JsonSerializerOptionsCache.cs | 75 ------------------- .../src/Hosting/WebAssemblyHostBuilder.cs | 13 ++-- ...bAssemblyComponentParameterDeserializer.cs | 2 + .../Services/DefaultWebAssemblyJSRuntime.cs | 2 + .../BasicTestApp/BasicTestApp.csproj | 5 +- .../Components.WasmMinimal.csproj | 3 - .../src/Microsoft.JSInterop.csproj | 1 - 10 files changed, 20 insertions(+), 88 deletions(-) delete mode 100644 src/Components/Shared/src/JsonSerializerOptionsCache.cs diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 3a548c346f60..96bb61826f1b 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs b/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs index ce90bdd316af..88753913226e 100644 --- a/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs +++ b/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs @@ -5,5 +5,6 @@ namespace Microsoft.AspNetCore.Components; -[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] // JS interop argument lists are always object arrays +// JS interop argument lists are always object arrays +[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs index f0451666b27a..1a243e565347 100644 --- a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs +++ b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs @@ -9,6 +9,9 @@ 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 { private static readonly MethodInfo _createValueInfoMethod = ((Delegate)JsonMetadataServices.CreateValueInfo).Method.GetGenericMethodDefinition(); diff --git a/src/Components/Shared/src/JsonSerializerOptionsCache.cs b/src/Components/Shared/src/JsonSerializerOptionsCache.cs deleted file mode 100644 index b3a8f6b3eddd..000000000000 --- a/src/Components/Shared/src/JsonSerializerOptionsCache.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Reflection.Metadata; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -[assembly: MetadataUpdateHandler(typeof(JsonSerializerOptionsCache.MetadataUpdateHandler))] - -internal sealed class JsonSerializerOptionsCache -{ - private readonly JsonSerializerOptions _baseOptions; - - // We expect JSON type info resolvers to be long-lived objects in most cases. This is because they'll - // typically be generated by the JSON source generator and referenced via generated static properties. - // Therefore, we shouldn't need to worry about type info resolvers not getting GC'd due to referencing - // them here. - private readonly ConcurrentDictionary _cachedSerializerOptions = []; - - public JsonSerializerOptionsCache(JsonSerializerOptions baseOptions) - { - _baseOptions = baseOptions; - - if (MetadataUpdater.IsSupported) - { - TrackInstance(this); - } - } - - public JsonSerializerOptions GetOrAdd( - IJsonTypeInfoResolver? resolver, - Func? valueFactory = null) - { - if (resolver is null) - { - return _baseOptions; - } - - return _cachedSerializerOptions.GetOrAdd(resolver, static (resolver, args) => - { - if (args.valueFactory is not null) - { - resolver = args.valueFactory(resolver); - } - - return new JsonSerializerOptions(args.cache._baseOptions) - { - TypeInfoResolver = resolver, - }; - }, (cache: this, valueFactory)); - } - - private static void TrackInstance(JsonSerializerOptionsCache instance) - => TrackedJsonSerializerOptionsCaches.All.Add(instance, null); - - internal static class TrackedJsonSerializerOptionsCaches - { - // Tracks all live JSRuntime instances. All instances add themselves to this table in their - // constructor when hot reload is enabled. - public static readonly ConditionalWeakTable All = []; - } - - internal static class MetadataUpdateHandler - { - public static void ClearCache(Type[]? _) - { - foreach (var (cache, _) in TrackedJsonSerializerOptionsCaches.All) - { - cache._cachedSerializerOptions.Clear(); - } - } - } -} diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index f308c60a410c..5811091c7a6f 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -310,12 +310,13 @@ internal void InitializeDefaultServices() Services.AddSingleton(_jsMethods); Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance)); Services.AddSingleton(_ => _rootComponentCache ?? new()); - Services.AddSingleton(static sp => - { - var jsonOptions = sp.GetRequiredService>(); - var logger = sp.GetRequiredService>(); - return new(jsonOptions, logger); - }); + Services.AddSingleton(); + //Services.AddSingleton(static sp => + //{ + // var jsonOptions = sp.GetRequiredService>(); + // var logger = sp.GetRequiredService>(); + // return new(jsonOptions, logger); + //}); Services.AddSingleton(sp => sp.GetRequiredService().State); Services.AddSingleton(); Services.AddSingleton(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs index 08bd539f9984..694caa52bca9 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -83,12 +83,14 @@ public ParameterView DeserializeParameters(IList parametersD [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The correct members will be preserved by the above DynamicDependency.")] public static ComponentParameter[] GetParameterDefinitions(string parametersDefinitions) { + // Keep in sync with WebAssemblyComponentParameterDeserializerSerializerContext return JsonSerializer.Deserialize(parametersDefinitions, _jsonSerializerOptions)!; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to preserve component parameter types.")] public static IList GetParameterValues(string parameterValues) { + // Keep in sync with WebAssemblyComponentParameterDeserializerSerializerContext return JsonSerializer.Deserialize>(parameterValues, _jsonSerializerOptions)!; } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index e37e71737989..7b4e4ea396d2 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -130,6 +130,8 @@ 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) { + // The type we're serializing should be kept in sync with + // DefaultWebAssemblyJSRuntimeSerializerContext var deserialized = JsonSerializer.Deserialize( operationsJson, Instance._rootComponentSerializerOptions ?? diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index ee7023243c0f..19b48ff20d58 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -52,7 +52,10 @@ - + diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index 5e7934989691..3b5fd66e8188 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -5,9 +5,6 @@ enable enable WasmMinimal - - browser; - true diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj index 8092928fcb03..baebaf34e3c8 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj @@ -18,7 +18,6 @@ - From b06b3c42ec6e80cfc156e46364f29f7cb18d30c0 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 15 Apr 2024 11:15:53 -0700 Subject: [PATCH 08/20] Disable profiling --- .../Components.TestServer/RazorComponents/App.razor | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index cb50751d3186..befd36e52a53 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -70,11 +70,6 @@ }); } }, - configureRuntime: (builder) => { - builder.withConfig({ - browserProfilerOptions: {}, - }); - } }, }).then(() => { const startedParagraph = document.createElement('p'); From 17f9810d2667ca7be363de8ce4b5e7a067ba9b4f Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 16 Apr 2024 11:04:57 -0700 Subject: [PATCH 09/20] Revert configuration-based approach --- .../ComponentStatePersistenceManager.cs | 21 +--- src/Components/Components/src/JsonOptions.cs | 17 --- .../src/JsonServiceCollectionExtensions.cs | 24 ---- .../Microsoft.AspNetCore.Components.csproj | 3 +- .../src/PersistentComponentState.cs | 39 +++++- .../Components/src/PublicAPI.Unshipped.txt | 7 +- .../Lifetime/ComponentApplicationStateTest.cs | 18 +-- .../Server/src/Circuits/RemoteJSRuntime.cs | 8 -- .../ComponentServiceCollectionExtensions.cs | 5 - ...rosoft.AspNetCore.Components.Server.csproj | 6 +- .../ProtectedBrowserStorage.cs | 13 +- .../ProtectedLocalStorage.cs | 26 +--- .../ProtectedSessionStorage.cs | 26 +--- .../Server/src/PublicAPI.Unshipped.txt | 2 - .../Server/test/Circuits/CircuitHostTest.cs | 2 +- .../test/Circuits/RemoteJSDataStreamTest.cs | 27 ++-- .../test/Circuits/RemoteJSRuntimeTest.cs | 13 +- .../test/Circuits/RemoteRendererTest.cs | 2 +- .../Server/test/Circuits/TestCircuitHost.cs | 6 +- .../test/ProtectedBrowserStorageTest.cs | 2 +- ...yJsonOptionsServiceCollectionExtensions.cs | 19 --- .../src/DefaultAntiforgeryStateProvider.cs | 5 +- .../src/DefaultJsonSerializerOptions.cs | 27 ---- .../JSRuntimeSerializerContext.cs | 10 -- .../JsonConverterFactoryTypeInfoResolver.cs | 44 ------- .../Shared/src/JsonSerializerOptionsCache.cs | 75 +++++++++++ .../src/JsonSerializerOptionsProvider.cs | 15 +++ .../JsonOptionsServiceCollectionExtensions.cs | 28 ----- .../Web/src/PublicAPI.Unshipped.txt | 2 - src/Components/Web/src/WebRenderer.cs | 6 +- .../WebEventData/ChangeEventArgsReaderTest.cs | 2 +- .../ClipboardEventArgsReaderTest.cs | 2 +- .../WebEventData/DragEventArgsReaderTest.cs | 2 +- .../WebEventData/ErrorEventArgsReaderTest.cs | 2 +- .../WebEventData/FocusEventArgsReaderTest.cs | 2 +- .../KeyboardEventArgsReaderTest.cs | 2 +- .../WebEventData/MouseEventArgsReaderTest.cs | 2 +- .../PointerEventArgsReaderTest.cs | 2 +- .../ProgressEventArgsReaderTest.cs | 2 +- .../WebEventData/TouchEventArgsReaderTest.cs | 2 +- .../WebEventDescriptorReaderTest.cs | 2 +- .../WebEventData/WheelEventArgsReaderTest.cs | 2 +- ...icationServiceCollectionExtensionsTests.cs | 33 ++--- .../src/Hosting/WebAssemblyHost.cs | 4 +- .../src/Hosting/WebAssemblyHostBuilder.cs | 32 ++--- ...t.AspNetCore.Components.WebAssembly.csproj | 4 +- ...bAssemblyComponentParameterDeserializer.cs | 2 - .../src/Rendering/WebAssemblyRenderer.cs | 9 +- .../Services/DefaultWebAssemblyJSRuntime.cs | 36 ++---- .../Hosting/WebAssemblyHostBuilderTest.cs | 25 ++-- .../test/Hosting/WebAssemblyHostTest.cs | 9 +- .../WebView/WebView/src/IpcCommon.cs | 4 +- ...osoft.AspNetCore.Components.WebView.csproj | 2 +- .../BasicTestApp/BasicTestApp.csproj | 2 - .../test/testassets/BasicTestApp/Program.cs | 13 +- .../Microsoft.JSInterop/src/IJSInteropTask.cs | 4 + .../Microsoft.JSInterop/src/IJSRuntime.cs | 31 +++++ .../Microsoft.JSInterop/src/JSInteropTask.cs | 3 + .../Microsoft.JSInterop/src/JSRuntime.cs | 118 +++++++++++------- .../src/JSRuntimeExtensions.cs | 113 +++++++++++++++++ .../src/Microsoft.JSInterop.csproj | 1 + .../src/PublicAPI.Unshipped.txt | 10 ++ 62 files changed, 474 insertions(+), 503 deletions(-) delete mode 100644 src/Components/Components/src/JsonOptions.cs delete mode 100644 src/Components/Components/src/JsonServiceCollectionExtensions.cs delete mode 100644 src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs delete mode 100644 src/Components/Shared/src/DefaultJsonSerializerOptions.cs delete mode 100644 src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs delete mode 100644 src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs create mode 100644 src/Components/Shared/src/JsonSerializerOptionsCache.cs create mode 100644 src/Components/Shared/src/JsonSerializerOptionsProvider.cs delete mode 100644 src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs diff --git a/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs index 7b81bc474ef1..e1b4fdf605ec 100644 --- a/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs +++ b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs @@ -1,10 +1,8 @@ // 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 Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.Infrastructure; @@ -23,25 +21,8 @@ public class ComponentStatePersistenceManager /// Initializes a new instance of . /// public ComponentStatePersistenceManager(ILogger logger) - : this(DefaultJsonSerializerOptions.Instance, logger) { - } - - /// - /// Initializes a new instance of . - /// - public ComponentStatePersistenceManager( - IOptions jsonOptions, - ILogger logger) - : this(jsonOptions.Value.SerializerOptions, logger) - { - } - - private ComponentStatePersistenceManager( - JsonSerializerOptions jsonSerializerOptions, - ILogger logger) - { - State = new PersistentComponentState(jsonSerializerOptions, _currentState, _registeredCallbacks); + State = new PersistentComponentState(_currentState, _registeredCallbacks); _logger = logger; } diff --git a/src/Components/Components/src/JsonOptions.cs b/src/Components/Components/src/JsonOptions.cs deleted file mode 100644 index d8b0ac51090b..000000000000 --- a/src/Components/Components/src/JsonOptions.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Components; - -/// -/// Options to configure JSON serialization settings for components. -/// -public sealed class JsonOptions -{ - /// - /// Gets the . - /// - public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultJsonSerializerOptions.Instance); -} diff --git a/src/Components/Components/src/JsonServiceCollectionExtensions.cs b/src/Components/Components/src/JsonServiceCollectionExtensions.cs deleted file mode 100644 index 29345a1cbf87..000000000000 --- a/src/Components/Components/src/JsonServiceCollectionExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Extension methods for configuring JSON options for components. -/// -public static class JsonServiceCollectionExtensions -{ - /// - /// Configures options used for serializing JSON in components functionality. - /// - /// The to configure options on. - /// The to configure the . - /// The modified . - public static IServiceCollection ConfigureComponentsJsonOptions(this IServiceCollection services, Action configureOptions) - { - services.Configure(configureOptions); - return services; - } -} diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 96bb61826f1b..ba20808b6a79 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -15,7 +15,8 @@ - + + diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index d285144ba200..ff6de37b09ec 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -14,15 +15,14 @@ public class PersistentComponentState { private IDictionary? _existingState; private readonly IDictionary _currentState; - private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly List _registeredCallbacks; + private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache = new(JsonSerializerOptionsProvider.Options); internal PersistentComponentState( - JsonSerializerOptions jsonSerializerOptions, - IDictionary currentState, + IDictionary currentState, List pauseCallbacks) { - _jsonSerializerOptions = jsonSerializerOptions; _currentState = currentState; _registeredCallbacks = pauseCallbacks; } @@ -86,7 +86,7 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call throw new ArgumentException($"There is already a persisted object under the same key '{key}'"); } - _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, _jsonSerializerOptions)); + _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options)); } /// @@ -106,7 +106,34 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call if (TryTake(key, out var data)) { var reader = new Utf8JsonReader(data); - instance = JsonSerializer.Deserialize(ref reader, _jsonSerializerOptions)!; + instance = JsonSerializer.Deserialize(ref reader, JsonSerializerOptionsProvider.Options)!; + return true; + } + else + { + instance = default; + return false; + } + } + + /// + /// Tries to retrieve the persisted state as JSON with the given and deserializes it into an + /// instance of type . + /// + /// The key used to persist the instance. + /// The to use when deserializing from JSON. + /// The persisted instance. + /// true if the state was found; false otherwise. + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + public bool TryTakeFromJson(string key, IJsonTypeInfoResolver resolver, [MaybeNullWhen(false)] out TValue? instance) + { + ArgumentNullException.ThrowIfNull(key); + + if (TryTake(key, out var data)) + { + var reader = new Utf8JsonReader(data); + var options = _jsonSerializerOptionsCache.GetOrAdd(resolver); + instance = JsonSerializer.Deserialize(ref reader, options)!; return true; } else diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 1d8ad6476c94..e111812b4a50 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,7 +1,2 @@ #nullable enable -Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Options.IOptions! jsonOptions, Microsoft.Extensions.Logging.ILogger! logger) -> void -Microsoft.AspNetCore.Components.JsonOptions -Microsoft.AspNetCore.Components.JsonOptions.JsonOptions() -> void -Microsoft.AspNetCore.Components.JsonOptions.SerializerOptions.get -> System.Text.Json.JsonSerializerOptions! -Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions.ConfigureComponentsJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(string! key, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, out TValue? instance) -> bool diff --git a/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs b/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs index 0d4ba5efa728..bed42d42dfbf 100644 --- a/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs +++ b/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs @@ -11,7 +11,7 @@ public class ComponentApplicationStateTest public void InitializeExistingState_SetupsState() { // Arrange - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); + var applicationState = new PersistentComponentState(new Dictionary(), new List()); var existingState = new Dictionary { ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 }) @@ -29,7 +29,7 @@ public void InitializeExistingState_SetupsState() public void InitializeExistingState_ThrowsIfAlreadyInitialized() { // Arrange - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); + var applicationState = new PersistentComponentState(new Dictionary(), new List()); var existingState = new Dictionary { ["MyState"] = new byte[] { 1, 2, 3, 4 } @@ -45,7 +45,7 @@ public void InitializeExistingState_ThrowsIfAlreadyInitialized() public void TryRetrieveState_ReturnsStateWhenItExists() { // Arrange - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); + var applicationState = new PersistentComponentState(new Dictionary(), new List()); var existingState = new Dictionary { ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 }) @@ -65,7 +65,7 @@ public void PersistState_SavesDataToTheStoreAsync() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) + var applicationState = new PersistentComponentState(currentState, new List()) { PersistingState = true }; @@ -84,7 +84,7 @@ public void PersistState_ThrowsForDuplicateKeys() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) + var applicationState = new PersistentComponentState(currentState, new List()) { PersistingState = true }; @@ -101,7 +101,7 @@ public void PersistAsJson_SerializesTheDataToJsonAsync() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) + var applicationState = new PersistentComponentState(currentState, new List()) { PersistingState = true }; @@ -120,7 +120,7 @@ public void PersistAsJson_NullValueAsync() { // Arrange var currentState = new Dictionary(); - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List()) + var applicationState = new PersistentComponentState(currentState, new List()) { PersistingState = true }; @@ -140,7 +140,7 @@ public void TryRetrieveFromJson_DeserializesTheDataFromJson() var myState = new byte[] { 1, 2, 3, 4 }; var serialized = JsonSerializer.SerializeToUtf8Bytes(myState); var existingState = new Dictionary() { ["MyState"] = serialized }; - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); + var applicationState = new PersistentComponentState(new Dictionary(), new List()); applicationState.InitializeExistingState(existingState); @@ -158,7 +158,7 @@ public void TryRetrieveFromJson_NullValue() // Arrange var serialized = JsonSerializer.SerializeToUtf8Bytes(null); var existingState = new Dictionary() { ["MyState"] = serialized }; - var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary(), new List()); + var applicationState = new PersistentComponentState(new Dictionary(), new List()); applicationState.InitializeExistingState(existingState); diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 0eeef0680ffa..34c76baada6f 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -40,7 +40,6 @@ internal partial class RemoteJSRuntime : JSRuntime public RemoteJSRuntime( IOptions circuitOptions, IOptions> componentHubOptions, - IOptions jsonOptions, ILogger logger) { _options = circuitOptions.Value; @@ -49,13 +48,6 @@ public RemoteJSRuntime( DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout; ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); - - JsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver.Instance); - - if (jsonOptions.Value.SerializerOptions is { TypeInfoResolver: { } typeInfoResolver }) - { - JsonSerializerOptions.TypeInfoResolverChain.Add(typeInfoResolver); - } } public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index b2c48b7cb5a5..bea90b2ec054 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -88,10 +87,6 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJavaScriptInitializersConfiguration>()); - // Configure JSON serializer options - services.ConfigureComponentsWebJsonOptions(); - services.ConfigureDefaultAntiforgeryJsonOptions(); - if (configure != null) { services.Configure(configure); diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index c82f21277938..260e60ac5efb 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -59,15 +59,11 @@ - - - - - + diff --git a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs index d71ec2a608e4..1903de5feb83 100644 --- a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs +++ b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs @@ -16,7 +16,6 @@ public abstract class ProtectedBrowserStorage private readonly string _storeName; private readonly IJSRuntime _jsRuntime; private readonly IDataProtectionProvider _dataProtectionProvider; - private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly ConcurrentDictionary _cachedDataProtectorsByPurpose = new ConcurrentDictionary(StringComparer.Ordinal); @@ -26,12 +25,7 @@ private readonly ConcurrentDictionary _cachedDataProtect /// The name of the store in which the data should be stored. /// The . /// The . - /// The . - private protected ProtectedBrowserStorage( - string storeName, - IJSRuntime jsRuntime, - IDataProtectionProvider dataProtectionProvider, - JsonSerializerOptions? jsonSerializerOptions) + private protected ProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) { // Performing data protection on the client would give users a false sense of security, so we'll prevent this. if (OperatingSystem.IsBrowser()) @@ -44,7 +38,6 @@ private protected ProtectedBrowserStorage( _storeName = storeName; _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); _dataProtectionProvider = dataProtectionProvider ?? throw new ArgumentNullException(nameof(dataProtectionProvider)); - _jsonSerializerOptions = jsonSerializerOptions ?? DefaultJsonSerializerOptions.Instance; } /// @@ -129,7 +122,7 @@ public ValueTask DeleteAsync(string key) private string Protect(string purpose, object value) { - var json = JsonSerializer.Serialize(value, options: _jsonSerializerOptions); + var json = JsonSerializer.Serialize(value, options: JsonSerializerOptionsProvider.Options); var protector = GetOrCreateCachedProtector(purpose); return protector.Protect(json); @@ -140,7 +133,7 @@ private TValue Unprotect(string purpose, string protectedJson) var protector = GetOrCreateCachedProtector(purpose); var json = protector.Unprotect(protectedJson); - return JsonSerializer.Deserialize(json, options: _jsonSerializerOptions)!; + return JsonSerializer.Deserialize(json, options: JsonSerializerOptionsProvider.Options)!; } private ValueTask SetProtectedJsonAsync(string key, string protectedJson) diff --git a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs index 67d8ed803929..eca79fde0c09 100644 --- a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs +++ b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs @@ -1,9 +1,7 @@ // 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 Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Options; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @@ -25,29 +23,7 @@ public sealed class ProtectedLocalStorage : ProtectedBrowserStorage /// The . /// The . public ProtectedLocalStorage(IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) - : this(jsRuntime, dataProtectionProvider, jsonSerializerOptions: null) - { - } - - /// - /// Constructs an instance of . - /// - /// The . - /// The . - /// The . - public ProtectedLocalStorage( - IJSRuntime jsRuntime, - IDataProtectionProvider dataProtectionProvider, - IOptions jsonOptions) - : this(jsRuntime, dataProtectionProvider, jsonOptions.Value.SerializerOptions) - { - } - - private ProtectedLocalStorage( - IJSRuntime jsRuntime, - IDataProtectionProvider dataProtectionProvider, - JsonSerializerOptions? jsonSerializerOptions) - : base("localStorage", jsRuntime, dataProtectionProvider, jsonSerializerOptions) + : base("localStorage", jsRuntime, dataProtectionProvider) { } } diff --git a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs index af213952c462..4a6d395931aa 100644 --- a/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs +++ b/src/Components/Server/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs @@ -1,9 +1,7 @@ // 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 Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Options; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @@ -25,29 +23,7 @@ public sealed class ProtectedSessionStorage : ProtectedBrowserStorage /// The . /// The . public ProtectedSessionStorage(IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) - : this(jsRuntime, dataProtectionProvider, jsonSerializerOptions: null) - { - } - - /// - /// Constructs an instance of . - /// - /// The . - /// The . - /// The . - public ProtectedSessionStorage( - IJSRuntime jsRuntime, - IDataProtectionProvider dataProtectionProvider, - IOptions jsonOptions) - : this(jsRuntime, dataProtectionProvider, jsonOptions.Value.SerializerOptions) - { - } - - private ProtectedSessionStorage( - IJSRuntime jsRuntime, - IDataProtectionProvider dataProtectionProvider, - JsonSerializerOptions? jsonSerializerOptions) - : base("sessionStorage", jsRuntime, dataProtectionProvider, jsonSerializerOptions) + : base("sessionStorage", jsRuntime, dataProtectionProvider) { } } diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index ebffe4283669..9d240138aaac 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedLocalStorage.ProtectedLocalStorage(Microsoft.JSInterop.IJSRuntime! jsRuntime, Microsoft.AspNetCore.DataProtection.IDataProtectionProvider! dataProtectionProvider, Microsoft.Extensions.Options.IOptions! jsonOptions) -> void -Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedSessionStorage.ProtectedSessionStorage(Microsoft.JSInterop.IJSRuntime! jsRuntime, Microsoft.AspNetCore.DataProtection.IDataProtectionProvider! dataProtectionProvider, Microsoft.Extensions.Options.IOptions! jsonOptions) -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebSocketAcceptContext.get -> System.Func? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebSocketAcceptContext.set -> void diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 9f9993cbc6b2..b1d2e5fe0a4d 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -726,7 +726,7 @@ protected override void Dispose(bool disposing) } private static RemoteJSRuntime CreateJSRuntime(CircuitOptions options) - => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), null); + => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions()), null); } private class DispatcherComponent : ComponentBase, IDisposable diff --git a/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs b/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs index ad254df37ea7..dac28c3acea4 100644 --- a/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs +++ b/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits; public class RemoteJSDataStreamTest { - private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); [Fact] public async Task CreateRemoteJSDataStreamAsync_CreatesStream() @@ -45,7 +45,7 @@ public async Task ReceiveData_DoesNotFindStream() public async Task ReceiveData_SuccessReadsBackStream() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[100]; @@ -73,7 +73,7 @@ public async Task ReceiveData_SuccessReadsBackStream() public async Task ReceiveData_SuccessReadsBackPipeReader() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[100]; @@ -101,7 +101,7 @@ public async Task ReceiveData_SuccessReadsBackPipeReader() public async Task ReceiveData_WithError() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); @@ -119,7 +119,7 @@ public async Task ReceiveData_WithError() public async Task ReceiveData_WithZeroLengthChunk() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = Array.Empty(); @@ -138,7 +138,7 @@ public async Task ReceiveData_WithZeroLengthChunk() public async Task ReceiveData_WithLargerChunksThanPermitted() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[50_000]; // more than the 32k maximum chunk size @@ -157,7 +157,7 @@ public async Task ReceiveData_WithLargerChunksThanPermitted() public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var jsStreamReference = Mock.Of(); var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); @@ -177,7 +177,7 @@ public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining() public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDisconnect() { // Arrange - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var jsStreamReference = Mock.Of(); var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); @@ -202,7 +202,7 @@ public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed() { // Arrange var unhandledExceptionRaisedTask = new TaskCompletionSource(); - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); jsRuntime.UnhandledException += (_, ex) => { Assert.Equal("Did not receive any data in the allotted time.", ex.Message); @@ -243,7 +243,7 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed() { // Arrange var unhandledExceptionRaisedTask = new TaskCompletionSource(); - var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); jsRuntime.UnhandledException += (_, ex) => { Assert.Equal("Did not receive any data in the allotted time.", ex.Message); @@ -299,12 +299,7 @@ private static long GetStreamId(RemoteJSDataStream stream, RemoteJSRuntime runti class TestRemoteJSRuntime : RemoteJSRuntime, IJSRuntime { - public TestRemoteJSRuntime( - IOptions circuitOptions, - IOptions> hubOptions, - IOptions jsonOptions, - ILogger logger) - : base(circuitOptions, hubOptions, jsonOptions, logger) + public TestRemoteJSRuntime(IOptions circuitOptions, IOptions> hubOptions, ILogger logger) : base(circuitOptions, hubOptions, logger) { } diff --git a/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs b/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs index 5ac490ab42dc..9b5673d5c973 100644 --- a/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs +++ b/src/Components/Server/test/Circuits/RemoteJSRuntimeTest.cs @@ -101,22 +101,13 @@ private static TestRemoteJSRuntime CreateTestRemoteJSRuntime(long? componentHubM { var componentHubOptions = Options.Create(new HubOptions()); componentHubOptions.Value.MaximumReceiveMessageSize = componentHubMaximumIncomingBytes; - var jsRuntime = new TestRemoteJSRuntime( - Options.Create(new CircuitOptions()), - componentHubOptions, - Options.Create(new JsonOptions()), - Mock.Of>()); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), componentHubOptions, Mock.Of>()); return jsRuntime; } class TestRemoteJSRuntime : RemoteJSRuntime, IJSRuntime { - public TestRemoteJSRuntime( - IOptions circuitOptions, - IOptions> hubOptions, - IOptions jsonOptions, - ILogger logger) - : base(circuitOptions, hubOptions, jsonOptions, logger) + public TestRemoteJSRuntime(IOptions circuitOptions, IOptions> hubOptions, ILogger logger) : base(circuitOptions, hubOptions, logger) { } diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index 896dfb5827dc..9ca641979610 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -721,7 +721,7 @@ protected override void AttachRootComponentToBrowser(int componentId, string dom } private static RemoteJSRuntime CreateJSRuntime(CircuitOptions options) - => new(Options.Create(options), Options.Create(new HubOptions()), Options.Create(new JsonOptions()), null); + => new RemoteJSRuntime(Options.Create(options), Options.Create(new HubOptions()), null); } private class TestComponent : IComponent, IHandleAfterRender diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index ec5b4929ec15..c0b09cb45189 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -28,11 +28,7 @@ public static CircuitHost Create( { serviceScope = serviceScope ?? new AsyncServiceScope(Mock.Of()); clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of(), Guid.NewGuid().ToString()); - var jsRuntime = new RemoteJSRuntime( - Options.Create(new CircuitOptions()), - Options.Create(new HubOptions()), - Options.Create(new JsonOptions()), - Mock.Of>()); + var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var navigationManager = new RemoteNavigationManager(Mock.Of>()); var serviceProvider = new Mock(); serviceProvider diff --git a/src/Components/Server/test/ProtectedBrowserStorageTest.cs b/src/Components/Server/test/ProtectedBrowserStorageTest.cs index 159bf71c9901..89da1b351874 100644 --- a/src/Components/Server/test/ProtectedBrowserStorageTest.cs +++ b/src/Components/Server/test/ProtectedBrowserStorageTest.cs @@ -370,7 +370,7 @@ public ValueTask InvokeAsync(string identifier, object[] args) class TestProtectedBrowserStorage : ProtectedBrowserStorage { public TestProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider) - : base(storeName, jsRuntime, dataProtectionProvider, jsonSerializerOptions: null) + : base(storeName, jsRuntime, dataProtectionProvider) { } } diff --git a/src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs b/src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs deleted file mode 100644 index 11366d032702..000000000000 --- a/src/Components/Shared/src/DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components.Forms; - -namespace Microsoft.Extensions.DependencyInjection; - -internal static class DefaultAntiforgeryJsonOptionsServiceCollectionExtensions -{ - public static IServiceCollection ConfigureDefaultAntiforgeryJsonOptions(this IServiceCollection services) - { - services.ConfigureComponentsJsonOptions(static options => - { - options.SerializerOptions.TypeInfoResolverChain.Insert(0, DefaultAntiforgeryStateProviderJsonSerializerContext.Default); - }); - - return services; - } -} diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 8460fdb687fc..9c12d3b42458 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -29,10 +29,9 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) return Task.CompletedTask; }, RenderMode.InteractiveAuto); - // The argument type should be kept in sync with - // DefaultAntiforgeryStateProviderJsonSerializerContext state.TryTakeFromJson( PersistenceKey, + DefaultAntiforgeryStateProviderSerializerContext.Default, out _currentToken); } @@ -44,4 +43,4 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) } [JsonSerializable(typeof(AntiforgeryRequestToken))] -internal sealed partial class DefaultAntiforgeryStateProviderJsonSerializerContext : JsonSerializerContext; +internal sealed partial class DefaultAntiforgeryStateProviderSerializerContext : JsonSerializerContext; diff --git a/src/Components/Shared/src/DefaultJsonSerializerOptions.cs b/src/Components/Shared/src/DefaultJsonSerializerOptions.cs deleted file mode 100644 index e5eb0f0b3789..000000000000 --- a/src/Components/Shared/src/DefaultJsonSerializerOptions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.AspNetCore.Components; - -internal static class DefaultJsonSerializerOptions -{ - public static readonly JsonSerializerOptions Instance = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeInfoResolver() : JsonTypeInfoResolver.Combine(), - }; - - static DefaultJsonSerializerOptions() - { - Instance.MakeReadOnly(); - } - - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This method only gets called when reflection is enabled for JsonSerializer")] - static DefaultJsonTypeInfoResolver CreateDefaultTypeInfoResolver() - => new(); -} diff --git a/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs b/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs deleted file mode 100644 index 88753913226e..000000000000 --- a/src/Components/Shared/src/JsonSerialization/JSRuntimeSerializerContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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.Serialization; - -namespace Microsoft.AspNetCore.Components; - -// JS interop argument lists are always object arrays -[JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] -internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs deleted file mode 100644 index 1a243e565347..000000000000 --- a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -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 -{ - private static readonly MethodInfo _createValueInfoMethod = ((Delegate)JsonMetadataServices.CreateValueInfo).Method.GetGenericMethodDefinition(); - - public static readonly JsonConverterFactoryTypeInfoResolver Instance = new(); - - [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "We expect the incoming type to have already been correctly preserved")] - public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) - { - foreach (var converter in options.Converters) - { - if (converter is not JsonConverterFactory factory || !factory.CanConvert(type)) - { - continue; - } - - var converterToUse = factory.CreateConverter(type, options); - var createValueInfo = _createValueInfoMethod.MakeGenericMethod(type); - - if (createValueInfo.Invoke(null, [options, converterToUse]) is not JsonTypeInfo jsonTypeInfo) - { - throw new InvalidOperationException($"Unable to create a {nameof(JsonTypeInfo)} for the type {type.FullName}"); - } - - return jsonTypeInfo; - } - - return null; - } -} diff --git a/src/Components/Shared/src/JsonSerializerOptionsCache.cs b/src/Components/Shared/src/JsonSerializerOptionsCache.cs new file mode 100644 index 000000000000..b3a8f6b3eddd --- /dev/null +++ b/src/Components/Shared/src/JsonSerializerOptionsCache.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Reflection.Metadata; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +[assembly: MetadataUpdateHandler(typeof(JsonSerializerOptionsCache.MetadataUpdateHandler))] + +internal sealed class JsonSerializerOptionsCache +{ + private readonly JsonSerializerOptions _baseOptions; + + // We expect JSON type info resolvers to be long-lived objects in most cases. This is because they'll + // typically be generated by the JSON source generator and referenced via generated static properties. + // Therefore, we shouldn't need to worry about type info resolvers not getting GC'd due to referencing + // them here. + private readonly ConcurrentDictionary _cachedSerializerOptions = []; + + public JsonSerializerOptionsCache(JsonSerializerOptions baseOptions) + { + _baseOptions = baseOptions; + + if (MetadataUpdater.IsSupported) + { + TrackInstance(this); + } + } + + public JsonSerializerOptions GetOrAdd( + IJsonTypeInfoResolver? resolver, + Func? valueFactory = null) + { + if (resolver is null) + { + return _baseOptions; + } + + return _cachedSerializerOptions.GetOrAdd(resolver, static (resolver, args) => + { + if (args.valueFactory is not null) + { + resolver = args.valueFactory(resolver); + } + + return new JsonSerializerOptions(args.cache._baseOptions) + { + TypeInfoResolver = resolver, + }; + }, (cache: this, valueFactory)); + } + + private static void TrackInstance(JsonSerializerOptionsCache instance) + => TrackedJsonSerializerOptionsCaches.All.Add(instance, null); + + internal static class TrackedJsonSerializerOptionsCaches + { + // Tracks all live JSRuntime instances. All instances add themselves to this table in their + // constructor when hot reload is enabled. + public static readonly ConditionalWeakTable All = []; + } + + internal static class MetadataUpdateHandler + { + public static void ClearCache(Type[]? _) + { + foreach (var (cache, _) in TrackedJsonSerializerOptionsCaches.All) + { + cache._cachedSerializerOptions.Clear(); + } + } + } +} diff --git a/src/Components/Shared/src/JsonSerializerOptionsProvider.cs b/src/Components/Shared/src/JsonSerializerOptionsProvider.cs new file mode 100644 index 000000000000..b8b60805feed --- /dev/null +++ b/src/Components/Shared/src/JsonSerializerOptionsProvider.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Microsoft.AspNetCore.Components; + +internal static class JsonSerializerOptionsProvider +{ + public static readonly JsonSerializerOptions Options = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; +} diff --git a/src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs b/src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs deleted file mode 100644 index 332bd786d01e..000000000000 --- a/src/Components/Web/src/Infrastructure/JsonOptionsServiceCollectionExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Components.Web.Infrastructure; - -/// -/// Extension methods for configuring web-specific JSON options for components. -/// -public static class JsonOptionsServiceCollectionExtensions -{ - /// - /// Configures options used for serializing JSON in web-specific components functionality. - /// - /// The to configure options on. - /// The modified . - public static IServiceCollection ConfigureComponentsWebJsonOptions(this IServiceCollection services) - { - services.ConfigureComponentsJsonOptions(static options => - { - options.SerializerOptions.TypeInfoResolverChain.Insert(0, WebRendererJsonSerializerContext.Default); - }); - - return services; - } -} diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 704f0eca5edb..4befff3c6426 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,5 +1,3 @@ #nullable enable -Microsoft.AspNetCore.Components.Web.Infrastructure.JsonOptionsServiceCollectionExtensions Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.get -> bool Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.set -> void -static Microsoft.AspNetCore.Components.Web.Infrastructure.JsonOptionsServiceCollectionExtensions.ConfigureComponentsWebJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 5ee21f2becf8..52c162752825 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -42,11 +42,9 @@ 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(); - - // The arguments passed to the following invocation should be kept in sync - // with WebRendererJsonSerializerContext. jsRuntime.InvokeVoidAsync( "Blazor._internal.attachWebRendererInterop", + WebRendererSerializerContext.Default, _rendererId, _interopMethodsReference, jsComponentInterop.Configuration.JSComponentParametersByIdentifier, @@ -153,4 +151,4 @@ public void RemoveRootComponent(int componentId) [JsonSerializable(typeof(int))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary>))] -internal sealed partial class WebRendererJsonSerializerContext : JsonSerializerContext; +internal sealed partial class WebRendererSerializerContext : JsonSerializerContext; diff --git a/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs index 786fa87c22b1..72feafe3cecf 100644 --- a/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ChangeEventArgsReaderTest.cs @@ -62,7 +62,7 @@ public void Read_WithStringValue() private static JsonElement GetJsonElement(ChangeEventArgs args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs index 70032b269e0e..0c60d32ca3b5 100644 --- a/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ClipboardEventArgsReaderTest.cs @@ -26,7 +26,7 @@ public void Read_Works() private static JsonElement GetJsonElement(ClipboardEventArgs args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs index 65618e39a8cb..d5bf85c7df1e 100644 --- a/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/DragEventArgsReaderTest.cs @@ -80,7 +80,7 @@ private void AssertEqual(DataTransferItem[] expected, DataTransferItem[] actual) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs index 7fee1ada8a58..607329646a18 100644 --- a/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ErrorEventArgsReaderTest.cs @@ -35,7 +35,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs index cc1f93ef9b9d..19836e297d51 100644 --- a/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/FocusEventArgsReaderTest.cs @@ -27,7 +27,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs index 5aab91e7a99d..8cbafaf5a0c5 100644 --- a/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/KeyboardEventArgsReaderTest.cs @@ -45,7 +45,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs index 17f5e783b9cf..d0e72b84145c 100644 --- a/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/MouseEventArgsReaderTest.cs @@ -65,7 +65,7 @@ internal static void AssertEqual(MouseEventArgs expected, MouseEventArgs actual) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs index 5e4d56f22ad4..2c8caf0a575a 100644 --- a/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/PointerEventArgsReaderTest.cs @@ -57,7 +57,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs index 98921111dce0..a3a7b4e48955 100644 --- a/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/ProgressEventArgsReaderTest.cs @@ -32,7 +32,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs index c29358bf0524..ef500609de1f 100644 --- a/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/TouchEventArgsReaderTest.cs @@ -110,7 +110,7 @@ private void AssertEqual(TouchPoint expected, TouchPoint actual) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs b/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs index 7ae7ba6f8f88..b3b3d6d4be4e 100644 --- a/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs +++ b/src/Components/Web/test/WebEventData/WebEventDescriptorReaderTest.cs @@ -64,7 +64,7 @@ public void Read_WithBoolValue_Works(bool value) private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs b/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs index 304abcf76006..9ef83c742bde 100644 --- a/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs +++ b/src/Components/Web/test/WebEventData/WheelEventArgsReaderTest.cs @@ -50,7 +50,7 @@ public void Read_Works() private static JsonElement GetJsonElement(T args) { - var json = JsonSerializer.SerializeToUtf8Bytes(args, DefaultJsonSerializerOptions.Instance); + var json = JsonSerializer.SerializeToUtf8Bytes(args, JsonSerializerOptionsProvider.Options); var jsonReader = new Utf8JsonReader(json); var jsonElement = JsonElement.ParseValue(ref jsonReader); return jsonElement; diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs index c22d7745ed05..e20acfac4fab 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Text.Json; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -11,10 +12,12 @@ 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()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -24,7 +27,7 @@ public void CanResolve_AccessTokenProvider() [Fact] public void CanResolve_IRemoteAuthenticationService() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -34,7 +37,7 @@ public void CanResolve_IRemoteAuthenticationService() [Fact] public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -68,7 +71,7 @@ public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied() [Fact] public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; builder.Services.AddApiAuthorization(options => { @@ -95,7 +98,7 @@ public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce() [Fact] public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; builder.Services.AddApiAuthorization(options => calls++); @@ -121,7 +124,7 @@ public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration() [Fact] public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -144,7 +147,7 @@ public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfigurati [Fact] public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; builder.Services.AddApiAuthorization(options => calls++); @@ -170,7 +173,7 @@ public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfigurat [Fact] public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddApiAuthorization(); var host = builder.Build(); @@ -193,7 +196,7 @@ public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpC [Fact] public void ApiAuthorizationOptions_DefaultsCanBeOverriden() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddApiAuthorization(options => { options.AuthenticationPaths.LogInPath = "a"; @@ -244,7 +247,7 @@ public void ApiAuthorizationOptions_DefaultsCanBeOverriden() [Fact] public void OidcOptions_ConfigurationDefaultsGetApplied() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.Replace(ServiceDescriptor.Singleton()); builder.Services.AddOidcAuthentication(options => { }); var host = builder.Build(); @@ -283,7 +286,7 @@ public void OidcOptions_ConfigurationDefaultsGetApplied() [Fact] public void OidcOptions_DefaultsCanBeOverriden() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddOidcAuthentication(options => { options.AuthenticationPaths.LogInPath = "a"; @@ -345,7 +348,7 @@ public void OidcOptions_DefaultsCanBeOverriden() [Fact] public void AddOidc_ConfigurationGetsCalledOnce() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; builder.Services.AddOidcAuthentication(options => calls++); @@ -362,7 +365,7 @@ public void AddOidc_ConfigurationGetsCalledOnce() [Fact] public void AddOidc_CustomState_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; builder.Services.AddOidcAuthentication(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture)); @@ -384,7 +387,7 @@ public void AddOidc_CustomState_SetsUpConfiguration() [Fact] public void AddOidc_CustomStateAndAccount_SetsUpConfiguration() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; builder.Services.AddOidcAuthentication(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture)); @@ -406,7 +409,7 @@ public void AddOidc_CustomStateAndAccount_SetsUpConfiguration() [Fact] public void OidcProviderOptionsAndDependencies_NotResolvedFromRootScope() { - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); var calls = 0; diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index ca453378aa2d..01e51c2920f0 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -148,9 +147,8 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl using (cancellationToken.Register(() => tcs.TrySetResult())) { var loggerFactory = Services.GetRequiredService(); - var jsonOptions = Services.GetRequiredService>(); var jsComponentInterop = new JSComponentInterop(_rootComponents.JSComponents); - _renderer = new WebAssemblyRenderer(Services, loggerFactory, jsonOptions, jsComponentInterop); + _renderer = new WebAssemblyRenderer(Services, loggerFactory, jsComponentInterop); WebAssemblyNavigationManager.Instance.CreateLogger(loggerFactory); diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 5811091c7a6f..810b49ebb713 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -4,12 +4,12 @@ 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; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Rendering; using Microsoft.AspNetCore.Components.WebAssembly.Services; @@ -17,7 +17,6 @@ using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Microsoft.JSInterop; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -28,6 +27,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; @@ -49,7 +49,9 @@ 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); + var builder = new WebAssemblyHostBuilder( + InternalJSImportMethods.Instance, + DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions()); WebAssemblyCultureProvider.Initialize(); @@ -63,11 +65,14 @@ public static WebAssemblyHostBuilder CreateDefault(string[]? args = default) /// /// Creates an instance of with the minimal configuration. /// - internal WebAssemblyHostBuilder(IInternalJSImportMethods jsMethods) + internal WebAssemblyHostBuilder( + IInternalJSImportMethods jsMethods, + JsonSerializerOptions jsonOptions) { // 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(); @@ -293,11 +298,6 @@ public WebAssemblyHost Build() var services = _createServiceProvider(); var scope = services.GetRequiredService().CreateAsyncScope(); - // Provide JsonOptions to the JS runtime as quickly as possible to ensure that - // JSON options are configured before any JS interop calls occur. - var jsonOptions = scope.ServiceProvider.GetRequiredService>(); - DefaultWebAssemblyJSRuntime.Instance.SetJsonOptions(jsonOptions); - return new WebAssemblyHost(this, services, scope, _persistedState); } @@ -311,12 +311,6 @@ internal void InitializeDefaultServices() Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance)); Services.AddSingleton(_ => _rootComponentCache ?? new()); Services.AddSingleton(); - //Services.AddSingleton(static sp => - //{ - // var jsonOptions = sp.GetRequiredService>(); - // var logger = sp.GetRequiredService>(); - // return new(jsonOptions, logger); - //}); Services.AddSingleton(sp => sp.GetRequiredService().State); Services.AddSingleton(); Services.AddSingleton(); @@ -325,13 +319,5 @@ internal void InitializeDefaultServices() builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance)); }); Services.AddSupplyValueFromQueryProvider(); - - // Configure JSON serializer options - Services.ConfigureComponentsJsonOptions(jsonOptions => - { - jsonOptions.SerializerOptions.TypeInfoResolverChain.Insert(0, DefaultWebAssemblyJSRuntimeSerializerContext.Default); - }); - Services.ConfigureComponentsWebJsonOptions(); - Services.ConfigureDefaultAntiforgeryJsonOptions(); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj index 054b6d73ed0d..896817768457 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj +++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj @@ -27,6 +27,7 @@ + @@ -36,15 +37,12 @@ - - - diff --git a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs index 694caa52bca9..08bd539f9984 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -83,14 +83,12 @@ public ParameterView DeserializeParameters(IList parametersD [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The correct members will be preserved by the above DynamicDependency.")] public static ComponentParameter[] GetParameterDefinitions(string parametersDefinitions) { - // Keep in sync with WebAssemblyComponentParameterDeserializerSerializerContext return JsonSerializer.Deserialize(parametersDefinitions, _jsonSerializerOptions)!; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to preserve component parameter types.")] public static IList GetParameterValues(string parameterValues) { - // Keep in sync with WebAssemblyComponentParameterDeserializerSerializerContext return JsonSerializer.Deserialize>(parameterValues, _jsonSerializerOptions)!; } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 3a09ae07a040..a2297cb2f8b7 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering; @@ -26,12 +25,8 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer private readonly Dispatcher _dispatcher; private readonly IInternalJSImportMethods _jsMethods; - public WebAssemblyRenderer( - IServiceProvider serviceProvider, - ILoggerFactory loggerFactory, - IOptions jsonOptions, - JSComponentInterop jsComponentInterop) - : base(serviceProvider, loggerFactory, jsonOptions.Value.SerializerOptions, jsComponentInterop) + public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) + : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop) { _logger = loggerFactory.CreateLogger(); _jsMethods = serviceProvider.GetRequiredService(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 7b4e4ea396d2..3446e35a76ba 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -8,7 +8,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.Extensions.Options; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; using Microsoft.JSInterop.WebAssembly; @@ -18,12 +17,15 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services; internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime { + private static readonly JsonSerializerOptions _rootComponentSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) + { + TypeInfoResolver = DefaultWebAssemblyJSRuntimeSerializerContext.Default, + }; + public static readonly DefaultWebAssemblyJSRuntime Instance = new(); private readonly RootComponentTypeCache _rootComponentCache = new(); - private JsonSerializerOptions? _rootComponentSerializerOptions; - public ElementReferenceContext ElementReferenceContext { get; } public event Action? OnUpdateRootComponents; @@ -37,24 +39,9 @@ private DefaultWebAssemblyJSRuntime() { ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); - JsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver.Instance); } - public void SetJsonOptions(IOptions jsonOptions) - { - if (JsonSerializerOptions.IsReadOnly) - { - throw new InvalidOperationException( - "JSON options must be provided to the JS runtime before the it gets used."); - } - - _rootComponentSerializerOptions = jsonOptions.Value.SerializerOptions; - - if (jsonOptions.Value.SerializerOptions is { TypeInfoResolver: { } typeInfoResolver }) - { - JsonSerializerOptions.TypeInfoResolverChain.Add(typeInfoResolver); - } - } + public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; [JSExport] [SupportedOSPlatform("browser")] @@ -130,12 +117,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) { - // The type we're serializing should be kept in sync with - // DefaultWebAssemblyJSRuntimeSerializerContext var deserialized = JsonSerializer.Deserialize( operationsJson, - Instance._rootComponentSerializerOptions ?? - DefaultWebAssemblyJSRuntimeSerializerContext.Default.Options)!; + _rootComponentSerializerOptions)!; for (var i = 0; i < deserialized.Operations.Length; i++) { @@ -159,7 +143,7 @@ internal static RootComponentOperationBatch DeserializeOperations(string operati return deserialized; } - private static WebRootComponentParameters DeserializeComponentParameters(ComponentMarker marker) + static WebRootComponentParameters DeserializeComponentParameters(ComponentMarker marker) { var definitions = WebAssemblyComponentParameterDeserializer.GetParameterDefinitions(marker.ParameterDefinitions!); var values = WebAssemblyComponentParameterDeserializer.GetParameterValues(marker.ParameterValues!); @@ -187,9 +171,5 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference } } -[JsonSourceGenerationOptions( - PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(RootComponentOperationBatch))] internal sealed partial class DefaultWebAssemblyJSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs index 12b2602cd9d3..0e6f259e7c72 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using System.Text.Json; using Microsoft.AspNetCore.Components.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -13,11 +14,13 @@ 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()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Configuration.AddInMemoryCollection(new[] { @@ -35,7 +38,7 @@ public void Build_AllowsConfiguringConfiguration() public void Build_AllowsConfiguringServices() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); // This test also verifies that we create a scope. builder.Services.AddScoped(); @@ -51,7 +54,7 @@ public void Build_AllowsConfiguringServices() public void Build_AllowsConfiguringContainer() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddScoped(); var factory = new MyFakeServiceProviderFactory(); @@ -69,7 +72,7 @@ public void Build_AllowsConfiguringContainer() public void Build_AllowsConfiguringContainer_WithDelegate() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddScoped(); @@ -92,7 +95,7 @@ public void Build_AllowsConfiguringContainer_WithDelegate() public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -109,7 +112,7 @@ public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation public void Build_InProduction_ConfiguresWithServiceProviderWithScopeValidation() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -126,7 +129,7 @@ public void Build_InProduction_ConfiguresWithServiceProviderWithScopeValidation( public void Builder_InDevelopment_SetsHostEnvironmentProperty() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions); // Assert Assert.NotNull(builder.HostEnvironment); @@ -137,7 +140,7 @@ public void Builder_InDevelopment_SetsHostEnvironmentProperty() public void Builder_CreatesNavigationManager() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions); // Act var host = builder.Build(); @@ -187,7 +190,7 @@ public IServiceProvider CreateServiceProvider(MyFakeDIBuilderThing containerBuil public void Build_AddsConfigurationToServices() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Configuration.AddInMemoryCollection(new[] { @@ -222,7 +225,7 @@ private static IReadOnlyList DefaultServiceTypes public void Constructor_AddsDefaultServices() { // Arrange & Act - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); foreach (var type in DefaultServiceTypes) { @@ -234,7 +237,7 @@ public void Constructor_AddsDefaultServices() public void Builder_SupportsConfiguringLogging() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); 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 6d68c3fe307f..587d3b626b31 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Text.Json; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; @@ -11,13 +12,15 @@ 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()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddSingleton(Mock.Of()); var host = builder.Build(); var cultureProvider = new TestSatelliteResourcesLoader(); @@ -37,7 +40,7 @@ public async Task RunAsync_CanExitBasedOnCancellationToken() public async Task RunAsync_CallingTwiceCausesException() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddSingleton(Mock.Of()); var host = builder.Build(); var cultureProvider = new TestSatelliteResourcesLoader(); @@ -59,7 +62,7 @@ public async Task RunAsync_CallingTwiceCausesException() public async Task DisposeAsync_CanDisposeAfterCallingRunAsync() { // Arrange - var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions); builder.Services.AddSingleton(Mock.Of()); builder.Services.AddSingleton(); var host = builder.Build(); diff --git a/src/Components/WebView/WebView/src/IpcCommon.cs b/src/Components/WebView/WebView/src/IpcCommon.cs index 6176bc3dd63b..e81b62ac8a34 100644 --- a/src/Components/WebView/WebView/src/IpcCommon.cs +++ b/src/Components/WebView/WebView/src/IpcCommon.cs @@ -31,7 +31,7 @@ private static string Serialize(string messageType, object[] args) // Note we do NOT need the JSRuntime specific JsonSerializerOptions as the args needing special handling // (JS/DotNetObjectReference & Byte Arrays) have already been serialized earlier in the JSRuntime. // We run the serialization here to add the `messageType`. - return $"{_ipcMessagePrefix}{JsonSerializer.Serialize(messageTypeAndArgs, DefaultJsonSerializerOptions.Instance)}"; + return $"{_ipcMessagePrefix}{JsonSerializer.Serialize(messageTypeAndArgs, JsonSerializerOptionsProvider.Options)}"; } private static bool TryDeserialize(string message, out T messageType, out ArraySegment args) @@ -41,7 +41,7 @@ private static bool TryDeserialize(string message, out T messageType, out Arr if (message != null && message.StartsWith(_ipcMessagePrefix, StringComparison.Ordinal)) { var messageAfterPrefix = message.AsSpan(_ipcMessagePrefix.Length); - var parsed = (JsonElement[])JsonSerializer.Deserialize(messageAfterPrefix, typeof(JsonElement[]), DefaultJsonSerializerOptions.Instance); + var parsed = (JsonElement[])JsonSerializer.Deserialize(messageAfterPrefix, typeof(JsonElement[]), JsonSerializerOptionsProvider.Options); messageType = (T)Enum.Parse(typeof(T), parsed[0].GetString()); args = new ArraySegment(parsed, 1, parsed.Length - 1); return true; diff --git a/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj b/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj index 067a9e878d5b..c669218e36f3 100644 --- a/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj +++ b/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj @@ -24,13 +24,13 @@ + - diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index 19b48ff20d58..a336a1090ced 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -11,8 +11,6 @@ true - - true diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 3f88e2defa5a..4b147896e621 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Net.Http; -using System.Runtime.InteropServices.JavaScript; using System.Web; using BasicTestApp.AuthTest; using BasicTestApp.PropertyInjection; @@ -17,7 +16,7 @@ namespace BasicTestApp; -public partial class Program +public class Program { public static async Task Main(string[] args) { @@ -83,10 +82,10 @@ private static void ConfigureCulture(WebAssemblyHost host) CultureInfo.DefaultThreadCurrentUICulture = culture; } - //Supports E2E tests in StartupErrorNotificationTest + // Supports E2E tests in StartupErrorNotificationTest private static async Task SimulateErrorsIfNeededForTest() { - var currentUrl = JSFunctions.GetCurrentUrl(); + var currentUrl = DefaultWebAssemblyJSRuntime.Instance.Invoke("getCurrentUrl"); if (currentUrl.Contains("error=sync")) { throw new InvalidTimeZoneException("This is a synchronous startup exception"); @@ -99,10 +98,4 @@ private static async Task SimulateErrorsIfNeededForTest() throw new InvalidTimeZoneException("This is an asynchronous startup exception"); } } - - private static partial class JSFunctions - { - [JSImport("globalThis.getCurrentUrl")] - public static partial string GetCurrentUrl(); - } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs index ea03292f91a1..e13ba2d3cadd 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs @@ -1,12 +1,16 @@ // 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; + namespace Microsoft.JSInterop; internal interface IJSInteropTask : IDisposable { public Type ResultType { get; } + public JsonSerializerOptions? DeserializeOptions { get; set; } + void SetResult(object? result); void SetException(Exception exception); diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 0313bf613470..1484c983d79b 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -24,6 +25,21 @@ public interface IJSRuntime /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args); + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, + /// consider using . + /// + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) + => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -36,4 +52,19 @@ public interface IJSRuntime /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args); + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) + => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs index 253224889941..7271a4a10568 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Text.Json; namespace Microsoft.JSInterop; @@ -11,6 +12,8 @@ internal sealed class JSInteropTask : IJSInteropTask private readonly CancellationTokenRegistration _cancellationTokenRegistration; private readonly Action? _onCanceled; + public JsonSerializerOptions? DeserializeOptions { get; set; } + public Task Task => _tcs.Task; public Type ResultType => typeof(TResult); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 65ffc60d2836..eb3ac1359b26 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -4,8 +4,10 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text; +using System.Linq; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -20,6 +22,7 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" private readonly ConcurrentDictionary _pendingTasks = new(); private readonly ConcurrentDictionary _trackedRefsById = new(); + private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache; internal readonly ArrayBuilder ByteArraysToBeRevived = new(); @@ -28,20 +31,22 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable /// protected JSRuntime() { - JsonSerializerOptions = new() + JsonSerializerOptions = new JsonSerializerOptions { MaxDepth = 32, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, Converters = - { - new DotNetObjectReferenceJsonConverterFactory(this), - new JSObjectReferenceJsonConverter(this), - new JSStreamReferenceJsonConverter(this), - new DotNetStreamReferenceJsonConverter(this), - new ByteArrayJsonConverter(this), - }, + { + new DotNetObjectReferenceJsonConverterFactory(this), + new JSObjectReferenceJsonConverter(this), + new JSStreamReferenceJsonConverter(this), + new DotNetStreamReferenceJsonConverter(this), + new ByteArrayJsonConverter(this), + }, }; + + _jsonSerializerOptionsCache = new(JsonSerializerOptions); } /// @@ -58,26 +63,38 @@ protected JSRuntime() public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); + /// + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) + => InvokeAsync(0, identifier, resolver, args); + /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => InvokeAsync(0, identifier, cancellationToken, args); - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) + /// + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) + => InvokeAsync(0, identifier, resolver, cancellationToken, args); + + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, IJsonTypeInfoResolver? resolver, object?[]? args) { if (DefaultAsyncTimeout.HasValue) { using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value); // We need to await here due to the using - return await InvokeAsync(targetInstanceId, identifier, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, resolver, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, resolver, CancellationToken.None, args); } + internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) + => InvokeAsync(targetInstanceId, identifier, resolver: null, args); + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, + IJsonTypeInfoResolver? resolver, CancellationToken cancellationToken, object?[]? args) { @@ -97,13 +114,20 @@ protected JSRuntime() try { var resultType = JSCallResultTypeHelper.FromGeneric(); + var jsonSerializerOptions = _jsonSerializerOptionsCache.GetOrAdd(resolver, static resolver => + JsonTypeInfoResolver.Combine( + resolver, + JSRuntimeSerializerContext.Default, + FallbackTypeInfoResolver.Instance)); var argsJson = args switch { null or { Length: 0 } => null, - _ => SerializeArgs(args), + _ => JsonSerializer.Serialize(args, jsonSerializerOptions), }; + interopTask.DeserializeOptions = jsonSerializerOptions; + BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); return new ValueTask(interopTask.Task); @@ -114,40 +138,15 @@ protected JSRuntime() interopTask.Dispose(); throw; } - - string SerializeArgs(object?[] args) - { - Debug.Assert(args.Length > 0); - - var builder = new StringBuilder(); - builder.Append('['); - - WriteArg(args[0]); - - for (var i = 1; i < args.Length; i++) - { - builder.Append(','); - WriteArg(args[i]); - } - - builder.Append(']'); - return builder.ToString(); - - void WriteArg(object? arg) - { - if (arg is null) - { - builder.Append("null"); - } - else - { - var argJson = JsonSerializer.Serialize(arg, arg.GetType(), JsonSerializerOptions); - builder.Append(argJson); - } - } - } } + internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( + long targetInstanceId, + string identifier, + CancellationToken cancellationToken, + object?[]? args) + => InvokeAsync(targetInstanceId, identifier, resolver: null, cancellationToken, args); + /// /// Begins an asynchronous function invocation. /// @@ -249,7 +248,7 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe } else { - result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); + result = JsonSerializer.Deserialize(ref jsonReader, resultType, interopTask.DeserializeOptions); } ByteArraysToBeRevived.Clear(); @@ -348,3 +347,30 @@ internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) /// public void Dispose() => ByteArraysToBeRevived.Dispose(); } + +[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync")] +internal sealed class FallbackTypeInfoResolver : IJsonTypeInfoResolver +{ + private static readonly DefaultJsonTypeInfoResolver _defaultJsonTypeInfoResolver = new(); + + public static readonly FallbackTypeInfoResolver Instance = new(); + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (options.Converters.Any(c => c.CanConvert(type))) + { + // TODO: We should allow types with custom converters to be serialized without + // having to generate metadata for them. + // Question: Why do we even need a JsonTypeInfo if the type is going to be serialized + // with a custom converter anyway? We shouldn't need to perform any reflection here. + // Is it possible to generate a "minimal" JsonTypeInfo that just points to the correct + // converter? + return _defaultJsonTypeInfoResolver.GetTypeInfo(type, options); + } + + return null; + } +} + +[JsonSerializable(typeof(object[]))] // JS interop argument lists are always object arrays +internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 7cb7cd934374..71b7d2f7ea52 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -26,6 +27,21 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, args); } + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization. + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + await jsRuntime.InvokeAsync(identifier, resolver, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -44,6 +60,25 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -62,6 +97,25 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -81,6 +135,26 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return jsRuntime.InvokeAsync(identifier, args); } + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, + /// consider using . + /// + /// + /// The . + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + return jsRuntime.InvokeAsync(identifier, resolver, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -100,6 +174,26 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return jsRuntime.InvokeAsync(identifier, cancellationToken, args); } + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + return jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + } + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -117,4 +211,23 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The to use for JSON serialization and deserialization. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) + { + ArgumentNullException.ThrowIfNull(jsRuntime); + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + return await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj index baebaf34e3c8..8092928fcb03 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj @@ -18,6 +18,7 @@ + diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 3759e9ad0478..1a79f6cd0b27 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -9,3 +9,13 @@ *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 +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file From d758ef83bf4abb505ea5e4b9b87febf89d1c78be Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 16 Apr 2024 16:28:18 -0700 Subject: [PATCH 10/20] Improvements --- .../Microsoft.AspNetCore.Components.csproj | 1 - .../src/PersistentComponentState.cs | 29 ------- .../Components/src/PublicAPI.Unshipped.txt | 1 - .../src/DefaultAntiforgeryStateProvider.cs | 16 ++-- .../JsonConverterFactoryTypeInfoResolver.cs | 45 +++++++++++ .../Shared/src/JsonSerializerOptionsCache.cs | 75 ------------------- ...Microsoft.AspNetCore.Components.Web.csproj | 1 + src/Components/Web/src/WebRenderer.cs | 9 ++- .../RazorComponents/App.razor | 5 ++ .../Components.WasmMinimal.csproj | 3 + .../Microsoft.JSInterop/src/IJSRuntime.cs | 21 ++++-- .../Microsoft.JSInterop/src/JSRuntime.cs | 32 ++++---- .../src/JSRuntimeExtensions.cs | 37 ++++----- .../src/Microsoft.JSInterop.csproj | 1 - .../src/PublicAPI.Unshipped.txt | 22 +++--- 15 files changed, 132 insertions(+), 166 deletions(-) create mode 100644 src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs delete mode 100644 src/Components/Shared/src/JsonSerializerOptionsCache.cs diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index ba20808b6a79..9ebdbfc3778f 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index ff6de37b09ec..bc193dd77e5f 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -17,7 +16,6 @@ public class PersistentComponentState private readonly IDictionary _currentState; private readonly List _registeredCallbacks; - private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache = new(JsonSerializerOptionsProvider.Options); internal PersistentComponentState( IDictionary currentState, @@ -116,33 +114,6 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func call } } - /// - /// Tries to retrieve the persisted state as JSON with the given and deserializes it into an - /// instance of type . - /// - /// The key used to persist the instance. - /// The to use when deserializing from JSON. - /// The persisted instance. - /// true if the state was found; false otherwise. - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] - public bool TryTakeFromJson(string key, IJsonTypeInfoResolver resolver, [MaybeNullWhen(false)] out TValue? instance) - { - ArgumentNullException.ThrowIfNull(key); - - if (TryTake(key, out var data)) - { - var reader = new Utf8JsonReader(data); - var options = _jsonSerializerOptionsCache.GetOrAdd(resolver); - instance = JsonSerializer.Deserialize(ref reader, options)!; - return true; - } - else - { - instance = default; - return false; - } - } - private bool TryTake(string key, out byte[]? value) { ArgumentNullException.ThrowIfNull(key); diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index e111812b4a50..7dc5c58110bf 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(string! key, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, out TValue? instance) -> bool diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 9c12d3b42458..a40137fc0e57 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Web; @@ -25,14 +26,19 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) // don't have access to the request. _subscription = state.RegisterOnPersisting(() => { - state.PersistAsJson(PersistenceKey, GetAntiforgeryToken()); + var bytes = JsonSerializer.SerializeToUtf8Bytes( + GetAntiforgeryToken(), + DefaultAntiforgeryStateProviderSerializerContext.Default.AntiforgeryRequestToken); + state.PersistAsJson(PersistenceKey, bytes); return Task.CompletedTask; }, RenderMode.InteractiveAuto); - state.TryTakeFromJson( - PersistenceKey, - DefaultAntiforgeryStateProviderSerializerContext.Default, - out _currentToken); + if (state.TryTakeFromJson(PersistenceKey, out var bytes)) + { + _currentToken = JsonSerializer.Deserialize( + bytes, + DefaultAntiforgeryStateProviderSerializerContext.Default.AntiforgeryRequestToken); + } } /// 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/Shared/src/JsonSerializerOptionsCache.cs b/src/Components/Shared/src/JsonSerializerOptionsCache.cs deleted file mode 100644 index b3a8f6b3eddd..000000000000 --- a/src/Components/Shared/src/JsonSerializerOptionsCache.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Reflection.Metadata; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -[assembly: MetadataUpdateHandler(typeof(JsonSerializerOptionsCache.MetadataUpdateHandler))] - -internal sealed class JsonSerializerOptionsCache -{ - private readonly JsonSerializerOptions _baseOptions; - - // We expect JSON type info resolvers to be long-lived objects in most cases. This is because they'll - // typically be generated by the JSON source generator and referenced via generated static properties. - // Therefore, we shouldn't need to worry about type info resolvers not getting GC'd due to referencing - // them here. - private readonly ConcurrentDictionary _cachedSerializerOptions = []; - - public JsonSerializerOptionsCache(JsonSerializerOptions baseOptions) - { - _baseOptions = baseOptions; - - if (MetadataUpdater.IsSupported) - { - TrackInstance(this); - } - } - - public JsonSerializerOptions GetOrAdd( - IJsonTypeInfoResolver? resolver, - Func? valueFactory = null) - { - if (resolver is null) - { - return _baseOptions; - } - - return _cachedSerializerOptions.GetOrAdd(resolver, static (resolver, args) => - { - if (args.valueFactory is not null) - { - resolver = args.valueFactory(resolver); - } - - return new JsonSerializerOptions(args.cache._baseOptions) - { - TypeInfoResolver = resolver, - }; - }, (cache: this, valueFactory)); - } - - private static void TrackInstance(JsonSerializerOptionsCache instance) - => TrackedJsonSerializerOptionsCaches.All.Add(instance, null); - - internal static class TrackedJsonSerializerOptionsCaches - { - // Tracks all live JSRuntime instances. All instances add themselves to this table in their - // constructor when hot reload is enabled. - public static readonly ConditionalWeakTable All = []; - } - - internal static class MetadataUpdateHandler - { - public static void ClearCache(Type[]? _) - { - foreach (var (cache, _) in TrackedJsonSerializerOptionsCaches.All) - { - cache._cachedSerializerOptions.Clear(); - } - } - } -} diff --git a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj index 2b337152af34..9362dc20a8dc 100644 --- a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj +++ b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 52c162752825..463143d91d64 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -41,10 +41,15 @@ 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(); + var jsRuntimeJsonSerializerOptions = jsRuntime.CloneJsonSerializerOptions(); + jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, JsonConverterFactoryTypeInfoResolver>.Instance); + jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, WebRendererSerializerContext.Default); + jsRuntime.InvokeVoidAsync( "Blazor._internal.attachWebRendererInterop", - WebRendererSerializerContext.Default, + jsRuntimeJsonSerializerOptions, _rendererId, _interopMethodsReference, jsComponentInterop.Configuration.JSComponentParametersByIdentifier, @@ -148,6 +153,8 @@ public void RemoveRootComponent(int componentId) } } +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] +[JsonSerializable(typeof(object[]))] [JsonSerializable(typeof(int))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary>))] diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index befd36e52a53..08b02cad20ee 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -70,6 +70,11 @@ }); } }, + configureRuntime: (builder) => { + builder.withConfig({ + browserProfilerOptions: {}, + }); + }, }, }).then(() => { const startedParagraph = document.createElement('p'); diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index 3b5fd66e8188..188f62b80e6b 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -5,6 +5,9 @@ enable enable WasmMinimal + + browser; + true diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 1484c983d79b..ed4f91a72296 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization.Metadata; +using System.Text.Json; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -34,11 +34,11 @@ public interface IJSRuntime /// /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. + /// The to use for JSON serialization and deserialization. /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) - => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, object?[]? args) + => throw new InvalidOperationException($"Supplying a custom {nameof(JsonSerializerOptions)} is not supported by the current JS runtime"); /// /// Invokes the specified JavaScript function asynchronously. @@ -58,13 +58,20 @@ public interface IJSRuntime /// /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. + /// The to use for JSON serialization and deserialization. /// /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts /// () from being applied. /// /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) - => throw new InvalidOperationException($"Supplying a custom {nameof(IJsonTypeInfoResolver)} is not supported by the current JS runtime"); + ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, object?[]? args) + => throw new InvalidOperationException($"Supplying a custom {nameof(JsonSerializerOptions)} is not supported by the current JS runtime"); + + /// + /// Returns a copy of the current used for JSON serialization and deserialization. + /// + /// A copy of the . + JsonSerializerOptions CloneJsonSerializerOptions() + => throw new InvalidOperationException($"The current JS runtime does not support cloning {nameof(JsonSerializerOptions)}"); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index eb3ac1359b26..28d2d78de61d 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -22,7 +22,6 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" private readonly ConcurrentDictionary _pendingTasks = new(); private readonly ConcurrentDictionary _trackedRefsById = new(); - private readonly JsonSerializerOptionsCache _jsonSerializerOptionsCache; internal readonly ArrayBuilder ByteArraysToBeRevived = new(); @@ -45,8 +44,6 @@ protected JSRuntime() new ByteArrayJsonConverter(this), }, }; - - _jsonSerializerOptionsCache = new(JsonSerializerOptions); } /// @@ -59,42 +56,45 @@ protected JSRuntime() /// protected TimeSpan? DefaultAsyncTimeout { get; set; } + /// + public JsonSerializerOptions CloneJsonSerializerOptions() => new(JsonSerializerOptions); + /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, object?[]? args) - => InvokeAsync(0, identifier, resolver, args); + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, object?[]? args) + => InvokeAsync(0, identifier, options, args); /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => InvokeAsync(0, identifier, cancellationToken, args); /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, object?[]? args) - => InvokeAsync(0, identifier, resolver, cancellationToken, args); + public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, object?[]? args) + => InvokeAsync(0, identifier, options, cancellationToken, args); - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, IJsonTypeInfoResolver? resolver, object?[]? args) + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, JsonSerializerOptions? options, object?[]? args) { if (DefaultAsyncTimeout.HasValue) { using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value); // We need to await here due to the using - return await InvokeAsync(targetInstanceId, identifier, resolver, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, options, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, resolver, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, options, CancellationToken.None, args); } internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) - => InvokeAsync(targetInstanceId, identifier, resolver: null, args); + => InvokeAsync(targetInstanceId, identifier, options: null, args); [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, - IJsonTypeInfoResolver? resolver, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken, object?[]? args) { @@ -114,11 +114,7 @@ protected JSRuntime() try { var resultType = JSCallResultTypeHelper.FromGeneric(); - var jsonSerializerOptions = _jsonSerializerOptionsCache.GetOrAdd(resolver, static resolver => - JsonTypeInfoResolver.Combine( - resolver, - JSRuntimeSerializerContext.Default, - FallbackTypeInfoResolver.Instance)); + jsonSerializerOptions ??= JsonSerializerOptions; var argsJson = args switch { @@ -145,7 +141,7 @@ protected JSRuntime() string identifier, CancellationToken cancellationToken, object?[]? args) - => InvokeAsync(targetInstanceId, identifier, resolver: null, cancellationToken, args); + => InvokeAsync(targetInstanceId, identifier, jsonSerializerOptions: null, cancellationToken, args); /// /// Begins an asynchronous function invocation. diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 71b7d2f7ea52..315bb8118277 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -32,14 +33,14 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. + /// The to use for JSON serialization. /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - await jsRuntime.InvokeAsync(identifier, resolver, args); + await jsRuntime.InvokeAsync(identifier, options, args); } /// @@ -65,18 +66,18 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. + /// The to use for JSON serialization. /// /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts /// () from being applied. /// /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + await jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); } /// @@ -102,18 +103,18 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. + /// The to use for JSON serialization. /// The duration after which to cancel the async operation. Overrides default timeouts (). /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, TimeSpan timeout, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + await jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); } /// @@ -145,14 +146,14 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// The . /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. + /// The to use for JSON serialization and deserialization. /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, params object?[]? args) + public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - return jsRuntime.InvokeAsync(identifier, resolver, args); + return jsRuntime.InvokeAsync(identifier, options, args); } /// @@ -180,18 +181,18 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// The JSON-serializable return type. /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. + /// The to use for JSON serialization and deserialization. /// /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts /// () from being applied. /// /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, CancellationToken cancellationToken, params object?[]? args) + public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - return jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + return jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); } /// @@ -217,17 +218,17 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. + /// The to use for JSON serialization and deserialization. /// The duration after which to cancel the async operation. Overrides default timeouts (). /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, IJsonTypeInfoResolver resolver, TimeSpan timeout, params object?[]? args) + public static async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, TimeSpan timeout, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - return await jsRuntime.InvokeAsync(identifier, resolver, cancellationToken, args); + return await jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj index 8092928fcb03..baebaf34e3c8 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj @@ -18,7 +18,6 @@ - diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 1a79f6cd0b27..152241b61ae5 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -9,13 +9,15 @@ *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 -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver! resolver, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file +Microsoft.JSInterop.IJSRuntime.CloneJsonSerializerOptions() -> System.Text.Json.JsonSerializerOptions! +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.CloneJsonSerializerOptions() -> System.Text.Json.JsonSerializerOptions! +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file From 04910bbbb15f78e245be8bfdec6a3edea32518fd Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 16 Apr 2024 17:12:05 -0700 Subject: [PATCH 11/20] Cleanup --- .../src/DefaultAntiforgeryStateProvider.cs | 6 +-- .../Services/DefaultWebAssemblyJSRuntime.cs | 4 +- .../RazorComponents/App.razor | 5 --- .../Components.WasmMinimal.csproj | 3 -- .../Microsoft.JSInterop/src/JSRuntime.cs | 43 ++----------------- 5 files changed, 8 insertions(+), 53 deletions(-) diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index a40137fc0e57..4aff2437d076 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -28,7 +28,7 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) { var bytes = JsonSerializer.SerializeToUtf8Bytes( GetAntiforgeryToken(), - DefaultAntiforgeryStateProviderSerializerContext.Default.AntiforgeryRequestToken); + DefaultAntiforgeryStateProviderJsonSerializerContext.Default.AntiforgeryRequestToken); state.PersistAsJson(PersistenceKey, bytes); return Task.CompletedTask; }, RenderMode.InteractiveAuto); @@ -37,7 +37,7 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) { _currentToken = JsonSerializer.Deserialize( bytes, - DefaultAntiforgeryStateProviderSerializerContext.Default.AntiforgeryRequestToken); + DefaultAntiforgeryStateProviderJsonSerializerContext.Default.AntiforgeryRequestToken); } } @@ -49,4 +49,4 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) } [JsonSerializable(typeof(AntiforgeryRequestToken))] -internal sealed partial class DefaultAntiforgeryStateProviderSerializerContext : JsonSerializerContext; +internal sealed partial class DefaultAntiforgeryStateProviderJsonSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 3446e35a76ba..0d8481ff71cb 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -19,7 +19,7 @@ internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime { private static readonly JsonSerializerOptions _rootComponentSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) { - TypeInfoResolver = DefaultWebAssemblyJSRuntimeSerializerContext.Default, + TypeInfoResolver = DefaultWebAssemblyJSRuntimeJsonSerializerContext.Default, }; public static readonly DefaultWebAssemblyJSRuntime Instance = new(); @@ -172,4 +172,4 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference } [JsonSerializable(typeof(RootComponentOperationBatch))] -internal sealed partial class DefaultWebAssemblyJSRuntimeSerializerContext : JsonSerializerContext; +internal sealed partial class DefaultWebAssemblyJSRuntimeJsonSerializerContext : JsonSerializerContext; diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 08b02cad20ee..befd36e52a53 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -70,11 +70,6 @@ }); } }, - configureRuntime: (builder) => { - builder.withConfig({ - browserProfilerOptions: {}, - }); - }, }, }).then(() => { const startedParagraph = document.createElement('p'); diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index 188f62b80e6b..3b5fd66e8188 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -5,9 +5,6 @@ enable enable WasmMinimal - - browser; - true diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 28d2d78de61d..41830dd48de1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -4,10 +4,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -236,16 +233,9 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe if (succeeded) { var resultType = interopTask.ResultType; - - object? result; - if (resultType == typeof(IJSVoidResult)) - { - result = null; - } - else - { - result = JsonSerializer.Deserialize(ref jsonReader, resultType, interopTask.DeserializeOptions); - } + var result = resultType == typeof(IJSVoidResult) + ? null + : JsonSerializer.Deserialize(ref jsonReader, resultType, interopTask.DeserializeOptions); ByteArraysToBeRevived.Clear(); interopTask.SetResult(result); @@ -343,30 +333,3 @@ internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) /// public void Dispose() => ByteArraysToBeRevived.Dispose(); } - -[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync")] -internal sealed class FallbackTypeInfoResolver : IJsonTypeInfoResolver -{ - private static readonly DefaultJsonTypeInfoResolver _defaultJsonTypeInfoResolver = new(); - - public static readonly FallbackTypeInfoResolver Instance = new(); - - public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) - { - if (options.Converters.Any(c => c.CanConvert(type))) - { - // TODO: We should allow types with custom converters to be serialized without - // having to generate metadata for them. - // Question: Why do we even need a JsonTypeInfo if the type is going to be serialized - // with a custom converter anyway? We shouldn't need to perform any reflection here. - // Is it possible to generate a "minimal" JsonTypeInfo that just points to the correct - // converter? - return _defaultJsonTypeInfoResolver.GetTypeInfo(type, options); - } - - return null; - } -} - -[JsonSerializable(typeof(object[]))] // JS interop argument lists are always object arrays -internal sealed partial class JSRuntimeSerializerContext : JsonSerializerContext; From 75a71f6e42efd81ecfc0197550be93eba126d0d6 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 17 Apr 2024 15:11:46 -0700 Subject: [PATCH 12/20] PR feedback --- src/Components/Server/src/Circuits/RemoteJSRuntime.cs | 3 +-- src/Components/Server/src/Circuits/RemoteRenderer.cs | 2 +- src/Components/Web/src/WebRenderer.cs | 3 +-- .../WebAssembly/src/Rendering/WebAssemblyRenderer.cs | 2 +- .../src/Services/DefaultWebAssemblyJSRuntime.cs | 3 +-- .../WebView/WebView/src/Services/WebViewJSRuntime.cs | 3 +-- .../WebView/WebView/src/Services/WebViewRenderer.cs | 2 +- src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs | 6 ++---- src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs | 9 ++------- .../Microsoft.JSInterop/src/PublicAPI.Unshipped.txt | 2 +- 10 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 34c76baada6f..381452e05389 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -48,10 +48,9 @@ public RemoteJSRuntime( DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout; ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); + JsonSerializerOptions.MakeReadOnly(); } - public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; - internal void Initialize(CircuitClientProxy clientProxy) { _clientProxy = clientProxy ?? throw new ArgumentNullException(nameof(clientProxy)); diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 6a57a2eccfa5..07d3b02314d7 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -44,7 +44,7 @@ public RemoteRenderer( ILogger logger, RemoteJSRuntime jsRuntime, CircuitJSComponentInterop jsComponentInterop) - : base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) + : base(serviceProvider, loggerFactory, jsRuntime.JsonSerializerOptions, jsComponentInterop) { _client = client; _options = options; diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 463143d91d64..347403439fdd 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -43,7 +43,7 @@ public WebRenderer( jsComponentInterop.AttachToRenderer(this); var jsRuntime = serviceProvider.GetRequiredService(); - var jsRuntimeJsonSerializerOptions = jsRuntime.CloneJsonSerializerOptions(); + var jsRuntimeJsonSerializerOptions = new JsonSerializerOptions(jsRuntime.JsonSerializerOptions); jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, JsonConverterFactoryTypeInfoResolver>.Instance); jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, WebRendererSerializerContext.Default); @@ -153,7 +153,6 @@ public void RemoveRootComponent(int componentId) } } -[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(object[]))] [JsonSerializable(typeof(int))] [JsonSerializable(typeof(Dictionary))] diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index a2297cb2f8b7..349840e6aecc 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -26,7 +26,7 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer private readonly IInternalJSImportMethods _jsMethods; public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) - : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop) + : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.JsonSerializerOptions, jsComponentInterop) { _logger = loggerFactory.CreateLogger(); _jsMethods = serviceProvider.GetRequiredService(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 0d8481ff71cb..24a2e5499e5b 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -39,10 +39,9 @@ private DefaultWebAssemblyJSRuntime() { ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); + JsonSerializerOptions.MakeReadOnly(); } - public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; - [JSExport] [SupportedOSPlatform("browser")] public static string? InvokeDotNet( diff --git a/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs index 6633bbb4bc5b..b8b5b9315dd2 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs @@ -19,6 +19,7 @@ public WebViewJSRuntime() JsonSerializerOptions.Converters.Add( new ElementReferenceJsonConverter( new WebElementReferenceContext(this))); + JsonSerializerOptions.MakeReadOnly(); } public void AttachToWebView(IpcSender ipcSender) @@ -26,8 +27,6 @@ public void AttachToWebView(IpcSender ipcSender) _ipcSender = ipcSender; } - public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; - protected override void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) { if (_ipcSender is null) diff --git a/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs b/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs index b323ed1f39b6..a6beb6879d0e 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs @@ -21,7 +21,7 @@ public WebViewRenderer( ILoggerFactory loggerFactory, WebViewJSRuntime jsRuntime, JSComponentInterop jsComponentInterop) : - base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) + base(serviceProvider, loggerFactory, jsRuntime.JsonSerializerOptions, jsComponentInterop) { _dispatcher = dispatcher; _ipcSender = ipcSender; diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index ed4f91a72296..d1ea2e7681e9 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -69,9 +69,7 @@ public interface IJSRuntime => throw new InvalidOperationException($"Supplying a custom {nameof(JsonSerializerOptions)} is not supported by the current JS runtime"); /// - /// Returns a copy of the current used for JSON serialization and deserialization. + /// Gets the used to serialize and deserialize interop payloads. /// - /// A copy of the . - JsonSerializerOptions CloneJsonSerializerOptions() - => throw new InvalidOperationException($"The current JS runtime does not support cloning {nameof(JsonSerializerOptions)}"); + JsonSerializerOptions JsonSerializerOptions => throw new InvalidOperationException($"The current JS runtime does not support accessing {nameof(JsonSerializerOptions)}"); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 41830dd48de1..e6afbb447560 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -43,19 +43,14 @@ protected JSRuntime() }; } - /// - /// Gets the used to serialize and deserialize interop payloads. - /// - protected internal JsonSerializerOptions JsonSerializerOptions { get; } + /// + public JsonSerializerOptions JsonSerializerOptions { get; } /// /// Gets or sets the default timeout for asynchronous JavaScript calls. /// protected TimeSpan? DefaultAsyncTimeout { get; set; } - /// - public JsonSerializerOptions CloneJsonSerializerOptions() => new(JsonSerializerOptions); - /// public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 152241b61ae5..3b0d1639efa1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -9,9 +9,9 @@ *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 -Microsoft.JSInterop.IJSRuntime.CloneJsonSerializerOptions() -> System.Text.Json.JsonSerializerOptions! Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSRuntime.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! Microsoft.JSInterop.JSRuntime.CloneJsonSerializerOptions() -> System.Text.Json.JsonSerializerOptions! Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask From 35c4f336b7a57b8f53c9d89e4b9b6b8341108604 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 17 Apr 2024 16:25:14 -0700 Subject: [PATCH 13/20] More PR feedback --- .../Server/src/Circuits/RemoteJSRuntime.cs | 2 +- src/Components/Web/src/WebRenderer.cs | 5 +-- ...icationServiceCollectionExtensionsTests.cs | 32 +++++++++---------- .../src/Hosting/WebAssemblyHostBuilder.cs | 11 ++----- .../WebAssemblyJsonSerializerContext.cs | 21 ++++++++++++ ...bAssemblyComponentParameterDeserializer.cs | 18 +++-------- .../Services/DefaultWebAssemblyJSRuntime.cs | 17 +++------- .../Hosting/WebAssemblyHostBuilderTest.cs | 24 +++++++------- .../test/Hosting/WebAssemblyHostTest.cs | 8 ++--- .../WebView/src/Services/WebViewJSRuntime.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 3 +- 11 files changed, 67 insertions(+), 76 deletions(-) create mode 100644 src/Components/WebAssembly/WebAssembly/src/Infrastructure/WebAssemblyJsonSerializerContext.cs diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 381452e05389..e2496e2364bc 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -48,7 +48,7 @@ public RemoteJSRuntime( DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout; ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); - JsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(populateMissingResolver: JsonSerializer.IsReflectionEnabledByDefault); } internal void Initialize(CircuitClientProxy clientProxy) diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 347403439fdd..f5589ea45b49 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -44,8 +44,9 @@ public WebRenderer( var jsRuntime = serviceProvider.GetRequiredService(); var jsRuntimeJsonSerializerOptions = new JsonSerializerOptions(jsRuntime.JsonSerializerOptions); - jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, JsonConverterFactoryTypeInfoResolver>.Instance); - jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, WebRendererSerializerContext.Default); + jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Clear(); + jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default); + jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver>.Instance); jsRuntime.InvokeVoidAsync( "Blazor._internal.attachWebRendererInterop", 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/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 810b49ebb713..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,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; /// public sealed class WebAssemblyHostBuilder { - private readonly JsonSerializerOptions _jsonOptions; private readonly IInternalJSImportMethods _jsMethods; private Func _createServiceProvider; private RootComponentTypeCache? _rootComponentCache; @@ -49,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(); @@ -65,14 +61,11 @@ 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(); 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 08bd539f9984..4610b95be0be 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -3,18 +3,13 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; internal sealed class WebAssemblyComponentParameterDeserializer { - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) - { - TypeInfoResolver = WebAssemblyComponentParameterDeserializerSerializerContext.Default, - }; - private readonly ComponentParametersTypeCache _parametersCache; public WebAssemblyComponentParameterDeserializer( @@ -65,7 +60,7 @@ public ParameterView DeserializeParameters(IList parametersD var parameterValue = JsonSerializer.Deserialize( value.GetRawText(), parameterType, - WebAssemblyComponentSerializationSettings.JsonSerializationOptions); + WebAssemblyJsonSerializerContext.Default); parametersDictionary[definition.Name] = parameterValue; } @@ -83,17 +78,12 @@ public ParameterView DeserializeParameters(IList parametersD [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The correct members will be preserved by the above DynamicDependency.")] public static ComponentParameter[] GetParameterDefinitions(string parametersDefinitions) { - return JsonSerializer.Deserialize(parametersDefinitions, _jsonSerializerOptions)!; + 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, _jsonSerializerOptions)!; + return JsonSerializer.Deserialize(parameterValues, WebAssemblyJsonSerializerContext.Default.IListObject)!; } } - -[JsonSerializable(typeof(ComponentParameter[]))] -[JsonSerializable(typeof(JsonElement))] -[JsonSerializable(typeof(IList))] -internal sealed partial class WebAssemblyComponentParameterDeserializerSerializerContext : JsonSerializerContext; diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index 24a2e5499e5b..f591b2c13959 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -6,8 +6,8 @@ using System.Runtime.InteropServices.JavaScript; using System.Runtime.Versioning; using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; using Microsoft.JSInterop.WebAssembly; @@ -17,11 +17,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services; internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime { - private static readonly JsonSerializerOptions _rootComponentSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions) - { - TypeInfoResolver = DefaultWebAssemblyJSRuntimeJsonSerializerContext.Default, - }; - public static readonly DefaultWebAssemblyJSRuntime Instance = new(); private readonly RootComponentTypeCache _rootComponentCache = new(); @@ -35,11 +30,12 @@ internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime [DynamicDependency(nameof(BeginInvokeDotNet))] [DynamicDependency(nameof(ReceiveByteArrayFromJS))] [DynamicDependency(nameof(UpdateRootComponentsCore))] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = $"The call to {nameof(JsonSerializerOptions.MakeReadOnly)} only populates the missing resolver when reflection is enabled")] private DefaultWebAssemblyJSRuntime() { ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); - JsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(populateMissingResolver: JsonSerializer.IsReflectionEnabledByDefault); } [JSExport] @@ -116,9 +112,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, - _rootComponentSerializerOptions)!; + WebAssemblyJsonSerializerContext.Default.RootComponentOperationBatch)!; for (var i = 0; i < deserialized.Operations.Length; i++) { @@ -169,6 +165,3 @@ protected override Task TransmitStreamAsync(long streamId, DotNetStreamReference return TransmitDataStreamToJS.TransmitStreamAsync(this, "Blazor._internal.receiveWebAssemblyDotNetDataStream", streamId, dotNetStreamReference); } } - -[JsonSerializable(typeof(RootComponentOperationBatch))] -internal sealed partial class DefaultWebAssemblyJSRuntimeJsonSerializerContext : JsonSerializerContext; 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/WebView/WebView/src/Services/WebViewJSRuntime.cs b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs index b8b5b9315dd2..7fd5681ac91f 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs @@ -19,7 +19,7 @@ public WebViewJSRuntime() JsonSerializerOptions.Converters.Add( new ElementReferenceJsonConverter( new WebElementReferenceContext(this))); - JsonSerializerOptions.MakeReadOnly(); + JsonSerializerOptions.MakeReadOnly(populateMissingResolver: JsonSerializer.IsReflectionEnabledByDefault); } public void AttachToWebView(IpcSender ipcSender) diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 3b0d1639efa1..3c6e19bffc5f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -12,7 +12,6 @@ Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.IJSRuntime.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! -Microsoft.JSInterop.JSRuntime.CloneJsonSerializerOptions() -> System.Text.Json.JsonSerializerOptions! Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, params object?[]? args) -> System.Threading.Tasks.ValueTask @@ -20,4 +19,4 @@ static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsof static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask \ No newline at end of file +static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask From 03bdce3637562e5c07f046a535d9a24f8a91662c Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 13:18:28 -0700 Subject: [PATCH 14/20] Use ad hoc API for wasm --- .../Server/src/Circuits/RemoteJSRuntime.cs | 3 +- .../Server/src/Circuits/RemoteRenderer.cs | 2 +- .../IInternalWebJSInProcessRuntime.cs | 20 +++ .../Web/src/PublicAPI.Unshipped.txt | 2 + src/Components/Web/src/WebRenderer.cs | 39 ++++-- .../src/Rendering/WebAssemblyRenderer.cs | 2 +- .../Services/DefaultWebAssemblyJSRuntime.cs | 10 +- .../WebView/src/Services/WebViewJSRuntime.cs | 3 +- .../WebView/src/Services/WebViewRenderer.cs | 2 +- .../Microsoft.JSInterop/src/IJSInteropTask.cs | 17 --- .../Microsoft.JSInterop/src/IJSRuntime.cs | 36 ----- .../src/Infrastructure/TaskGenericsUtil.cs | 56 +++++++- .../Microsoft.JSInterop/src/JSInteropTask.cs | 60 -------- .../Microsoft.JSInterop/src/JSRuntime.cs | 129 ++++++++++-------- .../src/JSRuntimeExtensions.cs | 124 +---------------- .../src/PublicAPI.Unshipped.txt | 11 -- 16 files changed, 192 insertions(+), 324 deletions(-) create mode 100644 src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs delete mode 100644 src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs delete mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index e2496e2364bc..34c76baada6f 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -48,9 +48,10 @@ public RemoteJSRuntime( DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout; ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); - JsonSerializerOptions.MakeReadOnly(populateMissingResolver: JsonSerializer.IsReflectionEnabledByDefault); } + public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; + internal void Initialize(CircuitClientProxy clientProxy) { _clientProxy = clientProxy ?? throw new ArgumentNullException(nameof(clientProxy)); diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 07d3b02314d7..6a57a2eccfa5 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -44,7 +44,7 @@ public RemoteRenderer( ILogger logger, RemoteJSRuntime jsRuntime, CircuitJSComponentInterop jsComponentInterop) - : base(serviceProvider, loggerFactory, jsRuntime.JsonSerializerOptions, jsComponentInterop) + : base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) { _client = client; _options = options; 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/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 f5589ea45b49..4cdbd3a0850d 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -6,6 +6,7 @@ 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; @@ -43,18 +44,7 @@ public WebRenderer( jsComponentInterop.AttachToRenderer(this); var jsRuntime = serviceProvider.GetRequiredService(); - var jsRuntimeJsonSerializerOptions = new JsonSerializerOptions(jsRuntime.JsonSerializerOptions); - jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Clear(); - jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default); - jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver>.Instance); - - jsRuntime.InvokeVoidAsync( - "Blazor._internal.attachWebRendererInterop", - jsRuntimeJsonSerializerOptions, - _rendererId, - _interopMethodsReference, - jsComponentInterop.Configuration.JSComponentParametersByIdentifier, - jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); + AttachWebRendererInterop(jsRuntime, jsonOptions, jsComponentInterop); } /// @@ -111,6 +101,31 @@ 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) + { + object[] args = [ + _rendererId, + _interopMethodsReference, + jsComponentInterop.Configuration.JSComponentParametersByIdentifier, + jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer, + ]; + + if (jsRuntime is IInternalWebJSInProcessRuntime inProcessRuntime) + { + 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("Blazor._internal.attachWebRendererInterop", argsJson, JSCallResultType.JSVoidResult, 0); + } + else + { + jsRuntime.InvokeVoidAsync("Blazor._internal.attachWebRendererInterop", 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 diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 349840e6aecc..a2297cb2f8b7 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -26,7 +26,7 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer private readonly IInternalJSImportMethods _jsMethods; public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) - : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.JsonSerializerOptions, jsComponentInterop) + : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop) { _logger = loggerFactory.CreateLogger(); _jsMethods = serviceProvider.GetRequiredService(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs index f591b2c13959..6d0ce0b78110 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs @@ -6,6 +6,7 @@ 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; @@ -15,7 +16,7 @@ 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(); @@ -30,14 +31,14 @@ internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime [DynamicDependency(nameof(BeginInvokeDotNet))] [DynamicDependency(nameof(ReceiveByteArrayFromJS))] [DynamicDependency(nameof(UpdateRootComponentsCore))] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = $"The call to {nameof(JsonSerializerOptions.MakeReadOnly)} only populates the missing resolver when reflection is enabled")] private DefaultWebAssemblyJSRuntime() { ElementReferenceContext = new WebElementReferenceContext(this); JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext)); - JsonSerializerOptions.MakeReadOnly(populateMissingResolver: JsonSerializer.IsReflectionEnabledByDefault); } + public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; + [JSExport] [SupportedOSPlatform("browser")] public static string? InvokeDotNet( @@ -164,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/WebView/WebView/src/Services/WebViewJSRuntime.cs b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs index 7fd5681ac91f..6633bbb4bc5b 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs @@ -19,7 +19,6 @@ public WebViewJSRuntime() JsonSerializerOptions.Converters.Add( new ElementReferenceJsonConverter( new WebElementReferenceContext(this))); - JsonSerializerOptions.MakeReadOnly(populateMissingResolver: JsonSerializer.IsReflectionEnabledByDefault); } public void AttachToWebView(IpcSender ipcSender) @@ -27,6 +26,8 @@ public void AttachToWebView(IpcSender ipcSender) _ipcSender = ipcSender; } + public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions; + protected override void BeginInvokeJS(long taskId, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) { if (_ipcSender is null) diff --git a/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs b/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs index a6beb6879d0e..b323ed1f39b6 100644 --- a/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs +++ b/src/Components/WebView/WebView/src/Services/WebViewRenderer.cs @@ -21,7 +21,7 @@ public WebViewRenderer( ILoggerFactory loggerFactory, WebViewJSRuntime jsRuntime, JSComponentInterop jsComponentInterop) : - base(serviceProvider, loggerFactory, jsRuntime.JsonSerializerOptions, jsComponentInterop) + base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) { _dispatcher = dispatcher; _ipcSender = ipcSender; diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs deleted file mode 100644 index e13ba2d3cadd..000000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInteropTask.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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; - -namespace Microsoft.JSInterop; - -internal interface IJSInteropTask : IDisposable -{ - public Type ResultType { get; } - - public JsonSerializerOptions? DeserializeOptions { get; set; } - - void SetResult(object? result); - - void SetException(Exception exception); -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index d1ea2e7681e9..0313bf613470 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.JSInterop; @@ -25,21 +24,6 @@ public interface IJSRuntime /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args); - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, - /// consider using . - /// - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, object?[]? args) - => throw new InvalidOperationException($"Supplying a custom {nameof(JsonSerializerOptions)} is not supported by the current JS runtime"); - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -52,24 +36,4 @@ public interface IJSRuntime /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args); - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, object?[]? args) - => throw new InvalidOperationException($"Supplying a custom {nameof(JsonSerializerOptions)} is not supported by the current JS runtime"); - - /// - /// Gets the used to serialize and deserialize interop payloads. - /// - JsonSerializerOptions JsonSerializerOptions => throw new InvalidOperationException($"The current JS runtime does not support accessing {nameof(JsonSerializerOptions)}"); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs index 7cfe3964dfe7..a03d87536db1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs @@ -2,12 +2,26 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Globalization; namespace Microsoft.JSInterop.Infrastructure; internal static class TaskGenericsUtil { - private static readonly ConcurrentDictionary _cachedResultGetters = []; + private static readonly ConcurrentDictionary _cachedResultGetters + = new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary _cachedResultSetters + = new ConcurrentDictionary(); + + public static void SetTaskCompletionSourceResult(object taskCompletionSource, object? result) + => CreateResultSetter(taskCompletionSource).SetResult(taskCompletionSource, result); + + public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception) + => CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception); + + public static Type GetTaskCompletionSourceResultType(object taskCompletionSource) + => CreateResultSetter(taskCompletionSource).ResultType; public static object? GetTaskResult(Task task) { @@ -38,6 +52,13 @@ internal static class TaskGenericsUtil : null; } + interface ITcsResultSetter + { + Type ResultType { get; } + void SetResult(object taskCompletionSource, object? result); + void SetException(object taskCompletionSource, Exception exception); + } + private interface ITaskResultGetter { object? GetResult(Task task); @@ -56,4 +77,37 @@ private sealed class VoidTaskResultGetter : ITaskResultGetter return null; } } + + private sealed class TcsResultSetter : ITcsResultSetter + { + public Type ResultType => typeof(T); + + public void SetResult(object tcs, object? result) + { + var typedTcs = (TaskCompletionSource)tcs; + + // If necessary, attempt a cast + var typedResult = result is T resultT + ? resultT + : (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture)!; + + typedTcs.SetResult(typedResult!); + } + + public void SetException(object tcs, Exception exception) + { + var typedTcs = (TaskCompletionSource)tcs; + typedTcs.SetException(exception); + } + } + + private static ITcsResultSetter CreateResultSetter(object taskCompletionSource) + { + return _cachedResultSetters.GetOrAdd(taskCompletionSource.GetType(), tcsType => + { + var resultType = tcsType.GetGenericArguments()[0]; + return (ITcsResultSetter)Activator.CreateInstance( + typeof(TcsResultSetter<>).MakeGenericType(resultType))!; + }); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs deleted file mode 100644 index 7271a4a10568..000000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInteropTask.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using System.Text.Json; - -namespace Microsoft.JSInterop; - -internal sealed class JSInteropTask : IJSInteropTask -{ - private readonly TaskCompletionSource _tcs; - private readonly CancellationTokenRegistration _cancellationTokenRegistration; - private readonly Action? _onCanceled; - - public JsonSerializerOptions? DeserializeOptions { get; set; } - - public Task Task => _tcs.Task; - - public Type ResultType => typeof(TResult); - - public JSInteropTask(CancellationToken cancellationToken, Action? onCanceled = null) - { - _tcs = new TaskCompletionSource(); - _onCanceled = onCanceled; - - if (cancellationToken.CanBeCanceled) - { - _cancellationTokenRegistration = cancellationToken.Register(Cancel); - } - } - - public void SetResult(object? result) - { - if (result is not TResult typedResult) - { - typedResult = (TResult)Convert.ChangeType(result, typeof(TResult), CultureInfo.InvariantCulture)!; - } - - _tcs.SetResult(typedResult); - _cancellationTokenRegistration.Dispose(); - } - - public void SetException(Exception exception) - { - _tcs.SetException(exception); - _cancellationTokenRegistration.Dispose(); - } - - private void Cancel() - { - _cancellationTokenRegistration.Dispose(); - _tcs.TrySetCanceled(); - _onCanceled?.Invoke(); - } - - public void Dispose() - { - _cancellationTokenRegistration.Dispose(); - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index e6afbb447560..3cbd024d2755 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -17,8 +17,9 @@ public abstract partial class JSRuntime : IJSRuntime, IDisposable { private long _nextObjectReferenceId; // Initial value of 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" - private readonly ConcurrentDictionary _pendingTasks = new(); + private readonly ConcurrentDictionary _pendingTasks = new(); private readonly ConcurrentDictionary _trackedRefsById = new(); + private readonly ConcurrentDictionary _cancellationRegistrations = new(); internal readonly ArrayBuilder ByteArraysToBeRevived = new(); @@ -39,101 +40,113 @@ protected JSRuntime() new JSStreamReferenceJsonConverter(this), new DotNetStreamReferenceJsonConverter(this), new ByteArrayJsonConverter(this), - }, + } }; } - /// - public JsonSerializerOptions JsonSerializerOptions { get; } + /// + /// Gets the used to serialize and deserialize interop payloads. + /// + protected internal JsonSerializerOptions JsonSerializerOptions { get; } /// /// Gets or sets the default timeout for asynchronous JavaScript calls. /// protected TimeSpan? DefaultAsyncTimeout { get; set; } - /// + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different, or no timeout, + /// consider using . + /// + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args) => InvokeAsync(0, identifier, args); - /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, object?[]? args) - => InvokeAsync(0, identifier, options, args); - - /// + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => InvokeAsync(0, identifier, cancellationToken, args); - /// - public ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, object?[]? args) - => InvokeAsync(0, identifier, options, cancellationToken, args); - - internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, JsonSerializerOptions? options, object?[]? args) + internal async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] 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(targetInstanceId, identifier, options, cts.Token, args); + return await InvokeAsync(targetInstanceId, identifier, cts.Token, args); } - return await InvokeAsync(targetInstanceId, identifier, options, CancellationToken.None, args); + return await InvokeAsync(targetInstanceId, identifier, CancellationToken.None, args); } - internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(long targetInstanceId, string identifier, object?[]? args) - => InvokeAsync(targetInstanceId, identifier, options: null, args); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure JS interop arguments are linker friendly.")] internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( long targetInstanceId, string identifier, - JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken, object?[]? args) { - if (cancellationToken.IsCancellationRequested) - { - return ValueTask.FromCanceled(cancellationToken); - } - var taskId = Interlocked.Increment(ref _nextPendingTaskId); - var interopTask = new JSInteropTask(cancellationToken, onCanceled: () => + var tcs = new TaskCompletionSource(); + if (cancellationToken.CanBeCanceled) { - _pendingTasks.TryRemove(taskId, out _); - }); - - _pendingTasks[taskId] = interopTask; + _cancellationRegistrations[taskId] = cancellationToken.Register(() => + { + tcs.TrySetCanceled(cancellationToken); + CleanupTasksAndRegistrations(taskId); + }); + } + _pendingTasks[taskId] = tcs; try { - var resultType = JSCallResultTypeHelper.FromGeneric(); - jsonSerializerOptions ??= JsonSerializerOptions; - - var argsJson = args switch + if (cancellationToken.IsCancellationRequested) { - null or { Length: 0 } => null, - _ => JsonSerializer.Serialize(args, jsonSerializerOptions), - }; + tcs.TrySetCanceled(cancellationToken); + CleanupTasksAndRegistrations(taskId); + + return new ValueTask(tcs.Task); + } - interopTask.DeserializeOptions = jsonSerializerOptions; + var argsJson = args is not null && args.Length != 0 ? + JsonSerializer.Serialize(args, JsonSerializerOptions) : + null; + var resultType = JSCallResultTypeHelper.FromGeneric(); BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId); - return new ValueTask(interopTask.Task); + return new ValueTask(tcs.Task); } catch { - _pendingTasks.TryRemove(taskId, out _); - interopTask.Dispose(); + CleanupTasksAndRegistrations(taskId); throw; } } - internal ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>( - long targetInstanceId, - string identifier, - CancellationToken cancellationToken, - object?[]? args) - => InvokeAsync(targetInstanceId, identifier, jsonSerializerOptions: null, cancellationToken, args); + private void CleanupTasksAndRegistrations(long taskId) + { + _pendingTasks.TryRemove(taskId, out _); + if (_cancellationRegistrations.TryRemove(taskId, out var registration)) + { + registration.Dispose(); + } + } /// /// Begins an asynchronous function invocation. @@ -216,29 +229,29 @@ protected internal virtual Task ReadJSDataAsStreamAsync(IJSStreamReferen [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync.")] internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader) { - if (!_pendingTasks.TryRemove(taskId, out var interopTask)) + if (!_pendingTasks.TryRemove(taskId, out var tcs)) { // We should simply return if we can't find an id for the invocation. // This likely means that the method that initiated the call defined a timeout and stopped waiting. return false; } + CleanupTasksAndRegistrations(taskId); + try { if (succeeded) { - var resultType = interopTask.ResultType; - var result = resultType == typeof(IJSVoidResult) - ? null - : JsonSerializer.Deserialize(ref jsonReader, resultType, interopTask.DeserializeOptions); + var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); + var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); ByteArraysToBeRevived.Clear(); - interopTask.SetResult(result); + TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); } else { var exceptionText = jsonReader.GetString() ?? string.Empty; - interopTask.SetException(new JSException(exceptionText)); + TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText)); } return true; @@ -246,13 +259,9 @@ internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe catch (Exception exception) { var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details."; - interopTask.SetException(new JSException(message, exception)); + TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception)); return false; } - finally - { - interopTask.Dispose(); - } } /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 315bb8118277..5a4202ed15da 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -28,95 +26,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string await jsRuntime.InvokeAsync(identifier, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - await jsRuntime.InvokeAsync(identifier, options, args); - } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - await jsRuntime.InvokeAsync(identifier, cancellationToken, args); - } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. - /// - /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts - /// () from being applied. - /// - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - await jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); - } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The duration after which to cancel the async operation. Overrides default timeouts (). - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); - var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - - await jsRuntime.InvokeAsync(identifier, cancellationToken, args); - } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The . - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization. - /// The duration after which to cancel the async operation. Overrides default timeouts (). - /// JSON-serializable arguments. - /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, TimeSpan timeout, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); - var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - - await jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -136,26 +45,6 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string return jsRuntime.InvokeAsync(identifier, args); } - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, - /// consider using . - /// - /// - /// The . - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, params object?[]? args) - { - ArgumentNullException.ThrowIfNull(jsRuntime); - - return jsRuntime.InvokeAsync(identifier, options, args); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -178,21 +67,19 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// Invokes the specified JavaScript function asynchronously. /// - /// The JSON-serializable return type. /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. /// /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts /// () from being applied. /// /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, CancellationToken cancellationToken, params object?[]? args) + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); - return jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); + await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } /// @@ -218,17 +105,16 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string /// /// The . /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// The to use for JSON serialization and deserialization. /// The duration after which to cancel the async operation. Overrides default timeouts (). /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(this IJSRuntime jsRuntime, string identifier, JsonSerializerOptions options, TimeSpan timeout, params object?[]? args) + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object?[]? args) { ArgumentNullException.ThrowIfNull(jsRuntime); using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; - return await jsRuntime.InvokeAsync(identifier, options, cancellationToken, args); + await jsRuntime.InvokeAsync(identifier, cancellationToken, args); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 3c6e19bffc5f..3759e9ad0478 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -9,14 +9,3 @@ *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 -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.IJSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.IJSRuntime.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions! -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, object?[]? args) -> System.Threading.Tasks.ValueTask -Microsoft.JSInterop.JSRuntime.InvokeAsync(string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.Threading.CancellationToken cancellationToken, params object?[]? args) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.Text.Json.JsonSerializerOptions! options, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask From fb95f5ce218a9bbcbbeb7a946c8bd6172893f0dd Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 13:21:30 -0700 Subject: [PATCH 15/20] Cleanup --- src/Components/Web/src/WebRenderer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 4cdbd3a0850d..2d65e3bfc362 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -42,7 +42,6 @@ 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(); AttachWebRendererInterop(jsRuntime, jsonOptions, jsComponentInterop); } @@ -104,6 +103,8 @@ protected override void Dispose(bool 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"; + object[] args = [ _rendererId, _interopMethodsReference, @@ -118,11 +119,11 @@ private void AttachWebRendererInterop(IJSRuntime jsRuntime, JsonSerializerOption newJsonOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default); newJsonOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver>.Instance); var argsJson = JsonSerializer.Serialize(args, newJsonOptions); - inProcessRuntime.InvokeJS("Blazor._internal.attachWebRendererInterop", argsJson, JSCallResultType.JSVoidResult, 0); + inProcessRuntime.InvokeJS(JSMethodIdentifier, argsJson, JSCallResultType.JSVoidResult, 0); } else { - jsRuntime.InvokeVoidAsync("Blazor._internal.attachWebRendererInterop", args).Preserve(); + jsRuntime.InvokeVoidAsync(JSMethodIdentifier, args).Preserve(); } } From 2996e5c83c6be5fb94d3bc0465130bff8a560ed5 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 14:04:56 -0700 Subject: [PATCH 16/20] Undo AntiforgeryStateProvider change --- .../src/DefaultAntiforgeryStateProvider.cs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs index 4aff2437d076..6a3d926a73a2 100644 --- a/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs +++ b/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.AspNetCore.Components.Forms; @@ -26,19 +24,11 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) // don't have access to the request. _subscription = state.RegisterOnPersisting(() => { - var bytes = JsonSerializer.SerializeToUtf8Bytes( - GetAntiforgeryToken(), - DefaultAntiforgeryStateProviderJsonSerializerContext.Default.AntiforgeryRequestToken); - state.PersistAsJson(PersistenceKey, bytes); + state.PersistAsJson(PersistenceKey, GetAntiforgeryToken()); return Task.CompletedTask; }, RenderMode.InteractiveAuto); - if (state.TryTakeFromJson(PersistenceKey, out var bytes)) - { - _currentToken = JsonSerializer.Deserialize( - bytes, - DefaultAntiforgeryStateProviderJsonSerializerContext.Default.AntiforgeryRequestToken); - } + state.TryTakeFromJson(PersistenceKey, out _currentToken); } /// @@ -47,6 +37,3 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state) /// public void Dispose() => _subscription.Dispose(); } - -[JsonSerializable(typeof(AntiforgeryRequestToken))] -internal sealed partial class DefaultAntiforgeryStateProviderJsonSerializerContext : JsonSerializerContext; From a10f5c0cbaabf74e91edf059b51c812b2c76b50e Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 15:17:52 -0700 Subject: [PATCH 17/20] Optimization --- .../JsonConverterFactoryTypeInfoResolver.cs | 45 ----------- ...Microsoft.AspNetCore.Components.Web.csproj | 1 - src/Components/Web/src/WebRenderer.cs | 79 +++++++++++++++---- 3 files changed, 63 insertions(+), 62 deletions(-) delete mode 100644 src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs diff --git a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs b/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs deleted file mode 100644 index 32d9973a0fd8..000000000000 --- a/src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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/Microsoft.AspNetCore.Components.Web.csproj b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj index 9362dc20a8dc..2b337152af34 100644 --- a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj +++ b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj @@ -18,7 +18,6 @@ - diff --git a/src/Components/Web/src/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index 2d65e3bfc362..c6fe900d29f4 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.Web.Internal; @@ -100,33 +102,76 @@ 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"; - object[] args = [ - _rendererId, - _interopMethodsReference, - jsComponentInterop.Configuration.JSComponentParametersByIdentifier, - jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer, - ]; - if (jsRuntime is IInternalWebJSInProcessRuntime inProcessRuntime) { - var newJsonOptions = new JsonSerializerOptions(jsonOptions); - newJsonOptions.TypeInfoResolverChain.Clear(); - newJsonOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default); - newJsonOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver>.Instance); - var argsJson = JsonSerializer.Serialize(args, newJsonOptions); + // 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 interopMethodsReferenceJsonTypeInfo = GetJsonTypeInfoFromJsonConverterFactories>( + jsonOptions.Converters, + WebRendererSerializerContext.Default.Options); + + var rendererIdJson = _rendererId.ToString(CultureInfo.InvariantCulture); + var interopMethodsReferenceJson = JsonSerializer.Serialize( + _interopMethodsReference, + interopMethodsReferenceJsonTypeInfo); + var jsComponentParametersByIdentifierJson = JsonSerializer.Serialize( + jsComponentInterop.Configuration.JSComponentParametersByIdentifier, + WebRendererSerializerContext.Default.DictionaryStringJSComponentParameterArray); + var jsComponentIdentifiersByInitializerJson = JsonSerializer.Serialize( + jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer, + WebRendererSerializerContext.Default.DictionaryStringListString); + + var argsJson = + $"[{rendererIdJson}, " + + $"{interopMethodsReferenceJson}, " + + $"{jsComponentParametersByIdentifierJson}, " + + $"{jsComponentIdentifiersByInitializerJson}]"; inProcessRuntime.InvokeJS(JSMethodIdentifier, argsJson, JSCallResultType.JSVoidResult, 0); } else { - jsRuntime.InvokeVoidAsync(JSMethodIdentifier, args).Preserve(); + jsRuntime.InvokeVoidAsync( + JSMethodIdentifier, + _rendererId, + _interopMethodsReference, + jsComponentInterop.Configuration.JSComponentParametersByIdentifier, + jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); } } + private static JsonTypeInfo GetJsonTypeInfoFromJsonConverterFactories( + IList converters, + JsonSerializerOptions optionsToUse) + { + foreach (var converter in converters) + { + if (converter is not JsonConverterFactory factory || !factory.CanConvert(typeof(T))) + { + continue; + } + + if (factory.CreateConverter(typeof(T), optionsToUse) is not { } converterToUse) + { + continue; + } + + return JsonMetadataServices.CreateValueInfo(optionsToUse, converterToUse); + } + + throw new InvalidOperationException($"Could not create a JsonTypeInfo for type {typeof(T).FullName}"); + } + /// /// 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 @@ -170,8 +215,10 @@ public void RemoveRootComponent(int componentId) } } -[JsonSerializable(typeof(object[]))] -[JsonSerializable(typeof(int))] +[JsonSourceGenerationOptions( + GenerationMode = JsonSourceGenerationMode.Serialization, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true)] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary>))] internal sealed partial class WebRendererSerializerContext : JsonSerializerContext; From 48e7961ffebf5b6f0725b53538bb7471db8ecef6 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 15:22:52 -0700 Subject: [PATCH 18/20] Remvoe unnecessary warning suppression --- .../Web/src/Microsoft.AspNetCore.Components.Web.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj index 2b337152af34..b3ec9165085a 100644 --- a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj +++ b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj @@ -7,8 +7,6 @@ true Microsoft.AspNetCore.Components enable - - $(NoWarn);SYSLIB0020 true false From 9a53261f620ea71f078784e2d3a142f3815dfdc4 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 15:55:45 -0700 Subject: [PATCH 19/20] Revert optimization It wasn't clear that this led to a real perf benefit in optimized builds --- .../JsonConverterFactoryTypeInfoResolver.cs | 45 +++++++++++ ...Microsoft.AspNetCore.Components.Web.csproj | 1 + src/Components/Web/src/WebRenderer.cs | 74 +++++-------------- 3 files changed, 66 insertions(+), 54 deletions(-) create mode 100644 src/Components/Shared/src/JsonSerialization/JsonConverterFactoryTypeInfoResolver.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/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/WebRenderer.cs b/src/Components/Web/src/WebRenderer.cs index c6fe900d29f4..becd887453d0 100644 --- a/src/Components/Web/src/WebRenderer.cs +++ b/src/Components/Web/src/WebRenderer.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.Web.Internal; @@ -102,10 +100,19 @@ 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 @@ -117,59 +124,18 @@ private void AttachWebRendererInterop(IJSRuntime jsRuntime, JsonSerializerOption // 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 interopMethodsReferenceJsonTypeInfo = GetJsonTypeInfoFromJsonConverterFactories>( - jsonOptions.Converters, - WebRendererSerializerContext.Default.Options); - - var rendererIdJson = _rendererId.ToString(CultureInfo.InvariantCulture); - var interopMethodsReferenceJson = JsonSerializer.Serialize( - _interopMethodsReference, - interopMethodsReferenceJsonTypeInfo); - var jsComponentParametersByIdentifierJson = JsonSerializer.Serialize( - jsComponentInterop.Configuration.JSComponentParametersByIdentifier, - WebRendererSerializerContext.Default.DictionaryStringJSComponentParameterArray); - var jsComponentIdentifiersByInitializerJson = JsonSerializer.Serialize( - jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer, - WebRendererSerializerContext.Default.DictionaryStringListString); - - var argsJson = - $"[{rendererIdJson}, " + - $"{interopMethodsReferenceJson}, " + - $"{jsComponentParametersByIdentifierJson}, " + - $"{jsComponentIdentifiersByInitializerJson}]"; + + 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, - _rendererId, - _interopMethodsReference, - jsComponentInterop.Configuration.JSComponentParametersByIdentifier, - jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve(); - } - } - - private static JsonTypeInfo GetJsonTypeInfoFromJsonConverterFactories( - IList converters, - JsonSerializerOptions optionsToUse) - { - foreach (var converter in converters) - { - if (converter is not JsonConverterFactory factory || !factory.CanConvert(typeof(T))) - { - continue; - } - - if (factory.CreateConverter(typeof(T), optionsToUse) is not { } converterToUse) - { - continue; - } - - return JsonMetadataServices.CreateValueInfo(optionsToUse, converterToUse); + jsRuntime.InvokeVoidAsync(JSMethodIdentifier, args).Preserve(); } - - throw new InvalidOperationException($"Could not create a JsonTypeInfo for type {typeof(T).FullName}"); } /// @@ -215,10 +181,10 @@ public void RemoveRootComponent(int componentId) } } -[JsonSourceGenerationOptions( - GenerationMode = JsonSourceGenerationMode.Serialization, - PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true)] +// 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; From f08d858d92910bccdb8bd6633b244daa5d4a106e Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 18 Apr 2024 17:20:42 -0700 Subject: [PATCH 20/20] Fix --- .../Prerendering/WebAssemblyComponentParameterDeserializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs index 4610b95be0be..0c4884243585 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs @@ -60,7 +60,7 @@ public ParameterView DeserializeParameters(IList parametersD var parameterValue = JsonSerializer.Deserialize( value.GetRawText(), parameterType, - WebAssemblyJsonSerializerContext.Default); + WebAssemblyComponentSerializationSettings.JsonSerializationOptions); parametersDictionary[definition.Name] = parameterValue; }