Skip to content

Commit c9af79a

Browse files
[Blazor] Use JSON source generator during WebAssembly startup (#54956)
1 parent 23afddf commit c9af79a

19 files changed

+238
-90
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using System.Text.Json.Serialization.Metadata;
7+
8+
namespace Microsoft.AspNetCore.Components;
9+
10+
// For custom converters that don't rely on serializing an object graph,
11+
// we can resolve the incoming type's JsonTypeInfo directly from the converter.
12+
// This skips extra work to collect metadata for the type that won't be used.
13+
internal sealed class JsonConverterFactoryTypeInfoResolver<T> : IJsonTypeInfoResolver
14+
{
15+
public static readonly JsonConverterFactoryTypeInfoResolver<T> Instance = new();
16+
17+
private JsonConverterFactoryTypeInfoResolver()
18+
{
19+
}
20+
21+
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
22+
{
23+
if (type != typeof(T))
24+
{
25+
return null;
26+
}
27+
28+
foreach (var converter in options.Converters)
29+
{
30+
if (converter is not JsonConverterFactory factory || !factory.CanConvert(type))
31+
{
32+
continue;
33+
}
34+
35+
if (factory.CreateConverter(type, options) is not { } converterToUse)
36+
{
37+
continue;
38+
}
39+
40+
return JsonMetadataServices.CreateValueInfo<T>(options, converterToUse);
41+
}
42+
43+
return null;
44+
}
45+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.JSInterop;
7+
8+
namespace Microsoft.AspNetCore.Components.Web.Internal;
9+
10+
/// <summary>
11+
/// For internal framework use only.
12+
/// </summary>
13+
[EditorBrowsable(EditorBrowsableState.Never)]
14+
public interface IInternalWebJSInProcessRuntime
15+
{
16+
/// <summary>
17+
/// For internal framework use only.
18+
/// </summary>
19+
string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId);
20+
}

src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Compile Include="$(ComponentsSharedSourceRoot)src\AttributeUtilities.cs" LinkBase="Forms" />
1717
<Compile Include="$(ComponentsSharedSourceRoot)src\ExpressionFormatting\**\*.cs" LinkBase="Forms\ExpressionFommatting" />
1818
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />
19+
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerialization\JsonConverterFactoryTypeInfoResolver.cs" LinkBase="JsonSerialization" />
1920
</ItemGroup>
2021

2122
<ItemGroup>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime
3+
Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime.InvokeJS(string! identifier, string? argsJson, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> string!
24
Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.get -> bool
35
Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.set -> void

src/Components/Web/src/WebRenderer.cs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Text.Json;
6+
using System.Text.Json.Serialization;
67
using Microsoft.AspNetCore.Components.Web;
78
using Microsoft.AspNetCore.Components.Web.Infrastructure;
9+
using Microsoft.AspNetCore.Components.Web.Internal;
810
using Microsoft.Extensions.DependencyInjection;
911
using Microsoft.Extensions.Logging;
1012
using Microsoft.JSInterop;
@@ -41,12 +43,7 @@ public WebRenderer(
4143
// Supply a DotNetObjectReference to JS that it can use to call us back for events etc.
4244
jsComponentInterop.AttachToRenderer(this);
4345
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
44-
jsRuntime.InvokeVoidAsync(
45-
"Blazor._internal.attachWebRendererInterop",
46-
_rendererId,
47-
_interopMethodsReference,
48-
jsComponentInterop.Configuration.JSComponentParametersByIdentifier,
49-
jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve();
46+
AttachWebRendererInterop(jsRuntime, jsonOptions, jsComponentInterop);
5047
}
5148

5249
/// <summary>
@@ -103,6 +100,44 @@ protected override void Dispose(bool disposing)
103100
base.Dispose(disposing);
104101
}
105102

103+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
104+
private void AttachWebRendererInterop(IJSRuntime jsRuntime, JsonSerializerOptions jsonOptions, JSComponentInterop jsComponentInterop)
105+
{
106+
const string JSMethodIdentifier = "Blazor._internal.attachWebRendererInterop";
107+
108+
// These arguments should be kept in sync with WebRendererSerializerContext
109+
object[] args = [
110+
_rendererId,
111+
_interopMethodsReference,
112+
jsComponentInterop.Configuration.JSComponentParametersByIdentifier,
113+
jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer,
114+
];
115+
116+
if (jsRuntime is IInternalWebJSInProcessRuntime inProcessRuntime)
117+
{
118+
// Fast path for WebAssembly: Rather than using the JSRuntime to serialize
119+
// parameters, we utilize the source-generated WebRendererSerializerContext
120+
// for a faster JsonTypeInfo resolution.
121+
122+
// We resolve a JsonTypeInfo for DotNetObjectReference<WebRendererInteropMethods> from
123+
// the JS runtime's JsonConverters. This is because adding DotNetObjectReference<T> as
124+
// a supported type in the JsonSerializerContext generates unnecessary code to produce
125+
// JsonTypeInfo for all the types referenced by both DotNetObjectReference<T> and its
126+
// generic type argument.
127+
128+
var newJsonOptions = new JsonSerializerOptions(jsonOptions);
129+
newJsonOptions.TypeInfoResolverChain.Clear();
130+
newJsonOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default);
131+
newJsonOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver<DotNetObjectReference<WebRendererInteropMethods>>.Instance);
132+
var argsJson = JsonSerializer.Serialize(args, newJsonOptions);
133+
inProcessRuntime.InvokeJS(JSMethodIdentifier, argsJson, JSCallResultType.JSVoidResult, 0);
134+
}
135+
else
136+
{
137+
jsRuntime.InvokeVoidAsync(JSMethodIdentifier, args).Preserve();
138+
}
139+
}
140+
106141
/// <summary>
107142
/// A collection of JS invokable methods that the JS-side code can use when it needs to
108143
/// make calls in the context of a particular renderer. This object is never exposed to
@@ -145,3 +180,11 @@ public void RemoveRootComponent(int componentId)
145180
=> _jsComponentInterop.RemoveRootComponent(componentId);
146181
}
147182
}
183+
184+
// This should be kept in sync with the argument types in the call to
185+
// 'Blazor._internal.attachWebRendererInterop'
186+
[JsonSerializable(typeof(object[]))]
187+
[JsonSerializable(typeof(int))]
188+
[JsonSerializable(typeof(Dictionary<string, JSComponentConfigurationStore.JSComponentParameter[]>))]
189+
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
190+
internal sealed partial class WebRendererSerializerContext : JsonSerializerContext;

