[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 4312b9a050
This commit is contained in:
Javier Calvarro Nelson 2019-07-17 17:12:37 +02:00 committed by GitHub
parent 4c0a9d9a6b
commit b4bcb1fd15
11 changed files with 173 additions and 106 deletions

View File

@ -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

View File

@ -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<T> InvokeAsync<T>(string identifier, System.Collections.Generic.IEnumerable<object> args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Threading.Tasks.Task<T> InvokeAsync<T>(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() { }
}
}

View File

@ -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<object>.__dotNetObject);
private static readonly Type[] EndInvokeParameterTypes = new Type[] { typeof(long), typeof(bool), typeof(JSAsyncCallResult) };
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
= new ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
@ -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 <see cref="Task"/> as completed.
/// </summary>
/// <param name="asyncHandle">The identifier for the function invocation.</param>
/// <param name="succeeded">A flag to indicate whether the invocation succeeded.</param>
/// <param name="result">If <paramref name="succeeded"/> is <c>true</c>, specifies the invocation result. If <paramref name="succeeded"/> is <c>false</c>, gives the <see cref="Exception"/> corresponding to the invocation failure.</param>
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))]
public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result)
/// <remarks>
/// All exceptions from <see cref="EndInvoke(long, bool, JSAsyncCallResult)"/> are caught
/// are delivered via JS interop to the JavaScript side when it requests confirmation, as
/// the mechanism to call <see cref="EndInvoke(long, bool, JSAsyncCallResult)"/> 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.
/// </remarks>
/// <param name="arguments">The serialized arguments for the callback completion.</param>
/// <exception cref="Exception">
/// This method can throw any exception either from the argument received or as a result
/// of executing any callback synchronously upon completion.
/// </exception>
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);
/// <summary>

View File

@ -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
/// <summary>
/// Intended for framework use only.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class JSAsyncCallResult
internal sealed class JSAsyncCallResult
{
internal JSAsyncCallResult(JsonDocument document, JsonElement jsonElement)
{

View File

@ -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);
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="exception">The exception that occurred.</param>
/// <param name="assemblyName">The assembly for the invoked .NET method.</param>
/// <param name="methodIdentifier">The identifier for the invoked .NET method.</param>
/// <returns>An object containing information about the exception.</returns>
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);
}
/// <param name="callId">The id of the JavaScript callback to execute on completion.</param>
/// <param name="success">Whether the operation succeeded or not.</param>
/// <param name="resultOrError">The result of the operation or an object containing error details.</param>
/// <param name="assemblyName">The name of the method assembly if the invocation was for a static method.</param>
/// <param name="methodIdentifier">The identifier for the method within the assembly.</param>
/// <param name="dotNetObjectId">The tracking id of the dotnet object if the invocation was for an instance method.</param>
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)
{

View File

@ -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<object []>(jsRuntime.LastCompletionResult);
var resultDto1 = Assert.IsType<TestDTO>(result[0]);
// Assert: First result value marshalled via JSON
var resultDto1 = JsonSerializer.Deserialize<TestDTO>(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<DotNetObjectRef<TestDTO>>(resultValue[1].GetRawText(), JsonSerializerOptionsProvider.Options);
var resultDto2Ref = Assert.IsType<DotNetObjectRef<TestDTO>>(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<ExceptionDispatchInfo>(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<ExceptionDispatchInfo>(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<TestJSRuntime> 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<object>();
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<object>();
}
}
}
}

View File

@ -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<JSRuntimeBase> testCode)

View File

@ -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");
}
}
}

View File

@ -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<object>("test identifier 1", new object[] { "arg1", 123, true }, cts.Token);
cts.Cancel();
// Assert
await Assert.ThrowsAsync<TaskCanceledException>(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<JSError>(call.ResultOrError);
Assert.Equal(expectedMessage, jsError.Message);
}
private class JSError
@ -265,6 +266,7 @@ namespace Microsoft.JSInterop.Tests
class TestJSRuntime : JSRuntimeBase
{
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
public List<EndInvokeDotNetArgs> EndInvokeDotNetCalls = new List<EndInvokeDotNetArgs>();
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<Exception, string, string, object> 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)

View File

@ -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<TRes>(string identifier) { throw null; }
public TRes InvokeUnmarshalled<T0, TRes>(string identifier, T0 arg0) { throw null; }

View File

@ -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
/// <summary>