Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Blazor] Use JSON source generator during WebAssembly startup #54956

Merged
merged 20 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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;
Expand All @@ -24,11 +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(),
DefaultAntiforgeryStateProviderJsonSerializerContext.Default.AntiforgeryRequestToken);
state.PersistAsJson(PersistenceKey, bytes);
return Task.CompletedTask;
}, RenderMode.InteractiveAuto);

state.TryTakeFromJson(PersistenceKey, out _currentToken);
if (state.TryTakeFromJson<byte[]>(PersistenceKey, out var bytes))
{
_currentToken = JsonSerializer.Deserialize(
bytes,
DefaultAntiforgeryStateProviderJsonSerializerContext.Default.AntiforgeryRequestToken);
}
}

/// <inheritdoc />
Expand All @@ -37,3 +47,6 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state)
/// <inheritdoc />
public void Dispose() => _subscription.Dispose();
}

[JsonSerializable(typeof(AntiforgeryRequestToken))]
internal sealed partial class DefaultAntiforgeryStateProviderJsonSerializerContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -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<T> : IJsonTypeInfoResolver
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
public static readonly JsonConverterFactoryTypeInfoResolver<T> 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<T>(options, converterToUse);
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Microsoft.AspNetCore.Components</RootNamespace>
<Nullable>enable</Nullable>
<!-- SYSLIB0020: JsonSerializerOptions.IgnoreNullValues is obsolete -->
<NoWarn>$(NoWarn);SYSLIB0020</NoWarn>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this warning happen? I can't see any usage of IgnoreNullValues in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning was coming from generated code, but it looks like that's not happening anymore for some reason. I've just removed this.

<IsTrimmable>true</IsTrimmable>
<!-- TODO: Address Native AOT analyzer warnings https://github.com/dotnet/aspnetcore/issues/45473 -->
<EnableAOTAnalyzer>false</EnableAOTAnalyzer>
Expand All @@ -16,6 +18,7 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\AttributeUtilities.cs" LinkBase="Forms" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ExpressionFormatting\**\*.cs" LinkBase="Forms\ExpressionFommatting" />
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerialization\JsonConverterFactoryTypeInfoResolver.cs" LinkBase="JsonSerialization" />
</ItemGroup>

<ItemGroup>
Expand Down
14 changes: 14 additions & 0 deletions src/Components/Web/src/WebRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,9 +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<IJSRuntime>();
var jsRuntimeJsonSerializerOptions = jsRuntime.CloneJsonSerializerOptions();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the jsonOptions parameter defined in line 34 factor into the configuration of this new JSO instance?

Copy link
Member Author

@MackinnonBuck MackinnonBuck Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That primarily gets used to deserialize event args from JavaScript events. If we reused jsonOptions rather than creating a new one specifically for the call to InvokeVoidAsync() at the end of this method, we'd have to add a DefaultJsonTypeInfoResolver at the end of the TypeInfoResolverChain. That's because developers can dynamically register their own custom EventArgs types that we later deserialize when that event gets emitted from JS.

However, if I do that, then startup time explodes, because ResolvePolymorphicConverter ends up initializing a JsonTypeInfo for each of the interfaces that each of the arguments to IJSRuntime.InvokeVoidAsync() implements. For example, all the interfaces that System.Int32 implements get their own JsonTypeInfo because we pass an int in the call to InvokeVoidAsync().

This is probably due to the fact that JSRuntime serializes arguments as an object[]. I worked around this in an earlier prototype by instead serializing each argument individually, using .GetType() as the inputType argument in JsonSerializer.Serialize(). But that felt like a hack. Maybe you know of a better way to avoid this pitfall?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this as another reason to deprecate the object[] overloads and give each argument a generic parameter.

jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, JsonConverterFactoryTypeInfoResolver<DotNetObjectReference<WebRendererInteropMethods>>.Instance);
jsRuntimeJsonSerializerOptions.TypeInfoResolverChain.Insert(0, WebRendererSerializerContext.Default);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is WebRenderer instantiated as a singleton? If not, creating a new JSO for every instance is bound to create initialization overheads.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's scoped, not singleton. The scope is per HTTP request for SSR, or per WebSocket connection for Blazor Server.

