Skip to content

Commit 3056f46

Browse files
committed
Serialized Args
1 parent fe513a4 commit 3056f46

File tree

11 files changed

+105
-44
lines changed

11 files changed

+105
-44
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in
7070
_clientProxy.SendAsync("JS.EndInvokeDotNet",
7171
invocationInfo.CallId,
7272
/* success */ true,
73-
invocationResult.ResultJson);
73+
invocationResult.ResultJson,
74+
invocationResult.ByteArrays);
7475
}
7576
}
7677

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ function attachInteropInvoker(): void {
487487
const dotNetDispatcherEndInvokeJSMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'EndInvokeJS');
488488

489489
DotNet.attachDispatcher({
490-
beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string): void => {
490+
beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string, byteArrays: Uint8Array[] | null): void => {
491491
assertHeapIsNotLocked();
492492
if (!dotNetObjectId && !assemblyName) {
493493
throw new Error('Either assemblyName or dotNetObjectId must have a non null value.');
@@ -503,21 +503,24 @@ function attachInteropInvoker(): void {
503503
assemblyNameOrDotNetObjectId,
504504
methodIdentifier,
505505
argsJson,
506+
byteArrays,
506507
);
507508
},
508-
endInvokeJSFromDotNet: (asyncHandle, succeeded, serializedArgs): void => {
509+
endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson, byteArrays: Uint8Array[] | null): void => {
509510
dotNetDispatcherEndInvokeJSMethodHandle(
510-
serializedArgs
511+
argsJson,
512+
byteArrays,
511513
);
512514
},
513-
invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
515+
invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson, byteArrays: Uint8Array[] | null) => {
514516
assertHeapIsNotLocked();
515517
return dotNetDispatcherInvokeMethodHandle(
516518
assemblyName ? assemblyName : null,
517519
methodIdentifier,
518520
dotNetObjectId ? dotNetObjectId.toString() : null,
519521
argsJson,
520-
) as string;
522+
byteArrays,
523+
) as DotNet.SerializedArgs | null;
521524
},
522525
});
523526
}

src/Components/Web.JS/src/Rendering/ElementReferenceCapture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function getCaptureIdAttributeName(referenceCaptureId: string) {
1515

1616
// Support receiving ElementRef instances as args in interop calls
1717
const elementRefKey = '__internalId'; // Keep in sync with ElementRef.cs
18-
DotNet.attachReviver(function reviveElementReference(key: any, value: any, byteArrays: Uint8Array[] | undefined) {
18+
DotNet.attachReviver(function reviveElementReference(key: any, value: any, byteArrays: Uint8Array[] | null) {
1919
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'string') {
2020
return getElementByCaptureId(value[elementRefKey]);
2121
}

src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
export module DotNet {
44
(window as any).DotNet = DotNet; // Ensure reachable from anywhere
55

6-
export type JsonReviver = ((key: any, value: any, byteArrays: Uint8Array[] | undefined) => any);
6+
export type JsonReviver = ((key: any, value: any, byteArrays: Uint8Array[] | null) => any);
77
const jsonRevivers: JsonReviver[] = [];
88

99
class JSObject {
@@ -156,12 +156,12 @@ export module DotNet {
156156
* @param json The JSON stirng to parse.
157157
* @param byteArrays The byte array data to revive.
158158
*/
159-
function parseJsonWithRevivers(json: string, byteArrays?: Uint8Array[] | undefined): any {
159+
function parseJsonWithRevivers(json: string, byteArrays?: Uint8Array[] | null): any {
160160
return json ? JSON.parse(json, (key, initialValue) => {
161161
// Invoke each reviver in order, passing the output from the previous reviver,
162162
// so that each one gets a chance to transform the value
163163
return jsonRevivers.reduce(
164-
(latestValue, reviver) => reviver(key, latestValue, byteArrays),
164+
(latestValue, reviver) => reviver(key, latestValue, byteArrays === undefined ? null : byteArrays),
165165
initialValue
166166
);
167167
}) : null;
@@ -172,8 +172,8 @@ export module DotNet {
172172
if (dispatcher.invokeDotNetFromJS) {
173173
const byteArrays : Uint8Array[] = [];
174174
const argsJson = JSON.stringify(args, (key, value) => argReplacer(key, value, byteArrays));
175-
const resultJson = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson, byteArrays);
176-
return resultJson ? parseJsonWithRevivers(resultJson) : null;
175+
const result = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson, byteArrays);
176+
return result && result.ArgsJson ? parseJsonWithRevivers(result.ArgsJson, result.ByteArrays) : null;
177177
} else {
178178
throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.');
179179
}
@@ -236,6 +236,19 @@ export module DotNet {
236236
JSObjectReference = 1
237237
}
238238

239+
/**
240+
* Represents the type of result expected from a JS interop call.
241+
*/
242+
export class SerializedArgs {
243+
ArgsJson: string | null;
244+
ByteArrays: Uint8Array[] | null;
245+
246+
constructor(argsJson: string | null, byteArrays: Uint8Array[] | null) {
247+
this.ArgsJson = argsJson;
248+
this.ByteArrays = byteArrays;
249+
}
250+
}
251+
239252
/**
240253
* Represents the ability to dispatch calls from JavaScript to a .NET runtime.
241254
*/
@@ -248,9 +261,9 @@ export module DotNet {
248261
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods.
249262
* @param argsJson JSON representation of arguments to pass to the method.
250263
* @param byteArrays Byte array data extracted from the arguments for direct transfer.
251-
* @returns JSON representation of the result of the invocation.
264+
* @returns SerializedArgs containing the string JSON args along with the extracted byte arrays representation of the result of the invocation.
252265
*/
253-
invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string, byteArrays: Uint8Array[] | null): string | null;
266+
invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string, byteArrays: Uint8Array[] | null): SerializedArgs | null;
254267

255268
/**
256269
* Invoked by the runtime to begin an asynchronous call to a .NET method.
@@ -303,16 +316,17 @@ export module DotNet {
303316
* @param byteArrays Byte array data extracted from the arguments for direct transfer.
304317
* @param resultType The type of result expected from the JS interop call.
305318
* @param targetInstanceId The instance ID of the target JS object.
306-
* @returns JSON representation of the invocation result.
319+
* @returns SerializedArgs containing the JSON representation of the invocation result and the associated byte arrays.
307320
*/
308-
invokeJSFromDotNet: (identifier: string, argsJson: string, byteArrays: Uint8Array[], resultType: JSCallResultType, targetInstanceId: number) => {
321+
invokeJSFromDotNet: (identifier: string, argsJson: string, byteArrays: Uint8Array[], resultType: JSCallResultType, targetInstanceId: number): SerializedArgs => {
309322
const returnValue = findJSFunction(identifier, targetInstanceId).apply(null, parseJsonWithRevivers(argsJson, byteArrays));
310323
const result = createJSCallResult(returnValue, resultType);
311324

312325
const returnedByteArrays : Uint8Array[] = [];
313-
return result === null || result === undefined
326+
var serializedJson = (result === null || result === undefined)
314327
? null
315-
: JSON.stringify(result, (key, value) => argReplacer(key, value, returnedByteArrays)); // TODO; what is this used for? Seems like it doesn't actually invoke anything
328+
: JSON.stringify(result, (key, value) => argReplacer(key, value, returnedByteArrays)); // TODO; confirm this works for blazor wasm
329+
return new SerializedArgs(serializedJson, returnedByteArrays);
316330
},
317331

318332
/**
@@ -405,7 +419,7 @@ export module DotNet {
405419
}
406420

407421
const dotNetObjectRefKey = '__dotNetObject';
408-
attachReviver(function reviveDotNetObject(key: any, value: any, byteArrays: Uint8Array[] | undefined) {
422+
attachReviver(function reviveDotNetObject(key: any, value: any, byteArrays: Uint8Array[] | null) {
409423
if (value && typeof value === 'object' && value.hasOwnProperty(dotNetObjectRefKey)) {
410424
return new DotNetObject(value.__dotNetObject);
411425
}
@@ -414,7 +428,7 @@ export module DotNet {
414428
return value;
415429
});
416430

417-
attachReviver(function reviveJSObjectReference(key: any, value: any, byteArrays: Uint8Array[] | undefined) {
431+
attachReviver(function reviveJSObjectReference(key: any, value: any, byteArrays: Uint8Array[] | null) {
418432
if (value && typeof value === 'object' && value.hasOwnProperty(jsObjectIdKey)) {
419433
const id = value[jsObjectIdKey];
420434
const jsObject = cachedJSObjectsById[id];
@@ -431,7 +445,7 @@ export module DotNet {
431445
});
432446

433447
const byteArrayRefKey = '__byte[]';
434-
attachReviver(function reviveByteArray(key: any, value: any, byteArrays: Uint8Array[] | undefined) {
448+
attachReviver(function reviveByteArray(key: any, value: any, byteArrays: Uint8Array[] | null) {
435449
if (value && typeof value === 'object' && value.hasOwnProperty(byteArrayRefKey) && byteArrays) {
436450
const index = value[byteArrayRefKey];
437451

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Text;
1212
using System.Text.Json;
1313
using System.Threading.Tasks;
14+
using static Microsoft.JSInterop.JSRuntime;
1415

1516
[assembly: MetadataUpdateHandler(typeof(Microsoft.JSInterop.Infrastructure.DotNetDispatcher))]
1617

@@ -36,8 +37,8 @@ public static class DotNetDispatcher
3637
/// <param name="invocationInfo">The <see cref="DotNetInvocationInfo"/>.</param>
3738
/// <param name="argsJson">A JSON representation of the parameters.</param>
3839
/// <param name="byteArrays">Byte array data extracted from the arguments for direct transfer.</param>
39-
/// <returns>A tuple containing the JSON representation of the return value, or null, along with the extracted byte arrays, or null.</returns>
40-
public static (string?, byte[][]?) Invoke(JSRuntime jsRuntime, in DotNetInvocationInfo invocationInfo, string argsJson, byte[][]? byteArrays)
40+
/// <returns>A record containing the JSON representation of the return value, or null, along with the extracted byte arrays, or null.</returns>
41+
public static SerializedArgs Invoke(JSRuntime jsRuntime, in DotNetInvocationInfo invocationInfo, string argsJson, byte[][]? byteArrays)
4142
{
4243
// This method doesn't need [JSInvokable] because the platform is responsible for having
4344
// some way to dispatch calls here. The logic inside here is the thing that checks whether
@@ -53,7 +54,7 @@ public static (string?, byte[][]?) Invoke(JSRuntime jsRuntime, in DotNetInvocati
5354
var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson, byteArrays);
5455
if (syncResult == null)
5556
{
56-
return (null, null);
57+
return new SerializedArgs(null, null);
5758
}
5859

5960
return jsRuntime.SerializeArgs(syncResult);
@@ -66,7 +67,6 @@ public static (string?, byte[][]?) Invoke(JSRuntime jsRuntime, in DotNetInvocati
6667
/// <param name="invocationInfo">The <see cref="DotNetInvocationInfo"/>.</param>
6768
/// <param name="argsJson">A JSON representation of the parameters.</param>
6869
/// <param name="byteArrays">Byte array data extracted from the arguments for direct transfer.</param>
69-
/// <returns>A JSON representation of the return value, or null.</returns>
7070
public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo invocationInfo, string argsJson, byte[][]? byteArrays)
7171
{
7272
// This method doesn't need [JSInvokable] because the platform is responsible for having
@@ -114,13 +114,13 @@ public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo i
114114
}
115115
else
116116
{
117-
var syncResultJson = JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions);
118-
var dispatchResult = new DotNetInvocationResult(syncResultJson);
117+
118+
var serializedArgs = jsRuntime.SerializeArgs(syncResult);
119+
var dispatchResult = new DotNetInvocationResult(serializedArgs);
119120
jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult);
120121
}
121122
}
122123

123-
// TODO: This is a new func needs to be integrated with byte array interop
124124
private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in DotNetInvocationInfo invocationInfo)
125125
{
126126
if (task.Exception != null)
@@ -131,8 +131,8 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
131131
}
132132

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

138138
private static object? InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference? objectReference, string argsJson, byte[][]? byteArrays)
@@ -429,7 +429,7 @@ private static Assembly GetRequiredLoadedAssembly(AssemblyKey assemblyKey)
429429
// In most ordinary scenarios, we wouldn't have two instances of the same Assembly in the AppDomain
430430
// so this doesn't change the outcome.
431431
Assembly? assembly = null;
432-
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
432+
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
433433
{
434434
if (new AssemblyKey(a).Equals(assemblyKey))
435435
{

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public readonly struct DotNetInvocationResult
1515
internal DotNetInvocationResult(Exception exception, string? errorKind)
1616
{
1717
ResultJson = default;
18+
ByteArrays = default;
1819
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
1920
ErrorKind = errorKind;
2021
Success = false;
@@ -23,10 +24,11 @@ internal DotNetInvocationResult(Exception exception, string? errorKind)
2324
/// <summary>
2425
/// Constructor for a successful invocation.
2526
/// </summary>
26-
/// <param name="resultJson">The JSON representation of the result.</param>
27-
internal DotNetInvocationResult(string? resultJson)
27+
/// <param name="args">Serialized JSON representation of the result with the extracted byte arrays.</param>
28+
internal DotNetInvocationResult(SerializedArgs args)
2829
{
29-
ResultJson = resultJson;
30+
ResultJson = args.ArgsJson;
31+
ByteArrays = args.ByteArrays;
3032
Exception = default;
3133
ErrorKind = default;
3234
Success = true;
@@ -47,6 +49,11 @@ internal DotNetInvocationResult(string? resultJson)
4749
/// </summary>
4850
public string? ResultJson { get; }
4951

52+
/// <summary>
53+
/// Gets the byte array data extracted from the result for direct transfer.
54+
/// </summary>
55+
public byte[][]? ByteArrays { get; }
56+
5057
/// <summary>
5158
/// <see langword="true"/> if the invocation succeeded, otherwise <see langword="false"/>.
5259
/// </summary>

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime
1515
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
1616
internal TValue Invoke<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, long targetInstanceId, params object?[]? args)
1717
{
18+
var serializedArgs = SerializeArgs(args);
19+
1820
var resultJson = InvokeJS(
1921
identifier,
20-
JsonSerializer.Serialize(args, JsonSerializerOptions),
22+
serializedArgs.ArgsJson,
23+
serializedArgs.ByteArrays,
2124
JSCallResultTypeHelper.FromGeneric<TValue>(),
2225
targetInstanceId);
2326

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ protected JSRuntime()
134134
return new ValueTask<TValue>(tcs.Task);
135135
}
136136

137-
var (argsJson, byteArrays) = args is not null && args.Length != 0 ?
137+
var serializedArgs = args is not null && args.Length != 0 ?
138138
SerializeArgs(args) :
139-
(null, null);
139+
new SerializedArgs(null, null);
140140
var resultType = JSCallResultTypeHelper.FromGeneric<TValue>();
141141

142142

143-
BeginInvokeJS(taskId, identifier, argsJson, byteArrays, resultType, targetInstanceId);
143+
BeginInvokeJS(taskId, identifier, serializedArgs.ArgsJson, serializedArgs.ByteArrays, resultType, targetInstanceId);
144144

145145
return new ValueTask<TValue>(tcs.Task);
146146
}
@@ -164,18 +164,18 @@ private void CleanupTasksAndRegistrations(long taskId)
164164
/// Serialize the args to a json string, with the byte arrays
165165
/// extracted out for seperate transmission.
166166
/// </summary>
167-
/// <param name="args">Argument(s) to be converted to json.</param>
167+
/// <param name="args">Arguments to be converted to json.</param>
168168
/// <returns>
169169
/// A tuple of the json string and an array containing the extracted
170170
/// byte arrays from the args.
171171
/// </returns>
172-
protected internal (string, byte[][]?) SerializeArgs(object? args)
172+
protected internal SerializedArgs SerializeArgs(object? args)
173173
{
174174
ByteArraysToSerialize.Clear();
175175
var serializedArgs = JsonSerializer.Serialize(args, JsonSerializerOptions);
176176
var byteArrays = ByteArraysToSerialize.ToArray();
177177

178-
return (serializedArgs, byteArrays);
178+
return new(serializedArgs, byteArrays);
179179
}
180180

181181
/// <summary>
@@ -228,8 +228,8 @@ internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe
228228

229229
ByteArraysToDeserialize = byteArrays;
230230
var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions);
231-
ByteArraysToDeserialize = null;
232231
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
232+
ByteArraysToDeserialize = null;
233233
}
234234
else
235235
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
namespace Microsoft.JSInterop
5+
{
6+
/// <summary>
7+
/// Combined json string and extracted byte arrays for an
8+
/// interop message.
9+
/// </summary>
10+
public readonly struct SerializedArgs
11+
{
12+
/// <summary>
13+
/// json string representing the arguments
14+
/// </summary>
15+
public readonly string? ArgsJson;
16+
17+
/// <summary>
18+
/// Byte arrays extracted from the arguments during serialization
19+
/// </summary>
20+
public readonly byte[][]? ByteArrays;
21+
22+
/// <summary>
23+
/// Struct containing the json args and extracted byte arrays
24+
/// </summary>
25+
/// <param name="argsJson">json string representing the arguments</param>
26+
/// <param name="byteArrays">Byte arrays extracted from the arguments during serialization</param>
27+
public SerializedArgs(string? argsJson, byte[][]? byteArrays)
28+
{
29+
ArgsJson = argsJson;
30+
ByteArrays = byteArrays;
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)