Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 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 WebEventJsonContext _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 @@ -423,8 +425,14 @@ 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.
if (_jsonContext is null)
{
_jsonContext = new(new JsonSerializerOptions(JSRuntime.ReadJsonSerializerOptions()));
}

webEventData = WebEventData.Parse(Renderer, _jsonContext, eventDescriptorJson, eventArgsJson);
}
catch (Exception ex)
{
Expand Down
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 @@ -23,7 +23,9 @@
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
<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
91 changes: 61 additions & 30 deletions src/Components/Shared/src/WebEventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@
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.RenderTree;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

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,
WebEventJsonContext jsonSerializerContext,
string eventDescriptorJson,
string eventArgsJson)
{
WebEventDescriptor eventDescriptor;
try
{
eventDescriptor = Deserialize<WebEventDescriptor>(eventDescriptorJson);
eventDescriptor = Deserialize(eventDescriptorJson, jsonSerializerContext.WebEventDescriptor);
}
catch (Exception e)
{
Expand All @@ -29,14 +34,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,
WebEventJsonContext 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 +69,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,
WebEventJsonContext 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,
WebEventJsonContext 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 +112,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 +128,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 +152,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 +165,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 +174,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 +187,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 +234,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, WebEventJsonContext jsonSerializerContext)
{
var changeArgs = Deserialize<ChangeEventArgs>(eventArgsJson);
var changeArgs = Deserialize(eventArgsJson, jsonSerializerContext.ChangeEventArgs);
var jsonElement = (JsonElement)changeArgs.Value!;
switch (jsonElement.ValueKind)
{
Expand All @@ -245,4 +258,22 @@ private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson)
return changeArgs;
}
}

[JsonSerializable(typeof(WebEventDescriptor))]
[JsonSerializable(typeof(WebEventDescriptor))]
[JsonSerializable(typeof(EventArgs))]
[JsonSerializable(typeof(ChangeEventArgs))]
[JsonSerializable(typeof(ClipboardEventArgs))]
[JsonSerializable(typeof(DragEventArgs))]
[JsonSerializable(typeof(ErrorEventArgs))]
[JsonSerializable(typeof(FocusEventArgs))]
[JsonSerializable(typeof(KeyboardEventArgs))]
[JsonSerializable(typeof(MouseEventArgs))]
[JsonSerializable(typeof(PointerEventArgs))]
[JsonSerializable(typeof(ProgressEventArgs))]
[JsonSerializable(typeof(TouchEventArgs))]
[JsonSerializable(typeof(WheelEventArgs))]
internal sealed partial class WebEventJsonContext : JsonSerializerContext
Copy link

Choose a reason for hiding this comment

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

It looks like it would be cleaner to have a global JsonSourceGenerationMode option per context:

public class JsonSourceGenerationModeAttribute : JsonAttribute
{
    public JsonSourceGenerationModeAttribute(JsonSourceGenerationMode mode) { }
}

Building on #32374 (comment), we'd have

[JsonSourceGenerationMode(JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(WebEventDescriptor)]
[JsonSerializable(typeof(EventArgs)]
internal sealed partial class WebEventJsonContext : JsonSerializerContext
{
}

[JsonSerializable] instances with an Unspecified mode would fallback to the context-global mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does seem like a better alternative. It feels unusual to want fine-grained control over the serialization behavior on a per-type basis. That said, I imagine most users wouldn't really configure this since the extra IL for serialization isn't really noticeable.

{
}
}
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
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
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 WebEventJsonContext? _jsonContext;

/// <summary>
/// For framework use only.
/// </summary>
Expand All @@ -34,8 +37,16 @@ public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string eventArgsJson)
{
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.
if (_jsonContext is null)
{
var jsonSerializerOptions = DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions();
_jsonContext = new(new JsonSerializerOptions(jsonSerializerOptions));
Copy link
Member

Choose a reason for hiding this comment

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

Can we move the ReadJsonSerializerOptions call inside this if block so it's only done once? Super minor detail I know, but helps to clarify that we only respect its value the first time, and it can't change after.

}

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
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.WebEventJsonContext,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
13 changes: 11 additions & 2 deletions src/Components/WebView/WebView/src/IpcReceiver.cs
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;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop.Infrastructure;
Expand All @@ -26,6 +27,7 @@ namespace Microsoft.AspNetCore.Components.WebView
internal class IpcReceiver
{
private readonly Func<string, string, Task> _onAttachMessage;
private WebEventJsonContext _jsonContext;

public IpcReceiver(Func<string,string,Task> onAttachMessage)
{
Expand Down Expand Up @@ -88,8 +90,15 @@ private void EndInvokeJS(PageContext pageContext, long asyncHandle, bool succeed
private Task DispatchBrowserEventAsync(PageContext pageContext, string eventDescriptor, string eventArgs)
{
var renderer = pageContext.Renderer;
var jsonSerializerOptions = pageContext.JSRuntime.ReadJsonSerializerOptions();
var webEventData = WebEventData.Parse(renderer, jsonSerializerOptions, eventDescriptor, eventArgs);
// JsonSerializerOptions are tightly bound to the JsonContext. Cache it on first use using a copy
// of the serializer settings.
if (_jsonContext is null)
{
var jsonSerializerOptions = pageContext.JSRuntime.ReadJsonSerializerOptions();
_jsonContext = new(new JsonSerializerOptions(jsonSerializerOptions));
}

var webEventData = WebEventData.Parse(renderer, _jsonContext, eventDescriptor, eventArgs);
return renderer.DispatchEventAsync(
webEventData.EventHandlerId,
webEventData.EventFieldInfo,
Expand Down
Loading