It might be possible to share a JSO across scopes as long as there's no risk of interference or information leakage across users when doing that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That shouldn't be an issue unless user info is encapsulated by any custom converters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you reminded me that the custom converters very much do require access to scoped context. For example see DotNetObjectReferenceJsonConverter which has to access the scope's JSRuntime in order to keep track of the references on a per-scope basis.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we've had this discussion before and it seems to be an unfortunate consequence of STJ not supporting DI that is scoped to a particular serialization operation (tracking issue). Is there any way we could decouple the converters from user state using APIs we have in our disposal today? Perhaps AsyncLocal<T>?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the degree of antipathy the team has towards AsyncLocal in general, and the likely back-compat problems with such a change, it's unlikely we'd go that way I'm afraid!


jsRuntime.InvokeVoidAsync(
"Blazor._internal.attachWebRendererInterop",
jsRuntimeJsonSerializerOptions,
_rendererId,
_interopMethodsReference,
jsComponentInterop.Configuration.JSComponentParametersByIdentifier,
Expand Down Expand Up @@ -145,3 +152,10 @@ public void RemoveRootComponent(int componentId)
=> _jsComponentInterop.RemoveRootComponent(componentId);
}
}

[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(object[]))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, JSComponentConfigurationStore.JSComponentParameter[]>))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
internal sealed partial class WebRendererSerializerContext : JsonSerializerContext;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about how you think we should maintain this in the future. How would a developer know when/what to add to this list of types? Am I right to think that if they start serializing something different and fail to add it here, everything would still work but would just be slower? Or would some error occur so they know to change this code?

Copy link
Member

@SteveSandersonMS SteveSandersonMS Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along the same lines, how did you even know to include these specific types? I'm hoping it's because if you don't, there's a clear error indicating they need to be included!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would a developer know when/what to add to this list of types?

These types match the types of the arguments passed to the JS interop call. Failing to do this results in a runtime error. However, I've just pushed a change that makes it even clearer where these types come from, so hopefully that eliminates any confusion. If we end up reverting that change, I can add a comment explaining how these types should be specified.

Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,6 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo
operation.Descriptor!.Parameters));
}

WebAssemblyRenderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
public sealed class WebAssemblyHostBuilder
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly IInternalJSImportMethods _jsMethods;
private Func<IServiceProvider> _createServiceProvider;
private RootComponentTypeCache? _rootComponentCache;
private string? _persistedState;
Expand Down Expand Up @@ -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();
Expand All @@ -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 = () =>
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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<IWebAssemblyHostEnvironment>(hostEnvironment);
Expand Down Expand Up @@ -305,6 +307,7 @@ internal void InitializeDefaultServices()
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<IScrollToLocationHash>(WebAssemblyScrollToLocationHash.Instance);
Services.AddSingleton<IInternalJSImportMethods>(_jsMethods);
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
Services.AddSingleton<RootComponentTypeCache>(_ => _rootComponentCache ?? new());
Services.AddSingleton<ComponentStatePersistenceManager>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
private static readonly JsonSerializerOptions _jsonSerializerOptions = new(WebAssemblyComponentSerializationSettings.JsonSerializationOptions)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on how WebAssemblyComponentSerializationSettings.JsonSerializationOption is defined, you might be able to avoid creating this intermediate options instance by configuring your JsonSerializerContext via the JsonSourceGenerationsOptionsAttribute.

{
TypeInfoResolver = WebAssemblyComponentParameterDeserializerSerializerContext.Default,
};

private readonly ComponentParametersTypeCache _parametersCache;

