Skip to content

Commit fcd4ed7

Browse files
authored
Use System.Text.Json's source generated code to deserialize WebEvents (#32374)
* Use System.Text.Json's source generated code to deserialize WebEvents Also address missing "type" property on TouchEvent Contributes to #32357
1 parent f22a5c3 commit fcd4ed7

File tree

13 files changed

+125
-43
lines changed

13 files changed

+125
-43
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Globalization;
77
using System.Security.Claims;
8+
using System.Text.Json;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.AspNetCore.Components.Authorization;
@@ -24,6 +25,7 @@ internal class CircuitHost : IAsyncDisposable
2425
private readonly ILogger _logger;
2526
private bool _initialized;
2627
private bool _disposed;
28+
private WebEventJsonContext _jsonContext;
2729

2830
// This event is fired when there's an unrecoverable exception coming from the circuit, and
2931
// it need so be torn down. The registry listens to this even so that the circuit can
@@ -423,8 +425,14 @@ public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson
423425
WebEventData webEventData;
424426
try
425427
{
426-
var jsonSerializerOptions = JSRuntime.ReadJsonSerializerOptions();
427-
webEventData = WebEventData.Parse(Renderer, jsonSerializerOptions, eventDescriptorJson, eventArgsJson);
428+
// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
429+
// of the serializer settings.
430+
if (_jsonContext is null)
431+
{
432+
_jsonContext = new(new JsonSerializerOptions(JSRuntime.ReadJsonSerializerOptions()));
433+
}
434+
435+
webEventData = WebEventData.Parse(Renderer, _jsonContext, eventDescriptorJson, eventArgsJson);
428436
}
429437
catch (Exception ex)
430438
{

src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
44
<Description>Runtime server features for ASP.NET Core Components.</Description>
@@ -23,7 +23,9 @@
2323
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
2424
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
2525
<Reference Include="Microsoft.Extensions.Logging" />
26-
26+
<!-- Required for S.T.J source generation -->
27+
<Reference Include="System.Text.Json" PrivateAssets="All" />
28+
2729
<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" />
2830
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
2931

src/Components/Shared/src/WebEventData.cs

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,26 @@
66
using System;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
using System.Text.Json.Serialization.Metadata;
911
using Microsoft.AspNetCore.Components.RenderTree;
10-
using static Microsoft.AspNetCore.Internal.LinkerFlags;
1112

1213
namespace Microsoft.AspNetCore.Components.Web
1314
{
1415
internal class WebEventData
1516
{
1617
// This class represents the second half of parsing incoming event data,
1718
// once the event ID (and possibly the type of the eventArgs) becomes known.
18-
public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, string eventDescriptorJson, string eventArgsJson)
19+
public static WebEventData Parse(
20+
Renderer renderer,
21+
WebEventJsonContext jsonSerializerContext,
22+
string eventDescriptorJson,
23+
string eventArgsJson)
1924
{
2025
WebEventDescriptor eventDescriptor;
2126
try
2227
{
23-
eventDescriptor = Deserialize<WebEventDescriptor>(eventDescriptorJson);
28+
eventDescriptor = Deserialize(eventDescriptorJson, jsonSerializerContext.WebEventDescriptor);
2429
}
2530
catch (Exception e)
2631
{
@@ -29,14 +34,18 @@ public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSe
2934

3035
return Parse(
3136
renderer,
32-
jsonSerializerOptions,
37+
jsonSerializerContext,
3338
eventDescriptor,
3439
eventArgsJson);
3540
}
3641

37-
public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, WebEventDescriptor eventDescriptor, string eventArgsJson)
42+
public static WebEventData Parse(
43+
Renderer renderer,
44+
WebEventJsonContext jsonSerializerContext,
45+
WebEventDescriptor eventDescriptor,
46+
string eventArgsJson)
3847
{
39-
var parsedEventArgs = ParseEventArgsJson(renderer, jsonSerializerOptions, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
48+
var parsedEventArgs = ParseEventArgsJson(renderer, jsonSerializerContext, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
4049
return new WebEventData(
4150
eventDescriptor.BrowserRendererId,
4251
eventDescriptor.EventHandlerId,
@@ -60,29 +69,35 @@ private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo
6069

6170
public EventArgs EventArgs { get; }
6271

63-
private static EventArgs ParseEventArgsJson(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, ulong eventHandlerId, string eventName, string eventArgsJson)
72+
private static EventArgs ParseEventArgsJson(
73+
Renderer renderer,
74+
WebEventJsonContext jsonSerializerContext,
75+
ulong eventHandlerId,
76+
string eventName,
77+
string eventArgsJson)
6478
{
6579
try
6680
{
67-
if (TryDeserializeStandardWebEventArgs(eventName, eventArgsJson, out var eventArgs))
81+
if (TryDeserializeStandardWebEventArgs(eventName, eventArgsJson, jsonSerializerContext, out var eventArgs))
6882
{
6983
return eventArgs;
7084
}
7185

7286
// For custom events, the args type is determined from the associated delegate
7387
var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
74-
return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, jsonSerializerOptions)!;
88+
return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, jsonSerializerContext.Options)!;
7589
}
7690
catch (Exception e)
7791
{
7892
throw new InvalidOperationException($"There was an error parsing the event arguments. EventId: '{eventHandlerId}'.", e);
7993
}
8094
}
8195

82-
[DynamicDependency(JsonSerialized, typeof(DataTransfer))]
83-
[DynamicDependency(JsonSerialized, typeof(DataTransferItem))]
84-
[DynamicDependency(JsonSerialized, typeof(TouchPoint))]
85-
private static bool TryDeserializeStandardWebEventArgs(string eventName, string eventArgsJson, [NotNullWhen(true)] out EventArgs? eventArgs)
96+
private static bool TryDeserializeStandardWebEventArgs(
97+
string eventName,
98+
string eventArgsJson,
99+
WebEventJsonContext jsonSerializerContext,
100+
[NotNullWhen(true)] out EventArgs? eventArgs)
86101
{
87102
// For back-compatibility, we recognize the built-in list of web event names and hard-code
88103
// rules about the deserialization type for their eventargs. This makes it possible to declare
@@ -97,13 +112,13 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
97112
case "change":
98113
// Special case for ChangeEventArgs because its value type can be one of
99114
// several types, and System.Text.Json doesn't pick types dynamically
100-
eventArgs = DeserializeChangeEventArgs(eventArgsJson);
115+
eventArgs = DeserializeChangeEventArgs(eventArgsJson, jsonSerializerContext);
101116
return true;
102117

103118
case "copy":
104119
case "cut":
105120
case "paste":
106-
eventArgs = Deserialize<ClipboardEventArgs>(eventArgsJson);
121+
eventArgs = Deserialize<ClipboardEventArgs>(eventArgsJson, jsonSerializerContext.ClipboardEventArgs);
107122
return true;
108123

109124
case "drag":
@@ -113,20 +128,20 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
113128
case "dragover":
114129
case "dragstart":
115130
case "drop":
116-
eventArgs = Deserialize<DragEventArgs>(eventArgsJson);
131+
eventArgs = Deserialize<DragEventArgs>(eventArgsJson, jsonSerializerContext.DragEventArgs);
117132
return true;
118133

119134
case "focus":
120135
case "blur":
121136
case "focusin":
122137
case "focusout":
123-
eventArgs = Deserialize<FocusEventArgs>(eventArgsJson);
138+
eventArgs = Deserialize<FocusEventArgs>(eventArgsJson, jsonSerializerContext.FocusEventArgs);
124139
return true;
125140

126141
case "keydown":
127142
case "keyup":
128143
case "keypress":
129-
eventArgs = Deserialize<KeyboardEventArgs>(eventArgsJson);
144+
eventArgs = Deserialize<KeyboardEventArgs>(eventArgsJson, jsonSerializerContext.KeyboardEventArgs);
130145
return true;
131146

132147
case "contextmenu":
@@ -137,11 +152,11 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
137152
case "mousedown":
138153
case "mouseup":
139154
case "dblclick":
140-
eventArgs = Deserialize<MouseEventArgs>(eventArgsJson);
155+
eventArgs = Deserialize<MouseEventArgs>(eventArgsJson, jsonSerializerContext.MouseEventArgs);
141156
return true;
142157

143158
case "error":
144-
eventArgs = Deserialize<ErrorEventArgs>(eventArgsJson);
159+
eventArgs = Deserialize<ErrorEventArgs>(eventArgsJson, jsonSerializerContext.ErrorEventArgs);
145160
return true;
146161

147162
case "loadstart":
@@ -150,7 +165,7 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
150165
case "load":
151166
case "loadend":
152167
case "progress":
153-
eventArgs = Deserialize<ProgressEventArgs>(eventArgsJson);
168+
eventArgs = Deserialize<ProgressEventArgs>(eventArgsJson, jsonSerializerContext.ProgressEventArgs);
154169
return true;
155170

156171
case "touchcancel":
@@ -159,7 +174,7 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
159174
case "touchenter":
160175
case "touchleave":
161176
case "touchstart":
162-
eventArgs = Deserialize<TouchEventArgs>(eventArgsJson);
177+
eventArgs = Deserialize<TouchEventArgs>(eventArgsJson, jsonSerializerContext.TouchEventArgs);
163178
return true;
164179

165180
case "gotpointercapture":
@@ -172,16 +187,16 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
172187
case "pointerout":
173188
case "pointerover":
174189
case "pointerup":
175-
eventArgs = Deserialize<PointerEventArgs>(eventArgsJson);
190+
eventArgs = Deserialize<PointerEventArgs>(eventArgsJson, jsonSerializerContext.PointerEventArgs);
176191
return true;
177192

178193
case "wheel":
179194
case "mousewheel":
180-
eventArgs = Deserialize<WheelEventArgs>(eventArgsJson);
195+
eventArgs = Deserialize<WheelEventArgs>(eventArgsJson, jsonSerializerContext.WheelEventArgs);
181196
return true;
182197

183198
case "toggle":
184-
eventArgs = Deserialize<EventArgs>(eventArgsJson);
199+
eventArgs = Deserialize<EventArgs>(eventArgsJson, jsonSerializerContext.EventArgs);
185200
return true;
186201

187202
default:
@@ -219,13 +234,11 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
219234
return null;
220235
}
221236

222-
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The correct members are preserved by DynamicDependencies.")]
223-
// This should use JSON source generation
224-
static T Deserialize<[DynamicallyAccessedMembers(JsonSerialized)] T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializerOptionsProvider.Options)!;
237+
static T Deserialize<T>(string json, JsonTypeInfo<T?> jsonTypeInfo) => JsonSerializer.Deserialize(json, jsonTypeInfo)!;
225238

226-
private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
239+
private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson, WebEventJsonContext jsonSerializerContext)
227240
{
228-
var changeArgs = Deserialize<ChangeEventArgs>(eventArgsJson);
241+
var changeArgs = Deserialize(eventArgsJson, jsonSerializerContext.ChangeEventArgs);
229242
var jsonElement = (JsonElement)changeArgs.Value!;
230243
switch (jsonElement.ValueKind)
231244
{
@@ -245,4 +258,22 @@ private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
245258
return changeArgs;
246259
}
247260
}
261+
262+
[JsonSerializable(typeof(WebEventDescriptor))]
263+
[JsonSerializable(typeof(WebEventDescriptor))]
264+
[JsonSerializable(typeof(EventArgs))]
265+
[JsonSerializable(typeof(ChangeEventArgs))]
266+
[JsonSerializable(typeof(ClipboardEventArgs))]
267+
[JsonSerializable(typeof(DragEventArgs))]
268+
[JsonSerializable(typeof(ErrorEventArgs))]
269+
[JsonSerializable(typeof(FocusEventArgs))]
270+
[JsonSerializable(typeof(KeyboardEventArgs))]
271+
[JsonSerializable(typeof(MouseEventArgs))]
272+
[JsonSerializable(typeof(PointerEventArgs))]
273+
[JsonSerializable(typeof(ProgressEventArgs))]
274+
[JsonSerializable(typeof(TouchEventArgs))]
275+
[JsonSerializable(typeof(WheelEventArgs))]
276+
internal sealed partial class WebEventJsonContext : JsonSerializerContext
277+
{
278+
}
248279
}

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Rendering/Events/EventTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ function parseTouchEvent(event: TouchEvent): TouchEventArgs {
144144
shiftKey: event.shiftKey,
145145
altKey: event.altKey,
146146
metaKey: event.metaKey,
147+
type: event.type
147148
};
148149
}
149150