src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
1212

1313
public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
1414
{
15-
private static readonly JsonSerializerOptions JsonOptions = new();
16-
1715
[Fact]
1816
public void CanResolve_AccessTokenProvider()
1917
{
20-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
18+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
2119
builder.Services.AddApiAuthorization();
2220
var host = builder.Build();
2321

@@ -27,7 +25,7 @@ public void CanResolve_AccessTokenProvider()
2725
[Fact]
2826
public void CanResolve_IRemoteAuthenticationService()
2927
{
30-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
28+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
3129
builder.Services.AddApiAuthorization();
3230
var host = builder.Build();
3331

@@ -37,7 +35,7 @@ public void CanResolve_IRemoteAuthenticationService()
3735
[Fact]
3836
public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied()
3937
{
40-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
38+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
4139
builder.Services.AddApiAuthorization();
4240
var host = builder.Build();
4341

@@ -71,7 +69,7 @@ public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied()
7169
[Fact]
7270
public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
7371
{
74-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
72+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
7573
var calls = 0;
7674
builder.Services.AddApiAuthorization(options =>
7775
{
@@ -98,7 +96,7 @@ public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
9896
[Fact]
9997
public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
10098
{
101-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
99+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
102100
var calls = 0;
103101
builder.Services.AddApiAuthorization<TestAuthenticationState>(options => calls++);
104102

@@ -124,7 +122,7 @@ public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
124122
[Fact]
125123
public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration()
126124
{
127-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
125+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
128126
builder.Services.AddApiAuthorization<TestAuthenticationState>();
129127

130128
var host = builder.Build();
@@ -147,7 +145,7 @@ public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfigurati
147145
[Fact]
148146
public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration()
149147
{
150-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
148+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
151149
var calls = 0;
152150
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>(options => calls++);
153151

@@ -173,7 +171,7 @@ public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfigurat
173171
[Fact]
174172
public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration()
175173
{
176-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
174+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
177175
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>();
178176

179177
var host = builder.Build();
@@ -196,7 +194,7 @@ public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpC
196194
[Fact]
197195
public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
198196
{
199-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
197+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
200198
builder.Services.AddApiAuthorization(options =>
201199
{
202200
options.AuthenticationPaths.LogInPath = "a";
@@ -247,7 +245,7 @@ public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
247245
[Fact]
248246
public void OidcOptions_ConfigurationDefaultsGetApplied()
249247
{
250-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
248+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
251249
builder.Services.Replace(ServiceDescriptor.Singleton<NavigationManager, TestNavigationManager>());
252250
builder.Services.AddOidcAuthentication(options => { });
253251
var host = builder.Build();
@@ -286,7 +284,7 @@ public void OidcOptions_ConfigurationDefaultsGetApplied()
286284
[Fact]
287285
public void OidcOptions_DefaultsCanBeOverriden()
288286
{
289-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
287+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
290288
builder.Services.AddOidcAuthentication(options =>
291289
{
292290
options.AuthenticationPaths.LogInPath = "a";
@@ -348,7 +346,7 @@ public void OidcOptions_DefaultsCanBeOverriden()
348346
[Fact]
349347
public void AddOidc_ConfigurationGetsCalledOnce()
350348
{
351-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
349+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
352350
var calls = 0;
353351

354352
builder.Services.AddOidcAuthentication(options => calls++);
@@ -365,7 +363,7 @@ public void AddOidc_ConfigurationGetsCalledOnce()
365363
[Fact]
366364
public void AddOidc_CustomState_SetsUpConfiguration()
367365
{
368-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
366+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
369367
var calls = 0;
370368

371369
builder.Services.AddOidcAuthentication<TestAuthenticationState>(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture));
@@ -387,7 +385,7 @@ public void AddOidc_CustomState_SetsUpConfiguration()
387385
[Fact]
388386
public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
389387
{
390-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
388+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
391389
var calls = 0;
392390

393391
builder.Services.AddOidcAuthentication<TestAuthenticationState, TestAccount>(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture));
@@ -409,7 +407,7 @@ public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
409407
[Fact]
410408
public void OidcProviderOptionsAndDependencies_NotResolvedFromRootScope()
411409
{
412-
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
410+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
413411

414412
var calls = 0;
415413

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,6 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo
227227
operation.Descriptor!.Parameters));
228228
}
229229

230-
WebAssemblyRenderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
230+
renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
231231
}
232232
}

0 commit comments

Comments
 (0)