Skip to content

Commit

Permalink
Use System.Text.Json's source generated code to deserialize WebEvents
Browse files Browse the repository at this point in the history
Also address missing "type" property on TouchEvent

Fixes #32357
  • Loading branch information
pranavkm committed May 15, 2021
1 parent 543eb19 commit 1bb5fe1
Show file tree
Hide file tree
Showing 15 changed files with 168 additions and 40 deletions.
8 changes: 6 additions & 2 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
Expand All @@ -24,6 +25,7 @@ internal class CircuitHost : IAsyncDisposable
private readonly ILogger _logger;
private bool _initialized;
private bool _disposed;
private JsonSourceGeneration.JsonContext _jsonContext;

// This event is fired when there's an unrecoverable exception coming from the circuit, and
// it need so be torn down. The registry listens to this even so that the circuit can
Expand Down Expand Up @@ -404,8 +406,10 @@ public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson
WebEventData webEventData;
try
{
var jsonSerializerOptions = JSRuntime.ReadJsonSerializerOptions();
webEventData = WebEventData.Parse(Renderer, jsonSerializerOptions, eventDescriptorJson, eventArgsJson);
// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
// of the serializer settings.
_jsonContext ??= new(new JsonSerializerOptions(JSRuntime.ReadJsonSerializerOptions()));
webEventData = WebEventData.Parse(Renderer, _jsonContext, eventDescriptorJson, eventArgsJson);
}
catch (Exception ex)
{
Expand Down
12 changes: 12 additions & 0 deletions src/Components/Server/src/JsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.Web;

namespace Microsoft.AspNetCore.Components.Server.JsonSourceGeneration
{
internal partial class JsonContext : JsonSerializerContext, WebEventData.IWebEventJsonSerializerContext
{
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Description>Runtime server features for ASP.NET Core Components.</Description>
Expand All @@ -24,6 +24,9 @@
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Microsoft.Extensions.Logging" />

<!-- Required for S.T.J source generation -->
<Reference Include="System.Text.Json" PrivateAssets="All" />

<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" />
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />

Expand Down
112 changes: 82 additions & 30 deletions src/Components/Shared/src/WebEventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,42 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
using Microsoft.AspNetCore.Components.Web;

[assembly: JsonSerializable(typeof(WebEventDescriptor))]
[assembly: JsonSerializable(typeof(EventArgs))]
[assembly: JsonSerializable(typeof(ChangeEventArgs))]
[assembly: JsonSerializable(typeof(ClipboardEventArgs))]
[assembly: JsonSerializable(typeof(DragEventArgs))]
[assembly: JsonSerializable(typeof(ErrorEventArgs))]
[assembly: JsonSerializable(typeof(FocusEventArgs))]
[assembly: JsonSerializable(typeof(KeyboardEventArgs))]
[assembly: JsonSerializable(typeof(MouseEventArgs))]
[assembly: JsonSerializable(typeof(PointerEventArgs))]
[assembly: JsonSerializable(typeof(ProgressEventArgs))]
[assembly: JsonSerializable(typeof(TouchEventArgs))]
[assembly: JsonSerializable(typeof(WheelEventArgs))]

namespace Microsoft.AspNetCore.Components.Web
{
internal class WebEventData
{
// This class represents the second half of parsing incoming event data,
// once the event ID (and possibly the type of the eventArgs) becomes known.
public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, string eventDescriptorJson, string eventArgsJson)
public static WebEventData Parse(
Renderer renderer,
IWebEventJsonSerializerContext jsonSerializerContext,
string eventDescriptorJson,
string eventArgsJson)
{
WebEventDescriptor eventDescriptor;
try
{
eventDescriptor = Deserialize<WebEventDescriptor>(eventDescriptorJson);
eventDescriptor = Deserialize(eventDescriptorJson, jsonSerializerContext.WebEventDescriptor);
}
catch (Exception e)
{
Expand All @@ -29,14 +50,18 @@ public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSe

return Parse(
renderer,
jsonSerializerOptions,
jsonSerializerContext,
eventDescriptor,
eventArgsJson);
}

public static WebEventData Parse(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, WebEventDescriptor eventDescriptor, string eventArgsJson)
public static WebEventData Parse(
Renderer renderer,
IWebEventJsonSerializerContext jsonSerializerContext,
WebEventDescriptor eventDescriptor,
string eventArgsJson)
{
var parsedEventArgs = ParseEventArgsJson(renderer, jsonSerializerOptions, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
var parsedEventArgs = ParseEventArgsJson(renderer, jsonSerializerContext, eventDescriptor.EventHandlerId, eventDescriptor.EventName, eventArgsJson);
return new WebEventData(
eventDescriptor.BrowserRendererId,
eventDescriptor.EventHandlerId,
Expand All @@ -60,29 +85,35 @@ private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo

public EventArgs EventArgs { get; }

private static EventArgs ParseEventArgsJson(Renderer renderer, JsonSerializerOptions jsonSerializerOptions, ulong eventHandlerId, string eventName, string eventArgsJson)
private static EventArgs ParseEventArgsJson(
Renderer renderer,
IWebEventJsonSerializerContext jsonSerializerContext,
ulong eventHandlerId,
string eventName,
string eventArgsJson)
{
try
{
if (TryDeserializeStandardWebEventArgs(eventName, eventArgsJson, out var eventArgs))
if (TryDeserializeStandardWebEventArgs(eventName, eventArgsJson, jsonSerializerContext, out var eventArgs))
{
return eventArgs;
}

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

[DynamicDependency(JsonSerialized, typeof(DataTransfer))]
[DynamicDependency(JsonSerialized, typeof(DataTransferItem))]
[DynamicDependency(JsonSerialized, typeof(TouchPoint))]
private static bool TryDeserializeStandardWebEventArgs(string eventName, string eventArgsJson, [NotNullWhen(true)] out EventArgs? eventArgs)
private static bool TryDeserializeStandardWebEventArgs(
string eventName,
string eventArgsJson,
IWebEventJsonSerializerContext jsonSerializerContext,
[NotNullWhen(true)] out EventArgs? eventArgs)
{
// For back-compatibility, we recognize the built-in list of web event names and hard-code
// rules about the deserialization type for their eventargs. This makes it possible to declare
Expand All @@ -97,13 +128,13 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
case "change":
// Special case for ChangeEventArgs because its value type can be one of
// several types, and System.Text.Json doesn't pick types dynamically
eventArgs = DeserializeChangeEventArgs(eventArgsJson);
eventArgs = DeserializeChangeEventArgs(eventArgsJson, jsonSerializerContext);
return true;

case "copy":
case "cut":
case "paste":
eventArgs = Deserialize<ClipboardEventArgs>(eventArgsJson);
eventArgs = Deserialize<ClipboardEventArgs>(eventArgsJson, jsonSerializerContext.ClipboardEventArgs);
return true;

case "drag":
Expand All @@ -113,20 +144,20 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
case "dragover":
case "dragstart":
case "drop":
eventArgs = Deserialize<DragEventArgs>(eventArgsJson);
eventArgs = Deserialize<DragEventArgs>(eventArgsJson, jsonSerializerContext.DragEventArgs);
return true;

case "focus":
case "blur":
case "focusin":
case "focusout":
eventArgs = Deserialize<FocusEventArgs>(eventArgsJson);
eventArgs = Deserialize<FocusEventArgs>(eventArgsJson, jsonSerializerContext.FocusEventArgs);
return true;

case "keydown":
case "keyup":
case "keypress":
eventArgs = Deserialize<KeyboardEventArgs>(eventArgsJson);
eventArgs = Deserialize<KeyboardEventArgs>(eventArgsJson, jsonSerializerContext.KeyboardEventArgs);
return true;

case "contextmenu":
Expand All @@ -137,11 +168,11 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
case "mousedown":
case "mouseup":
case "dblclick":
eventArgs = Deserialize<MouseEventArgs>(eventArgsJson);
eventArgs = Deserialize<MouseEventArgs>(eventArgsJson, jsonSerializerContext.MouseEventArgs);
return true;

case "error":
eventArgs = Deserialize<ErrorEventArgs>(eventArgsJson);
eventArgs = Deserialize<ErrorEventArgs>(eventArgsJson, jsonSerializerContext.ErrorEventArgs);
return true;

case "loadstart":
Expand All @@ -150,7 +181,7 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
case "load":
case "loadend":
case "progress":
eventArgs = Deserialize<ProgressEventArgs>(eventArgsJson);
eventArgs = Deserialize<ProgressEventArgs>(eventArgsJson, jsonSerializerContext.ProgressEventArgs);
return true;

case "touchcancel":
Expand All @@ -159,7 +190,7 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
case "touchenter":
case "touchleave":
case "touchstart":
eventArgs = Deserialize<TouchEventArgs>(eventArgsJson);
eventArgs = Deserialize<TouchEventArgs>(eventArgsJson, jsonSerializerContext.TouchEventArgs);
return true;

case "gotpointercapture":
Expand All @@ -172,16 +203,16 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
case "pointerout":
case "pointerover":
case "pointerup":
eventArgs = Deserialize<PointerEventArgs>(eventArgsJson);
eventArgs = Deserialize<PointerEventArgs>(eventArgsJson, jsonSerializerContext.PointerEventArgs);
return true;

case "wheel":
case "mousewheel":
eventArgs = Deserialize<WheelEventArgs>(eventArgsJson);
eventArgs = Deserialize<WheelEventArgs>(eventArgsJson, jsonSerializerContext.WheelEventArgs);
return true;

case "toggle":
eventArgs = Deserialize<EventArgs>(eventArgsJson);
eventArgs = Deserialize<EventArgs>(eventArgsJson, jsonSerializerContext.EventArgs);
return true;

default:
Expand Down Expand Up @@ -219,13 +250,11 @@ private static bool TryDeserializeStandardWebEventArgs(string eventName, string
return null;
}

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

private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson, IWebEventJsonSerializerContext jsonSerializerContext)
{
var changeArgs = Deserialize<ChangeEventArgs>(eventArgsJson);
var changeArgs = Deserialize(eventArgsJson, jsonSerializerContext.ChangeEventArgs);
var jsonElement = (JsonElement)changeArgs.Value!;
switch (jsonElement.ValueKind)
{
Expand All @@ -244,5 +273,28 @@ private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
}
return changeArgs;
}

#nullable disable
// WebView has different nullability settings compared to Server and WebAssembly
// which weirds out JSON's nullability for these types. Disable nullability for this contract
// until we can update everything to haave uniform nullability.
internal interface IWebEventJsonSerializerContext
{
JsonSerializerOptions Options { get; }

JsonTypeInfo<ChangeEventArgs> ChangeEventArgs { get; }
JsonTypeInfo<WebEventDescriptor> WebEventDescriptor { get; }
JsonTypeInfo<ClipboardEventArgs> ClipboardEventArgs { get; }
JsonTypeInfo<DragEventArgs> DragEventArgs { get; }
JsonTypeInfo<FocusEventArgs> FocusEventArgs { get; }
JsonTypeInfo<KeyboardEventArgs> KeyboardEventArgs { get; }
JsonTypeInfo<MouseEventArgs> MouseEventArgs { get; }
JsonTypeInfo<ErrorEventArgs> ErrorEventArgs { get; }
JsonTypeInfo<ProgressEventArgs> ProgressEventArgs { get; }
JsonTypeInfo<TouchEventArgs> TouchEventArgs { get; }
JsonTypeInfo<PointerEventArgs> PointerEventArgs { get; }
JsonTypeInfo<WheelEventArgs> WheelEventArgs { get; }
JsonTypeInfo<EventArgs> EventArgs { get; }
}
}
}
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webview.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Components/Web.JS/src/Rendering/Events/EventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ function parseTouchEvent(event: TouchEvent): TouchEventArgs {
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey,
type: event.type
};
}

Expand Down Expand Up @@ -357,6 +358,7 @@ interface TouchEventArgs {
shiftKey: boolean;
altKey: boolean;
metaKey: boolean;
type: string;
}

interface TouchPoint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
Expand All @@ -18,6 +19,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Infrastructure
[EditorBrowsable(EditorBrowsableState.Never)]
public static class JSInteropMethods
{
private static JsonSourceGeneration.JsonContext? _jsonContext;

/// <summary>
/// For framework use only.
/// </summary>
Expand All @@ -35,7 +38,12 @@ public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string even
{
var renderer = RendererRegistry.Find(eventDescriptor.BrowserRendererId);
var jsonSerializerOptions = DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions();
var webEvent = WebEventData.Parse(renderer, jsonSerializerOptions, eventDescriptor, eventArgsJson);

// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
// of the serializer settings.
_jsonContext ??= new(new JsonSerializerOptions(jsonSerializerOptions));

var webEvent = WebEventData.Parse(renderer, _jsonContext, eventDescriptor, eventArgsJson);
return renderer.DispatchEventAsync(
webEvent.EventHandlerId,
webEvent.EventFieldInfo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.Web;

namespace Microsoft.AspNetCore.Components.WebAssembly.JsonSourceGeneration
{
internal partial class JsonContext : JsonSerializerContext, WebEventData.IWebEventJsonSerializerContext
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<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>
<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>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand Down Expand Up @@ -53,7 +53,7 @@
<argument>ILLink</argument>
<argument>IL2072</argument>
<property name="Scope">member</property>
<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>
<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>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Description>Build client-side single-page applications (SPAs) with Blazor running under WebAssembly.</Description>
<NoWarn>$(NoWarn);BL0006</NoWarn>
<!-- Workaround for https://github.com/dotnet/runtime/issues/52227 -->
<NoWarn>$(NoWarn);CS8603</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
<Trimmable>true</Trimmable>
Expand Down
Loading

0 comments on commit 1bb5fe1

Please sign in to comment.