@@ -357,6 +358,7 @@ interface TouchEventArgs {
357358
shiftKey: boolean;
358359
altKey: boolean;
359360
metaKey: boolean;
361+
type: string;
360362
}
361363

362364
interface TouchPoint {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>

src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.ComponentModel;
5+
using System.Text.Json;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Components.RenderTree;
78
using Microsoft.AspNetCore.Components.Web;
@@ -18,6 +19,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Infrastructure
1819
[EditorBrowsable(EditorBrowsableState.Never)]
1920
public static class JSInteropMethods
2021
{
22+
private static WebEventJsonContext? _jsonContext;
23+
2124
/// <summary>
2225
/// For framework use only.
2326
/// </summary>
@@ -34,8 +37,16 @@ public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
3437
public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string eventArgsJson)
3538
{
3639
var renderer = RendererRegistry.Find(eventDescriptor.BrowserRendererId);
37-
var jsonSerializerOptions = DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions();
38-
var webEvent = WebEventData.Parse(renderer, jsonSerializerOptions, eventDescriptor, eventArgsJson);
40+
41+
// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
42+
// of the serializer settings.
43+
if (_jsonContext is null)
44+
{
45+
var jsonSerializerOptions = DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions();
46+
_jsonContext = new(new JsonSerializerOptions(jsonSerializerOptions));
47+
}
48+
49+
var webEvent = WebEventData.Parse(renderer, _jsonContext, eventDescriptor, eventArgsJson);
3950
return renderer.DispatchEventAsync(
4051
webEvent.EventHandlerId,
4152
webEvent.EventFieldInfo,

src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<argument>ILLink</argument>
1818
<argument>IL2026</argument>
1919
<property name="Scope">member</property>
20-
<property name="Target">M:Microsoft.AspNetCore.Components.Web.WebEventData.ParseEventArgsJson(Microsoft.AspNetCore.Components.RenderTree.Renderer,System.Text.Json.JsonSerializerOptions,System.UInt64,System.String,System.String)</property>
20+
<property name="Target">M:Microsoft.AspNetCore.Components.Web.WebEventData.ParseEventArgsJson(Microsoft.AspNetCore.Components.RenderTree.Renderer,Microsoft.AspNetCore.Components.Web.WebEventJsonContext,System.UInt64,System.String,System.String)</property>
2121
</attribute>
2222
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
2323
<argument>ILLink</argument>

src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
55
<Description>Build client-side single-page applications (SPAs) with Blazor running under WebAssembly.</Description>
66
<NoWarn>$(NoWarn);BL0006</NoWarn>
7+
<!-- Workaround for https://github.com/dotnet/runtime/issues/52227 -->
8+
<NoWarn>$(NoWarn);CS8603</NoWarn>
79
<GenerateDocumentationFile>true</GenerateDocumentationFile>
810
<Nullable>enable</Nullable>
911
<Trimmable>true</Trimmable>

0 commit comments

Comments
 (0)