Skip to content

Commit 9995e90

Browse files
committed
Refactored and Guarded Serialization
1 parent a0dc457 commit 9995e90

File tree

6 files changed

+78
-64
lines changed

6 files changed

+78
-64
lines changed

src/Components/Server/src/Circuits/RemoteJSRuntime.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in
7575
}
7676
}
7777

78-
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, byte[][]? byteArrays, JSCallResultType resultType, long targetInstanceId)
78+
protected override void BeginInvokeJS(long asyncHandle, string identifier, SerializedArgs serializedArgs, JSCallResultType resultType, long targetInstanceId)
7979
{
8080
if (_clientProxy is null)
8181
{
@@ -87,7 +87,7 @@ protected override void BeginInvokeJS(long asyncHandle, string identifier, strin
8787

8888
Log.BeginInvokeJS(_logger, asyncHandle, identifier);
8989

90-
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, byteArrays, (int)resultType, targetInstanceId);
90+
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, serializedArgs.ArgsJson, serializedArgs.ByteArrays, (int)resultType, targetInstanceId);
9191
}
9292

9393
public static class Log

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/ByteArrayJsonConverter.cs

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

44
using System;
5+
using System.Collections.Generic;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78

89
namespace Microsoft.JSInterop.Infrastructure
910
{
1011
internal sealed class ByteArrayJsonConverter : JsonConverter<byte[]>
1112
{
13+
private byte[][]? _byteArraysToDeserialize;
1214
internal static readonly JsonEncodedText ByteArrayRefKey = JsonEncodedText.Encode("__byte[]");
13-
private readonly JSRuntime _jsRuntime;
1415

15-
public ByteArrayJsonConverter(JSRuntime jsRuntime)
16+
/// <summary>
17+
/// Contains the byte array(s) being serialized.
18+
/// </summary>
19+
internal readonly List<byte[]> ByteArraysToSerialize = new();
20+
21+
/// <summary>
22+
/// Sets the byte array(s) being deserialized.
23+
/// </summary>
24+
internal byte[][]? ByteArraysToDeserialize
1625
{
17-
_jsRuntime = jsRuntime;
26+
set
27+
{
28+
if (_byteArraysToDeserialize is not null && value is not null)
29+
{
30+
throw new JsonException("Unable to deserialize arguments, previous deserialization is incomplete.");
31+
}
32+
33+
_byteArraysToDeserialize = value;
34+
}
1835
}
1936

2037
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(byte[]);
2138

2239
public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2340
{
24-
if (_jsRuntime.ByteArraysToDeserialize is null)
41+
if (_byteArraysToDeserialize is null)
2542
{
2643
throw new JsonException("ByteArraysToDeserialize not set.");
2744
}
@@ -53,20 +70,20 @@ public ByteArrayJsonConverter(JSRuntime jsRuntime)
5370
throw new JsonException($"Required property {ByteArrayRefKey} not found.");
5471
}
5572

56-
if (byteArrayIndex >= _jsRuntime.ByteArraysToDeserialize.Length || byteArrayIndex < 0)
73+
if (byteArrayIndex >= _byteArraysToDeserialize.Length || byteArrayIndex < 0)
5774
{
5875
throw new JsonException($"Byte array {byteArrayIndex} not found.");
5976
}
6077

61-
var value = _jsRuntime.ByteArraysToDeserialize[byteArrayIndex.Value];
78+
var value = _byteArraysToDeserialize[byteArrayIndex.Value];
6279
return value;
6380
}
6481

6582
public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
6683
{
67-
var id = _jsRuntime.ByteArraysToSerialize.Count;
84+
var id = ByteArraysToSerialize.Count;
6885

69-
_jsRuntime.ByteArraysToSerialize.Add(value);
86+
ByteArraysToSerialize.Add(value);
7087

7188
writer.WriteStartObject();
7289
writer.WriteNumber(ByteArrayRefKey, id);

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static SerializedArgs Invoke(JSRuntime jsRuntime, in DotNetInvocationInfo
5757
return new SerializedArgs(null, null);
5858
}
5959

60-
return jsRuntime.SerializeArgs(syncResult);
60+
return SerializeArgs(jsRuntime, syncResult);
6161
}
6262

6363
/// <summary>
@@ -115,7 +115,7 @@ public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo i
115115
else
116116
{
117117

118-
var serializedArgs = jsRuntime.SerializeArgs(syncResult);
118+
var serializedArgs = SerializeArgs(jsRuntime, syncResult);
119119
var dispatchResult = new DotNetInvocationResult(serializedArgs);
120120
jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult);
121121
}
@@ -131,7 +131,7 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
131131
}
132132