public WebAssemblyComponentParameterDeserializer(
Expand Down Expand Up @@ -75,15 +81,19 @@ public ParameterView DeserializeParameters(IList<ComponentParameter> 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<ComponentParameter[]>(parametersDefinitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!;
return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, _jsonSerializerOptions)!;
}

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to preserve component parameter types.")]
public static IList<object> GetParameterValues(string parameterValues)
{
return JsonSerializer.Deserialize<IList<object>>(parameterValues, WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!;
return JsonSerializer.Deserialize<IList<object>>(parameterValues, _jsonSerializerOptions)!;
}
}

[JsonSerializable(typeof(ComponentParameter[]))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(IList<object>))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to work when we have to deserialize the concrete parameters?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd need to additionally register the types you expect object to be. Given that @MackinnonBuck is planning on adding the reflection-based resolver as a fallback it would work even if the list isn't exhaustive, but making sure the 80% is handled by the source generator should contribute to improved startup times.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this JsonSerializerContext is only used during deserialization, we happen to know that the object in IList<object> will either be a JsonElement or null. Why we didn't initially choose to deserialize a JsonElement[] directly, I'm not sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not call Deserialize<IList<JsonElement>> (or Deserialize<IList<JsonElement?>> if null is valid) and register [JsonSerializable(typeof(IList<JsonElement?>))] here?

internal sealed partial class WebAssemblyComponentParameterDeserializerSerializerContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WebAssemblyRenderer>();
_jsMethods = serviceProvider.GetRequiredService<IInternalJSImportMethods>();

// if SynchronizationContext.Current is null, it means we are on the single-threaded runtime
_dispatcher = WebAssemblyDispatcher._mainSynchronizationContext == null
Expand Down Expand Up @@ -70,9 +72,9 @@ private void OnUpdateRootComponents(RootComponentOperationBatch batch)
NotifyEndUpdateRootComponents(batch.BatchId);
}

public static void NotifyEndUpdateRootComponents(long batchId)
public void NotifyEndUpdateRootComponents(long batchId)
{
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid("Blazor._internal.endUpdateRootComponents", batchId);
_jsMethods.EndUpdateRootComponents(batchId);
}

public override Dispatcher Dispatcher => _dispatcher;
Expand All @@ -87,11 +89,7 @@ public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type compo

protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector)
{
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(
"Blazor._internal.attachRootComponentToElement",
domElementSelector,
componentId,
RendererId);
_jsMethods.AttachRootComponentToElement(domElementSelector, componentId, RendererId);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = DefaultWebAssemblyJSRuntimeJsonSerializerContext.Default,
};

public static readonly DefaultWebAssemblyJSRuntime Instance = new();

private readonly RootComponentTypeCache _rootComponentCache = new();
internal static readonly DefaultWebAssemblyJSRuntime Instance = new();

public ElementReferenceContext ElementReferenceContext { get; }

Expand Down Expand Up @@ -112,7 +119,7 @@ internal static RootComponentOperationBatch DeserializeOperations(string operati
{
var deserialized = JsonSerializer.Deserialize<RootComponentOperationBatch>(
operationsJson,
WebAssemblyComponentSerializationSettings.JsonSerializationOptions)!;
_rootComponentSerializerOptions)!;

for (var i = 0; i < deserialized.Operations.Length; i++)
{
Expand Down Expand Up @@ -163,3 +170,6 @@ 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would strongly recommend creating one central JsonSerializerContext in your project that declares all needed types in one place. This should serve to improve both application size and startup time. Even though these might generate for different types superficially, transitively there is bound to be overlap (e.g. JsonTypeInfo<int>, JsonTypeInfo<string> but also shared internal types) and it would be best if these were only generated/initialized at a single location.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this would be ideal - Unfortunately, I haven't found a good way to achieve that in totality. The types that we serialize are split across multiple assemblies and shared sources. I fixed the example highlighted here, but I don't think there's much else we can easily improve. Maybe one possible improvement is having the DefaultAntiforgeryStateProvider shared source use a duck-typed InternalJsonSerializerContext that supports AntiforgeryRequestToken. So each project that references DefaultAntiforgeryStateProvider must also define an InternalJsonSerializerContext. But then we'd probably like to be sure that each InternalJsonSerializerContext has the same JsonSourceGenerationOptions configured... Hm..

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading