Skip to content

Commit 1bb5fe1

Browse files
committed
Use System.Text.Json's source generated code to deserialize WebEvents
Also address missing "type" property on TouchEvent Fixes #32357
1 parent 543eb19 commit 1bb5fe1

File tree

15 files changed

+168
-40
lines changed

15 files changed

+168
-40
lines changed

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

Lines changed: 6 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 JsonSourceGeneration.JsonContext _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
@@ -404,8 +406,10 @@ public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson
404406
WebEventData webEventData;
405407
try
406408
{
407-
var jsonSerializerOptions = JSRuntime.ReadJsonSerializerOptions();
408-
webEventData = WebEventData.Parse(Renderer, jsonSerializerOptions, eventDescriptorJson, eventArgsJson);
409+
// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
410+
// of the serializer settings.
411+
_jsonContext ??= new(new JsonSerializerOptions(JSRuntime.ReadJsonSerializerOptions()));
412+
webEventData = WebEventData.Parse(Renderer, _jsonContext, eventDescriptorJson, eventArgsJson);
409413
}
410414
catch (Exception ex)
411415
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Text.Json.Serialization;
5+
using Microsoft.AspNetCore.Components.Web;
6+
7+
namespace Microsoft.AspNetCore.Components.Server.JsonSourceGeneration
8+
{
9+
internal partial class JsonContext : JsonSerializerContext, WebEventData.IWebEventJsonSerializerContext
10+
{
11+
}
12+
}

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

Lines changed: 4 additions & 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
<PropertyGroup>
33
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
44
<Description>Runtime server features for ASP.NET Core Components.</Description>
@@ -24,6 +24,9 @@
2424
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
2525
<Reference Include="Microsoft.Extensions.Logging" />
2626

27+
<!-- Required for S.T.J source generation -->
28+
<Reference Include="System.Text.Json" PrivateAssets="All" />
29+
2730
<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" />
2831
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
2932

src/Components/Shared/src/WebEventData.cs

Lines changed: 82 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,42 @@
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;
11+
using Microsoft.AspNetCore.Components;
912
using Microsoft.AspNetCore.Components.RenderTree;
10-
using static Microsoft.AspNetCore.Internal.LinkerFlags;
13+
using Microsoft.AspNetCore.Components.Web;
14+
15+
[assembly: JsonSerializable(typeof(WebEventDescriptor))]
16+
[assembly: JsonSerializable(typeof(EventArgs))]
17+
[assembly: JsonSerializable(typeof(ChangeEventArgs))]
18+
[assembly: JsonSerializable(typeof(ClipboardEventArgs))]
19+
[assembly: JsonSerializable(typeof(DragEventArgs))]
20+
[assembly: JsonSerializable(typeof(ErrorEventArgs))]
21+
[assembly: JsonSerializable(typeof(FocusEventArgs))]
22+
[assembly: JsonSerializable(typeof(KeyboardEventArgs))]
23+
[assembly: JsonSerializable(typeof(MouseEventArgs))]
24+
[assembly: JsonSerializable(typeof(PointerEventArgs))]
25+
[assembly: JsonSerializable(typeof(ProgressEventArgs))]
26+
[assembly: JsonSerializable(typeof(TouchEventArgs))]
27+
[assembly: JsonSerializable(typeof(WheelEventArgs))]
1128

1229
namespace Microsoft.AspNetCore.Components.Web
1330
{
1431
internal class WebEventData
1532
{
1633
// This class represents the second half of parsing incoming event data,
1734
// 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)
35+
public static WebEventData Parse(
36+
Renderer renderer,
37+
IWebEventJsonSerializerContext jsonSerializerContext,
38+
string eventDescriptorJson,
39+
string eventArgsJson)
1940
{
2041
WebEventDescriptor eventDescriptor;
2142
try
2243
{
23-
eventDescriptor = Deserialize<WebEventDescriptor>(eventDescriptorJson);
44+
eventDescriptor = Deserialize(eventDescriptorJson, jsonSerializerContext.WebEventDescriptor);
2445
}
2546
catch (Exception e)
2647
{
@@ -29,14 +50,18 @@ public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSe
2950

3051
return Parse(
3152
renderer,
32-
jsonSerializerOptions,
53+
jsonSerializerContext,
3354
eventDescriptor,
3455
eventArgsJson);
3556
}
3657

37-
public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, WebEventDescriptor eventDescriptor, string eventArgsJson)
58+
public static WebEventData Parse(
59+
Renderer renderer,
60+
IWebEventJsonSerializerContext jsonSerializerContext,
61+
WebEventDescriptor eventDescriptor,
62+
string eventArgsJson)
3863
{
39-
var parsedEventArgs = ParseEventArgsJson(renderer, jsonSerializerOptions, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
64+
var parsedEventArgs = ParseEventArgsJson(renderer, jsonSerializerContext, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
4065
return new WebEventData(
4166
eventDescriptor.BrowserRendererId,
4267
eventDescriptor.EventHandlerId,
@@ -60,29 +85,35 @@ private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo
6085

6186
public EventArgs EventArgs { get; }
6287

63-
private static EventArgs ParseEventArgsJson(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, ulong eventHandlerId, string eventName, string eventArgsJson)
88+
private static EventArgs ParseEventArgsJson(
89+
Renderer renderer,
90+
IWebEventJsonSerializerContext jsonSerializerContext,
91+
ulong eventHandlerId,
92+
string eventName,
93+
string eventArgsJson)
6494
{
6595
try
6696
{
67-
if (TryDeserializeStandardWebEventArgs(eventName, eventArgsJson, out var eventArgs))
97+
if (TryDeserializeStandardWebEventArgs(eventName, eventArgsJson, jsonSerializerContext, out var eventArgs))
6898
{
6999
return eventArgs;
70100
}
71101

72102
// For custom events, the args type is determined from the associated delegate
73103
var eventArgsType = renderer.GetEventArgsType(eventHandlerId);
74-
return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, jsonSerializerOptions)!;
104+
return (EventArgs)JsonSerializer.Deserialize(eventArgsJson, eventArgsType, jsonSerializerContext.Options)!;
75105
}
76106
catch (Exception e)
77107
{
78108
throw new InvalidOperationException($"There was an error parsing the event arguments. EventId: '{eventHandlerId}'.", e);
79109
}
80110
}
81111

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)
112+
private static bool TryDeserializeStandardWebEventArgs(
113+
string eventName,
114+
string eventArgsJson,
115+
IWebEventJsonSerializerContext jsonSerializerContext,
116+
[NotNullWhen(true)] out EventArgs? eventArgs)
86117
{
87118
// For back-compatibility, we recognize the built-in list of web event names and hard-code
88119
// rules about the deserialization type for their eventargs. This makes it possible to declare
@@ -97,13 +128,13 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
97128
case "change":
98129
// Special case for ChangeEventArgs because its value type can be one of
99130
// several types, and System.Text.Json doesn't pick types dynamically
100-
eventArgs = DeserializeChangeEventArgs(eventArgsJson);
131+
eventArgs = DeserializeChangeEventArgs(eventArgsJson, jsonSerializerContext);
101132
return true;
102133

103134
case "copy":
104135
case "cut":
105136
case "paste":
106-
eventArgs = Deserialize<ClipboardEventArgs>(eventArgsJson);
137+
eventArgs = Deserialize<ClipboardEventArgs>(eventArgsJson, jsonSerializerContext.ClipboardEventArgs);
107138
return true;
108139

109140
case "drag":
@@ -113,20 +144,20 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
113144
case "dragover":
114145
case "dragstart":
115146
case "drop":
116-
eventArgs = Deserialize<DragEventArgs>(eventArgsJson);
147+
eventArgs = Deserialize<DragEventArgs>(eventArgsJson, jsonSerializerContext.DragEventArgs);
117148
return true;
118149

119150
case "focus":
120151
case "blur":
121152
case "focusin":
122153
case "focusout":
123-
eventArgs = Deserialize<FocusEventArgs>(eventArgsJson);
154+
eventArgs = Deserialize<FocusEventArgs>(eventArgsJson, jsonSerializerContext.FocusEventArgs);
124155
return true;
125156

126157
case "keydown":
127158
case "keyup":
128159
case "keypress":
129-
eventArgs = Deserialize<KeyboardEventArgs>(eventArgsJson);
160+
eventArgs = Deserialize<KeyboardEventArgs>(eventArgsJson, jsonSerializerContext.KeyboardEventArgs);
130161
return true;
131162

132163
case "contextmenu":
@@ -137,11 +168,11 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
137168
case "mousedown":
138169
case "mouseup":
139170
case "dblclick":
140-
eventArgs = Deserialize<MouseEventArgs>(eventArgsJson);
171+
eventArgs = Deserialize<MouseEventArgs>(eventArgsJson, jsonSerializerContext.MouseEventArgs);
141172
return true;
142173

143174
case "error":
144-
eventArgs = Deserialize<ErrorEventArgs>(eventArgsJson);
175+
eventArgs = Deserialize<ErrorEventArgs>(eventArgsJson, jsonSerializerContext.ErrorEventArgs);
145176
return true;
146177

147178
case "loadstart":
@@ -150,7 +181,7 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
150181
case "load":
151182
case "loadend":
152183
case "progress":
153-
eventArgs = Deserialize<ProgressEventArgs>(eventArgsJson);
184+
eventArgs = Deserialize<ProgressEventArgs>(eventArgsJson, jsonSerializerContext.ProgressEventArgs);
154185
return true;
155186

156187
case "touchcancel":
@@ -159,7 +190,7 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
159190
case "touchenter":
160191
case "touchleave":
161192
case "touchstart":
162-
eventArgs = Deserialize<TouchEventArgs>(eventArgsJson);
193+
eventArgs = Deserialize<TouchEventArgs>(eventArgsJson, jsonSerializerContext.TouchEventArgs);
163194
return true;
164195

165196
case "gotpointercapture":
@@ -172,16 +203,16 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
172203
case "pointerout":
173204
case "pointerover":
174205
case "pointerup":
175-
eventArgs = Deserialize<PointerEventArgs>(eventArgsJson);
206+
eventArgs = Deserialize<PointerEventArgs>(eventArgsJson, jsonSerializerContext.PointerEventArgs);
176207
return true;
177208

178209
case "wheel":
179210
case "mousewheel":
180-
eventArgs = Deserialize<WheelEventArgs>(eventArgsJson);
211+
eventArgs = Deserialize<WheelEventArgs>(eventArgsJson, jsonSerializerContext.WheelEventArgs);
181212
return true;
182213

183214
case "toggle":
184-
eventArgs = Deserialize<EventArgs>(eventArgsJson);
215+
eventArgs = Deserialize<EventArgs>(eventArgsJson, jsonSerializerContext.EventArgs);
185216
return true;
186217

187218
default:
@@ -219,13 +250,11 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
219250
return null;
220251
}
221252

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)!;
253+
static T Deserialize<T>(string json, JsonTypeInfo<T?> jsonTypeInfo) => JsonSerializer.Deserialize(json, jsonTypeInfo)!;
225254

226-
private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
255+
private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson, IWebEventJsonSerializerContext jsonSerializerContext)
227256
{
228-
var changeArgs = Deserialize<ChangeEventArgs>(eventArgsJson);
257+
var changeArgs = Deserialize(eventArgsJson, jsonSerializerContext.ChangeEventArgs);
229258
var jsonElement = (JsonElement)changeArgs.Value!;
230259
switch (jsonElement.ValueKind)
231260
{
@@ -244,5 +273,28 @@ private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
244273
}
245274
return changeArgs;
246275
}
276+
277+
#nullable disable
278+
// WebView has different nullability settings compared to Server and WebAssembly
279+
// which weirds out JSON's nullability for these types. Disable nullability for this contract
280+
// until we can update everything to haave uniform nullability.
281+
internal interface IWebEventJsonSerializerContext
282+
{
283+
JsonSerializerOptions Options { get; }
284+
285+
JsonTypeInfo<ChangeEventArgs> ChangeEventArgs { get; }
286+
JsonTypeInfo<WebEventDescriptor> WebEventDescriptor { get; }
287+
JsonTypeInfo<ClipboardEventArgs> ClipboardEventArgs { get; }
288+
JsonTypeInfo<DragEventArgs> DragEventArgs { get; }
289+
JsonTypeInfo<FocusEventArgs> FocusEventArgs { get; }
290+
JsonTypeInfo<KeyboardEventArgs> KeyboardEventArgs { get; }
291+
JsonTypeInfo<MouseEventArgs> MouseEventArgs { get; }
292+
JsonTypeInfo<ErrorEventArgs> ErrorEventArgs { get; }
293+
JsonTypeInfo<ProgressEventArgs> ProgressEventArgs { get; }
294+
JsonTypeInfo<TouchEventArgs> TouchEventArgs { get; }
295+
JsonTypeInfo<PointerEventArgs> PointerEventArgs { get; }
296+
JsonTypeInfo<WheelEventArgs> WheelEventArgs { get; }
297+
JsonTypeInfo<EventArgs> EventArgs { get; }
298+
}
247299
}
248300
}

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/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs

Lines changed: 9 additions & 1 deletion
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 JsonSourceGeneration.JsonContext? _jsonContext;
23+
2124
/// <summary>
2225
/// For framework use only.
2326
/// </summary>
@@ -35,7 +38,12 @@ public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string even
3538
{
3639
var renderer = RendererRegistry.Find(eventDescriptor.BrowserRendererId);
3740
var jsonSerializerOptions = DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions();
38-
var webEvent = WebEventData.Parse(renderer, jsonSerializerOptions, eventDescriptor, eventArgsJson);
41+
42+
// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
43+
// of the serializer settings.
44+
_jsonContext ??= new(new JsonSerializerOptions(jsonSerializerOptions));
45+
46+
var webEvent = WebEventData.Parse(renderer, _jsonContext, eventDescriptor, eventArgsJson);
3947
return renderer.DispatchEventAsync(
4048
webEvent.EventHandlerId,
4149
webEvent.EventFieldInfo,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Text.Json.Serialization;
5+
using Microsoft.AspNetCore.Components.Web;
6+
7+
namespace Microsoft.AspNetCore.Components.WebAssembly.JsonSourceGeneration
8+
{
9+
internal partial class JsonContext : JsonSerializerContext, WebEventData.IWebEventJsonSerializerContext
10+
{
11+
}
12+
}

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

Lines changed: 2 additions & 2 deletions
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.WebEventData.IWebEventJsonSerializerContext,System.UInt64,System.String,System.String)</property>
2121
</attribute>
2222
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
2323
<argument>ILLink</argument>
@@ -53,7 +53,7 @@
5353
<argument>ILLink</argument>
5454
<argument>IL2072</argument>
5555
<property name="Scope">member</property>
56-
<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>
56+
<property name="Target">M:Microsoft.AspNetCore.Components.Web.WebEventData.ParseEventArgsJson(Microsoft.AspNetCore.Components.RenderTree.Renderer,Microsoft.AspNetCore.Components.Web.WebEventData.IWebEventJsonSerializerContext,System.UInt64,System.String,System.String)</property>
5757
</attribute>
5858
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
5959
<argument>ILLink</argument>

0 commit comments

Comments
 (0)