133133
var result = TaskGenericsUtil.GetTaskResult(task);
134-
var serializedArgs = jsRuntime.SerializeArgs(result);
134+
var serializedArgs = SerializeArgs(jsRuntime, result);
135135
jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(serializedArgs));
136136
}
137137

@@ -200,7 +200,7 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
200200

201201
var suppliedArgs = new object?[parameterTypes.Length];
202202

203-
jsRuntime.ByteArraysToDeserialize = byteArrays;
203+
jsRuntime.ByteArrayJsonConverter.ByteArraysToDeserialize = byteArrays;
204204

205205
var index = 0;
206206
while (index < parameterTypes.Length && reader.Read() && reader.TokenType != JsonTokenType.EndArray)
@@ -227,7 +227,7 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
227227
throw new JsonException($"Unexpected JSON token {reader.TokenType}. Ensure that the call to `{methodIdentifier}' is supplied with exactly '{parameterTypes.Length}' parameters.");
228228
}
229229

230-
jsRuntime.ByteArraysToDeserialize = null;
230+
jsRuntime.ByteArrayJsonConverter.ByteArraysToDeserialize = null;
231231

232232
return suppliedArgs;
233233

@@ -300,6 +300,30 @@ public static void EndInvokeJS(JSRuntime jsRuntime, string arguments, byte[][]?
300300
}
301301
}
302302

303+
/// <summary>
304+
/// Serialize the args to a json string, with the byte arrays
305+
/// extracted out for seperate transmission.
306+
/// </summary>
307+
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
308+
/// <param name="args">Arguments to be converted to json.</param>
309+
/// <returns>
310+
/// A tuple of the json string and an array containing the extracted
311+
/// byte arrays from the args.
312+
/// </returns>
313+
internal static SerializedArgs SerializeArgs(JSRuntime jsRuntime, object? args)
314+
{
315+
if (jsRuntime.ByteArrayJsonConverter.ByteArraysToSerialize.Count != 0)
316+
{
317+
throw new JsonException("Unable to serialize arguments, previous serialization is incomplete.");
318+
}
319+
320+
var serializedArgs = JsonSerializer.Serialize(args, jsRuntime.JsonSerializerOptions);
321+
var byteArrays = jsRuntime.ByteArrayJsonConverter.ByteArraysToSerialize.ToArray();
322+
jsRuntime.ByteArrayJsonConverter.ByteArraysToSerialize.Clear();
323+
324+
return new(serializedArgs, byteArrays);
325+
}
326+
303327
private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier)
304328
{
305329
if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName))

src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Text.Json;
6+
using Microsoft.JSInterop.Infrastructure;
67
using static Microsoft.AspNetCore.Internal.LinkerFlags;
78

