[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:
parent
4c0a9d9a6b
commit
b4bcb1fd15
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue