From b4bcb1fd15d19eb008079bd4344edc05d920e51b Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 17 Jul 2019 17:12:37 +0200 Subject: [PATCH] [JSInterop] Updates the JSInterop abstractions to better support logging and diagnostics. (dotnet/extensions#2043) * Add an overload to handle .NET completion calls without going through JS interop. * Add endInvokeHook on the client-side. * Replace OnDotNetInvocationException with EndInvokeDotNet.\n\nCommit migrated from https://github.com/dotnet/extensions/commit/4312b9a050700833e01a01cf9381ff9033660bc1 --- .../src/src/Microsoft.JSInterop.ts | 25 ++++--- .../ref/Microsoft.JSInterop.netstandard2.0.cs | 13 +--- .../src/DotNetDispatcher.cs | 41 ++++++++--- .../src/JSAsyncCallResult.cs | 6 +- .../Microsoft.JSInterop/src/JSRuntimeBase.cs | 45 ++++-------- .../test/DotNetDispatcherTest.cs | 71 +++++++++++-------- .../test/DotNetObjectRefTest.cs | 5 ++ .../test/JSInProcessRuntimeBaseTest.cs | 3 + .../test/JSRuntimeBaseTest.cs | 36 +++++++--- ...Mono.WebAssembly.Interop.netstandard2.0.cs | 1 + .../src/MonoWebAssemblyJSRuntime.cs | 33 +++++++++ 11 files changed, 173 insertions(+), 106 deletions(-) diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index 27d22b0536..60f6f800a6 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -75,7 +75,7 @@ module DotNet { try { const argsJson = JSON.stringify(args, argReplacer); getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); - } catch(ex) { + } catch (ex) { // Synchronous failure completePendingCall(asyncCallId, false, ex); } @@ -116,7 +116,7 @@ module DotNet { export interface DotNetCallDispatcher { /** * Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method. - * + * * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods. @@ -135,6 +135,15 @@ module DotNet { * @param argsJson JSON representation of arguments to pass to the method. */ beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void; + + /** + * Invoked by the runtime to complete an asynchronous JavaScript function call started from .NET + * + * @param callId A value identifying the asynchronous operation. + * @param succeded Whether the operation succeeded or not. + * @param resultOrError The serialized result or the serialized error from the async operation. + */ + endInvokeJSFromDotNet(callId: number, succeeded: boolean, resultOrError: any): void; } /** @@ -183,8 +192,8 @@ module DotNet { // On completion, dispatch result back to .NET // Not using "await" because it codegens a lot of boilerplate promise.then( - result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)), - error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)])) + result => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, true, JSON.stringify([asyncHandle, true, result], argReplacer)), + error => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, false, JSON.stringify([asyncHandle, false, formatError(error)])) ); } }, @@ -219,7 +228,7 @@ module DotNet { return error ? error.toString() : 'null'; } } - + function findJSFunction(identifier: string): Function { if (cachedJSFunctions.hasOwnProperty(identifier)) { return cachedJSFunctions[identifier]; @@ -247,7 +256,7 @@ module DotNet { } } - class DotNetObject { + class DotNetObject { constructor(private _id: number) { } @@ -268,14 +277,14 @@ module DotNet { } public serializeAsArg() { - return {__dotNetObject: this._id}; + return { __dotNetObject: this._id }; } } const dotNetObjectRefKey = '__dotNetObject'; attachReviver(function reviveDotNetObject(key: any, value: any) { if (value && typeof value === 'object' && value.hasOwnProperty(dotNetObjectRefKey)) { - return new DotNetObject(value.__dotNetObject); + return new DotNetObject(value.__dotNetObject); } // Unrecognized - let another reviver handle it diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs index ad95b7342b..125a555c9d 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -6,8 +6,7 @@ namespace Microsoft.JSInterop public static partial class DotNetDispatcher { public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } - [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.EndInvoke")] - public static void EndInvoke(long asyncHandle, bool succeeded, Microsoft.JSInterop.Internal.JSAsyncCallResult result) { } + public static void EndInvoke(string arguments) { } public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] public static void ReleaseDotNetObject(long dotNetObjectId) { } @@ -62,16 +61,8 @@ namespace Microsoft.JSInterop protected JSRuntimeBase() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); + protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); public System.Threading.Tasks.Task InvokeAsync(string identifier, System.Collections.Generic.IEnumerable args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args) { throw null; } - protected virtual object OnDotNetInvocationException(System.Exception exception, string assemblyName, string methodIdentifier) { throw null; } - } -} -namespace Microsoft.JSInterop.Internal -{ - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - public sealed partial class JSAsyncCallResult - { - internal JSAsyncCallResult() { } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs index 952a6061ab..60b6ad9ab0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs @@ -8,9 +8,7 @@ using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; -using Microsoft.JSInterop.Internal; namespace Microsoft.JSInterop { @@ -20,6 +18,7 @@ namespace Microsoft.JSInterop public static class DotNetDispatcher { internal const string DotNetObjectRefKey = nameof(DotNetObjectRef.__dotNetObject); + private static readonly Type[] EndInvokeParameterTypes = new Type[] { typeof(long), typeof(bool), typeof(JSAsyncCallResult) }; private static readonly ConcurrentDictionary> _cachedMethodsByAssembly = new ConcurrentDictionary>(); @@ -104,7 +103,7 @@ namespace Microsoft.JSInterop else if (syncException != null) { // Threw synchronously, let's respond. - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier); + jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); } else if (syncResult is Task task) { @@ -116,16 +115,16 @@ namespace Microsoft.JSInterop { var exception = t.Exception.GetBaseException(); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier); + jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); } var result = TaskGenericsUtil.GetTaskResult(task); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier); + jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); }, TaskScheduler.Current); } else { - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier); + jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); } } @@ -248,11 +247,31 @@ namespace Microsoft.JSInterop /// Receives notification that a call from .NET to JS has finished, marking the /// associated as completed. /// - /// The identifier for the function invocation. - /// A flag to indicate whether the invocation succeeded. - /// If is true, specifies the invocation result. If is false, gives the corresponding to the invocation failure. - [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] - public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) + /// + /// All exceptions from are caught + /// are delivered via JS interop to the JavaScript side when it requests confirmation, as + /// the mechanism to call relies on + /// using JS->.NET interop. This overload is meant for directly triggering completion callbacks + /// for .NET -> JS operations without going through JS interop, so the callsite for this + /// method is responsible for handling any possible exception generated from the arguments + /// passed in as parameters. + /// + /// The serialized arguments for the callback completion. + /// + /// This method can throw any exception either from the argument received or as a result + /// of executing any callback synchronously upon completion. + /// + public static void EndInvoke(string arguments) + { + var parsedArgs = ParseArguments( + nameof(EndInvoke), + arguments, + EndInvokeParameterTypes); + + EndInvoke((long)parsedArgs[0], (bool)parsedArgs[1], (JSAsyncCallResult)parsedArgs[2]); + } + + private static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result); /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs b/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs index 1ea8c47995..209438529b 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs @@ -1,10 +1,9 @@ // 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.ComponentModel; using System.Text.Json; -namespace Microsoft.JSInterop.Internal +namespace Microsoft.JSInterop { // This type takes care of a special case in handling the result of an async call from // .NET to JS. The information about what type the result should be exists only on the @@ -23,8 +22,7 @@ namespace Microsoft.JSInterop.Internal /// /// Intended for framework use only. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class JSAsyncCallResult + internal sealed class JSAsyncCallResult { internal JSAsyncCallResult(JsonDocument document, JsonElement jsonElement) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs index 562a6a27ba..13a9ccae1c 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs @@ -9,7 +9,6 @@ using System.Runtime.ExceptionServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.JSInterop.Internal; namespace Microsoft.JSInterop { @@ -124,37 +123,21 @@ namespace Microsoft.JSInterop protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); /// - /// Allows derived classes to configure the information about an exception in a JS interop call that gets sent to JavaScript. + /// Completes an async JS interop call from JavaScript to .NET /// - /// - /// This callback can be used in remote JS interop scenarios to sanitize exceptions that happen on the server to avoid disclosing - /// sensitive information to remote browser clients. - /// - /// The exception that occurred. - /// The assembly for the invoked .NET method. - /// The identifier for the invoked .NET method. - /// An object containing information about the exception. - protected virtual object OnDotNetInvocationException(Exception exception, string assemblyName, string methodIdentifier) => exception.ToString(); - - internal void EndInvokeDotNet(string callId, bool success, object resultOrException, string assemblyName, string methodIdentifier) - { - // For failures, the common case is to call EndInvokeDotNet with the Exception object. - // For these we'll serialize as something that's useful to receive on the JS side. - // If the value is not an Exception, we'll just rely on it being directly JSON-serializable. - if (!success && resultOrException is Exception ex) - { - resultOrException = OnDotNetInvocationException(ex, assemblyName, methodIdentifier); - } - else if (!success && resultOrException is ExceptionDispatchInfo edi) - { - resultOrException = OnDotNetInvocationException(edi.SourceException, assemblyName, methodIdentifier); - } - - // We pass 0 as the async handle because we don't want the JS-side code to - // send back any notification (we're just providing a result for an existing async call) - var args = JsonSerializer.Serialize(new[] { callId, success, resultOrException }, JsonSerializerOptionsProvider.Options); - BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); - } + /// The id of the JavaScript callback to execute on completion. + /// Whether the operation succeeded or not. + /// The result of the operation or an object containing error details. + /// The name of the method assembly if the invocation was for a static method. + /// The identifier for the method within the assembly. + /// The tracking id of the dotnet object if the invocation was for an instance method. + protected internal abstract void EndInvokeDotNet( + string callId, + bool success, + object resultOrError, + string assemblyName, + string methodIdentifier, + long dotNetObjectId); internal void EndInvokeJS(long taskId, bool succeeded, JSAsyncCallResult asyncCallResult) { diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs index 9e8a7b672e..a4a0de5a3f 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Runtime.ExceptionServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -295,23 +297,18 @@ namespace Microsoft.JSInterop.Tests var resultTask = jsRuntime.NextInvocationTask; DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); await resultTask; - var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement; - var resultValue = result[2]; - // Assert: Correct info to complete the async call - Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET - Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier); - Assert.Equal(3, result.GetArrayLength()); - Assert.Equal(callId, result[0].GetString()); - Assert.True(result[1].GetBoolean()); // Success flag + // Assert: Correct completion information + Assert.Equal(callId, jsRuntime.LastCompletionCallId); + Assert.True(jsRuntime.LastCompletionStatus); + var result = Assert.IsType(jsRuntime.LastCompletionResult); + var resultDto1 = Assert.IsType(result[0]); - // Assert: First result value marshalled via JSON - var resultDto1 = JsonSerializer.Deserialize(resultValue[0].GetRawText(), JsonSerializerOptionsProvider.Options); Assert.Equal("STRING VIA JSON", resultDto1.StringVal); Assert.Equal(2000, resultDto1.IntVal); // Assert: Second result value marshalled by ref - var resultDto2Ref = JsonSerializer.Deserialize>(resultValue[1].GetRawText(), JsonSerializerOptionsProvider.Options); + var resultDto2Ref = Assert.IsType>(result[1]); var resultDto2 = resultDto2Ref.Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(2468, resultDto2.IntVal); @@ -330,13 +327,12 @@ namespace Microsoft.JSInterop.Tests await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert - var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement; - Assert.Equal(callId, result[0].GetString()); - Assert.False(result[1].GetBoolean()); // Fails + Assert.Equal(callId, jsRuntime.LastCompletionCallId); + Assert.False(jsRuntime.LastCompletionStatus); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = result[2].GetString(); + var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); }); @@ -353,13 +349,12 @@ namespace Microsoft.JSInterop.Tests await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert - var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement; - Assert.Equal(callId, result[0].GetString()); - Assert.False(result[1].GetBoolean()); // Fails + Assert.Equal(callId, jsRuntime.LastCompletionCallId); + Assert.False(jsRuntime.LastCompletionStatus); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = result[2].GetString(); + var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); }); @@ -374,11 +369,10 @@ namespace Microsoft.JSInterop.Tests await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert - using var jsonDocument = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson); - var result = jsonDocument.RootElement; - Assert.Equal(callId, result[0].GetString()); - Assert.False(result[1].GetBoolean()); // Fails - Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result[2].GetString()); + Assert.Equal(callId, jsRuntime.LastCompletionCallId); + Assert.False(jsRuntime.LastCompletionStatus); // Fails + var result = Assert.IsType(jsRuntime.LastCompletionResult); + Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result.SourceException.ToString()); }); [Fact] @@ -390,12 +384,10 @@ namespace Microsoft.JSInterop.Tests DotNetDispatcher.BeginInvoke(callId, null, "InvokableInstanceVoid", 1, null); // Assert - using var jsonDocument = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson); - var result = jsonDocument.RootElement; - Assert.Equal(callId, result[0].GetString()); - Assert.False(result[1].GetBoolean()); // Fails - - Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectRef instance was already disposed.", result[2].GetString()); + Assert.Equal(callId, jsRuntime.LastCompletionCallId); + Assert.False(jsRuntime.LastCompletionStatus); // Fails + var result = Assert.IsType(jsRuntime.LastCompletionResult); + Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectRef instance was already disposed.", result.SourceException.ToString()); }); Task WithJSRuntime(Action testCode) @@ -556,6 +548,10 @@ namespace Microsoft.JSInterop.Tests public string LastInvocationIdentifier { get; private set; } public string LastInvocationArgsJson { get; private set; } + public string LastCompletionCallId { get; private set; } + public bool LastCompletionStatus { get; private set; } + public object LastCompletionResult { get; private set; } + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { LastInvocationAsyncHandle = asyncHandle; @@ -574,6 +570,21 @@ namespace Microsoft.JSInterop.Tests _nextInvocationTcs = new TaskCompletionSource(); return null; } + + protected internal override void EndInvokeDotNet( + string callId, + bool success, + object resultOrError, + string assemblyName, + string methodIdentifier, + long dotNetObjectId) + { + LastCompletionCallId = callId; + LastCompletionStatus = success; + LastCompletionResult = resultOrError; + _nextInvocationTcs.SetResult(null); + _nextInvocationTcs = new TaskCompletionSource(); + } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs index 2b46831a16..77af280d94 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs @@ -37,6 +37,11 @@ namespace Microsoft.JSInterop.Tests { throw new NotImplementedException(); } + + protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + { + throw new NotImplementedException(); + } } async Task WithJSRuntime(Action testCode) diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs index dda3f74184..a8d551e94e 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs @@ -115,6 +115,9 @@ namespace Microsoft.JSInterop.Tests protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) => throw new NotImplementedException("This test only covers sync calls"); + + protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) => + throw new NotImplementedException("This test only covers sync calls"); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs index 3ad78c303f..fb9227c996 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.JSInterop.Internal; using Xunit; namespace Microsoft.JSInterop.Tests @@ -79,7 +79,7 @@ namespace Microsoft.JSInterop.Tests var task = runtime.InvokeAsync("test identifier 1", new object[] { "arg1", 123, true }, cts.Token); cts.Cancel(); - + // Assert await Assert.ThrowsAsync(async () => await task); } @@ -248,13 +248,14 @@ namespace Microsoft.JSInterop.Tests var exception = new Exception("Some really sensitive data in here"); // Act - runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method"); + runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method", 0); // Assert - var call = runtime.BeginInvokeCalls.Single(); - Assert.Equal(0, call.AsyncHandle); - Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", call.Identifier); - Assert.Equal($"[\"0\",false,{{\"message\":\"{expectedMessage.Replace("'", "\\u0027")}\"}}]", call.ArgsJson); + var call = runtime.EndInvokeDotNetCalls.Single(); + Assert.Equal("0", call.CallId); + Assert.False(call.Success); + var jsError = Assert.IsType(call.ResultOrError); + Assert.Equal(expectedMessage, jsError.Message); } private class JSError @@ -265,6 +266,7 @@ namespace Microsoft.JSInterop.Tests class TestJSRuntime : JSRuntimeBase { public List BeginInvokeCalls = new List(); + public List EndInvokeDotNetCalls = new List(); public TimeSpan? DefaultTimeout { @@ -281,16 +283,28 @@ namespace Microsoft.JSInterop.Tests public string ArgsJson { get; set; } } + public class EndInvokeDotNetArgs + { + public string CallId { get; set; } + public bool Success { get; set; } + public object ResultOrError { get; set; } + } + public Func OnDotNetException { get; set; } - protected override object OnDotNetInvocationException(Exception exception, string assemblyName, string methodName) + protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { - if (OnDotNetException != null) + if (OnDotNetException != null && !success) { - return OnDotNetException(exception, assemblyName, methodName); + resultOrError = OnDotNetException(resultOrError as Exception, assemblyName, methodIdentifier); } - return base.OnDotNetInvocationException(exception, assemblyName, methodName); + EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs + { + CallId = callId, + Success = success, + ResultOrError = resultOrError + }); } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) diff --git a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs index 033e97237a..5299370576 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs @@ -7,6 +7,7 @@ namespace Mono.WebAssembly.Interop { public MonoWebAssemblyJSRuntime() { } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { } + protected override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { } protected override string InvokeJS(string identifier, string argsJson) { throw null; } public TRes InvokeUnmarshalled(string identifier) { throw null; } public TRes InvokeUnmarshalled(string identifier, T0 arg0) { throw null; } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs index 9a502b8bc8..e65df172f8 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -1,6 +1,9 @@ // 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; +using System.Runtime.ExceptionServices; +using System.Text.Json; using Microsoft.JSInterop; using WebAssembly.JSInterop; @@ -32,6 +35,10 @@ namespace Mono.WebAssembly.Interop private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) => DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + // Invoked via Mono's JS interop mechanism (invoke_method) + private static void EndInvokeJS(string argsJson) + => DotNetDispatcher.EndInvoke(argsJson); + // Invoked via Mono's JS interop mechanism (invoke_method) private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) { @@ -54,6 +61,32 @@ namespace Mono.WebAssembly.Interop DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); } + protected override void EndInvokeDotNet( + string callId, + bool success, + object resultOrError, + string assemblyName, + string methodIdentifier, + long dotNetObjectId) + { + // For failures, the common case is to call EndInvokeDotNet with the Exception object. + // For these we'll serialize as something that's useful to receive on the JS side. + // If the value is not an Exception, we'll just rely on it being directly JSON-serializable. + if (!success && resultOrError is Exception ex) + { + resultOrError = ex.ToString(); + } + else if (!success && resultOrError is ExceptionDispatchInfo edi) + { + resultOrError = edi.SourceException.ToString(); + } + + // We pass 0 as the async handle because we don't want the JS-side code to + // send back any notification (we're just providing a result for an existing async call) + var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); + } + #region Custom MonoWebAssemblyJSRuntime methods ///