89
namespace Microsoft.JSInterop
@@ -15,7 +16,7 @@ public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime
1516
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
1617
internal TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, long targetInstanceId, params object?[]? args)
1718
{
18-
var serializedArgs = SerializeArgs(args);
19+
var serializedArgs = DotNetDispatcher.SerializeArgs(this, args);
1920

2021
var resultJson = InvokeJS(
2122
identifier,

src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs

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

44
using System;
55
using System.Collections.Concurrent;
6-
using System.Collections.Generic;
76
using System.Diagnostics;
87
using System.Diagnostics.CodeAnalysis;
9-
using System.IO;
108
using System.Text.Json;
119
using System.Threading;
1210
using System.Threading.Tasks;
@@ -22,16 +20,16 @@ public abstract partial class JSRuntime : IJSRuntime
2220
{
2321
private long _nextObjectReferenceId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
2422
private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed"
25-
private readonly ConcurrentDictionary<long, object> _pendingTasks = new ConcurrentDictionary<long, object>();
26-
private readonly ConcurrentDictionary<long, IDotNetObjectReference> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectReference>();
27-
private readonly ConcurrentDictionary<long, CancellationTokenRegistration> _cancellationRegistrations =
28-
new ConcurrentDictionary<long, CancellationTokenRegistration>();
23+
private readonly ConcurrentDictionary<long, object> _pendingTasks = new();
24+
private readonly ConcurrentDictionary<long, IDotNetObjectReference> _trackedRefsById = new();
25+
private readonly ConcurrentDictionary<long, CancellationTokenRegistration> _cancellationRegistrations = new();
2926

3027
/// <summary>
3128
/// Initializes a new instance of <see cref="JSRuntime"/>.
3229
/// </summary>
3330
protected JSRuntime()
3431
{
32+
ByteArrayJsonConverter = new ByteArrayJsonConverter();
3533
JsonSerializerOptions = new JsonSerializerOptions
3634
{
3735
MaxDepth = 32,
@@ -41,7 +39,7 @@ protected JSRuntime()
4139
{
4240
new DotNetObjectReferenceJsonConverterFactory(this),
4341
new JSObjectReferenceJsonConverter(this),
44-
new ByteArrayJsonConverter(this),
42+
ByteArrayJsonConverter,
4543
}
4644
};
4745
}
@@ -52,19 +50,14 @@ protected JSRuntime()
5250
protected internal JsonSerializerOptions JsonSerializerOptions { get; }
5351

5452
/// <summary>
55-
/// Gets or sets the default timeout for asynchronous JavaScript calls.
53+
/// Gets the <see cref="Infrastructure.ByteArrayJsonConverter"/> used to serialize and deserialize byte arrays from the interop payloads.
5654
/// </summary>
57-
protected TimeSpan? DefaultAsyncTimeout { get; set; }
55+
internal ByteArrayJsonConverter ByteArrayJsonConverter { get; }
5856

5957
/// <summary>
60-
/// Contains the combined byte array(s) being serialized.
61-
/// </summary>
62-
internal readonly List<byte[]> ByteArraysToSerialize = new();
63-
64-
/// <summary>
65-
/// Contains the combined byte array(s) being deserialized.
58+
/// Gets or sets the default timeout for asynchronous JavaScript calls.
6659
/// </summary>
67-
internal byte[][]? ByteArraysToDeserialize { get; set; }
60+
protected TimeSpan? DefaultAsyncTimeout { get; set; }
6861

6962
/// <summary>
7063
/// Invokes the specified JavaScript function asynchronously.
@@ -135,12 +128,12 @@ protected JSRuntime()
135128
}
136129

137130
var serializedArgs = args is not null && args.Length != 0 ?
138-
SerializeArgs(args) :
131+
DotNetDispatcher.SerializeArgs(this, args) :
139132
new SerializedArgs(null, null);
140133
var resultType = JSCallResultTypeHelper.FromGeneric<TValue>();
141134

142135

143-
BeginInvokeJS(taskId, identifier, serializedArgs.ArgsJson, serializedArgs.ByteArrays, resultType, targetInstanceId);
136+
BeginInvokeJS(taskId, identifier, serializedArgs, resultType, targetInstanceId);
144137

145138
return new ValueTask<TValue>(tcs.Task);
146139
}
@@ -160,44 +153,24 @@ private void CleanupTasksAndRegistrations(long taskId)
160153
}
161154
}
162155

163-
/// <summary>
164-
/// Serialize the args to a json string, with the byte arrays
165-
/// extracted out for seperate transmission.
166-
/// </summary>
167-
/// <param name="args">Arguments to be converted to json.</param>
168-
/// <returns>
169-
/// A tuple of the json string and an array containing the extracted
170-
/// byte arrays from the args.
171-
/// </returns>
172-
protected internal SerializedArgs SerializeArgs(object? args)
173-
{
174-
ByteArraysToSerialize.Clear();
175-
var serializedArgs = JsonSerializer.Serialize(args, JsonSerializerOptions);
176-
var byteArrays = ByteArraysToSerialize.ToArray();
177-
178-
return new(serializedArgs, byteArrays);
179-
}
180-
181156
/// <summary>
182157
/// Begins an asynchronous function invocation.
183158
/// </summary>
184159
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
185160
/// <param name="identifier">The identifier for the function to invoke.</param>
186-
/// <param name="argsJson">A JSON representation of the arguments.</param>
187-
/// <param name="byteArrays">Byte array data extracted from the arguments for direct transfer.</param>
188-
protected virtual void BeginInvokeJS(long taskId, string identifier, string? argsJson, byte[][]? byteArrays)
189-
=> BeginInvokeJS(taskId, identifier, argsJson, byteArrays, JSCallResultType.Default, 0);
161+
/// <param name="serializedArgs">The serialized args containing the JSON representation along with the extracted byte arrays.</param>
162+
protected virtual void BeginInvokeJS(long taskId, string identifier, SerializedArgs serializedArgs)
163+
=> BeginInvokeJS(taskId, identifier, serializedArgs, JSCallResultType.Default, 0);
190164

191165
/// <summary>
192166
/// Begins an asynchronous function invocation.
193167
/// </summary>
194168
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
195169
/// <param name="identifier">The identifier for the function to invoke.</param>
196-
/// <param name="argsJson">A JSON representation of the arguments.</param>
197-
/// <param name="byteArrays">Byte array data extracted from the arguments for direct transfer.</param>
170+
/// <param name="serializedArgs">The serialized args containing the JSON representation along with the extracted byte arrays.</param>
198171
/// <param name="resultType">The type of result expected from the invocation.</param>
199172
/// <param name="targetInstanceId">The instance ID of the target JS object.</param>
200-
protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson, byte[][]? byteArrays, JSCallResultType resultType, long targetInstanceId);
173+
protected abstract void BeginInvokeJS(long taskId, string identifier, SerializedArgs serializedArgs, JSCallResultType resultType, long targetInstanceId);
201174

202175
/// <summary>
203176
/// Completes an async JS interop call from JavaScript to .NET
@@ -226,10 +199,10 @@ internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe
226199
{
227200
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
228201

229-
ByteArraysToDeserialize = byteArrays;
202+
ByteArrayJsonConverter.ByteArraysToDeserialize = byteArrays;
230203
var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions);
231204
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
232-
ByteArraysToDeserialize = null;
205+
ByteArrayJsonConverter.ByteArraysToDeserialize = null;
233206
}
234207
else
235208
{

src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
Microsoft.JSInterop.Implementation.JSObjectReferenceJsonWorker
33
Microsoft.JSInterop.Infrastructure.DotNetInvocationResult.ByteArrays.get -> byte[]![]?
44
Microsoft.JSInterop.Infrastructure.DotNetInvocationResult.ResultJson.get -> string?
5-
Microsoft.JSInterop.JSRuntime.SerializeArgs(object? args) -> Microsoft.JSInterop.SerializedArgs
65
Microsoft.JSInterop.SerializedArgs
76
Microsoft.JSInterop.SerializedArgs.SerializedArgs(string? argsJson, byte[]![]? byteArrays) -> void
87
abstract Microsoft.JSInterop.JSInProcessRuntime.InvokeJS(string! identifier, string? argsJson, byte[]![]? byteArrays, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> string?
9-
abstract Microsoft.JSInterop.JSRuntime.BeginInvokeJS(long taskId, string! identifier, string? argsJson, byte[]![]? byteArrays, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> void
8+
abstract Microsoft.JSInterop.JSRuntime.BeginInvokeJS(long taskId, string! identifier, Microsoft.JSInterop.SerializedArgs serializedArgs, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> void
109
readonly Microsoft.JSInterop.SerializedArgs.ArgsJson -> string?
1110
readonly Microsoft.JSInterop.SerializedArgs.ByteArrays -> byte[]![]?
1211
static Microsoft.JSInterop.Implementation.JSObjectReferenceJsonWorker.ReadJSObjectReferenceIdentifier(ref System.Text.Json.Utf8JsonReader reader) -> long
@@ -40,4 +39,4 @@ static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JS
4039
*REMOVED*static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object![]! args) -> System.Threading.Tasks.ValueTask
4140
static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask
4241
virtual Microsoft.JSInterop.JSInProcessRuntime.InvokeJS(string! identifier, string? argsJson, byte[]![]? byteArrays) -> string?
43-
virtual Microsoft.JSInterop.JSRuntime.BeginInvokeJS(long taskId, string! identifier, string? argsJson, byte[]![]? byteArrays) -> void
42+
virtual Microsoft.JSInterop.JSRuntime.BeginInvokeJS(long taskId, string! identifier, Microsoft.JSInterop.SerializedArgs serializedArgs) -> void

0 commit comments

Comments
 (0)