diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs index 953f8b0329..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs @@ -50,7 +50,7 @@ namespace Microsoft.JSInterop protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } 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); + protected internal abstract void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult invocationResult); public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args) { throw null; } public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; } } @@ -72,8 +72,31 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { } public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } - public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, in Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationInfo + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) { throw null; } + public string AssemblyName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string CallId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public long DotNetObjectId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string MethodIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationResult + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationResult(System.Exception exception, string errorKind) { throw null; } + public DotNetInvocationResult(object result) { throw null; } + public string ErrorKind { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public object Result { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } } 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 953f8b0329..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -50,7 +50,7 @@ namespace Microsoft.JSInterop protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } 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); + protected internal abstract void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult invocationResult); public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args) { throw null; } public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; } } @@ -72,8 +72,31 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { } public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } - public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, in Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationInfo + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) { throw null; } + public string AssemblyName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string CallId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public long DotNetObjectId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string MethodIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationResult + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationResult(System.Exception exception, string errorKind) { throw null; } + public DotNetInvocationResult(object result) { throw null; } + public string ErrorKind { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public object Result { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index 92afc6278d..6a3a4f8d5f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -28,12 +28,10 @@ namespace Microsoft.JSInterop.Infrastructure /// Receives a call from JS to .NET, locating and invoking the specified method. /// /// The . - /// The assembly containing the method to be invoked. - /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. - /// For instance method calls, identifies the target object. + /// The . /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static string Invoke(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static string Invoke(JSRuntime jsRuntime, in DotNetInvocationInfo invocationInfo, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether @@ -41,12 +39,12 @@ namespace Microsoft.JSInterop.Infrastructure // because there would be nobody to police that. This method *is* the police. IDotNetObjectReference targetInstance = default; - if (dotNetObjectId != default) + if (invocationInfo.DotNetObjectId != default) { - targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - var syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); + var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); if (syncResult == null) { return null; @@ -59,13 +57,10 @@ namespace Microsoft.JSInterop.Infrastructure /// Receives a call from JS to .NET, locating and invoking the specified method asynchronously. /// /// The . - /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. - /// The assembly containing the method to be invoked. - /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. - /// For instance method calls, identifies the target object. + /// The . /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static void BeginInvokeDotNet(JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo invocationInfo, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether @@ -75,18 +70,19 @@ namespace Microsoft.JSInterop.Infrastructure // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. + var callId = invocationInfo.CallId; + object syncResult = null; ExceptionDispatchInfo syncException = null; IDotNetObjectReference targetInstance = null; - try { - if (dotNetObjectId != default) + if (invocationInfo.DotNetObjectId != default) { - targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); + syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); } catch (Exception ex) { @@ -101,7 +97,7 @@ namespace Microsoft.JSInterop.Infrastructure else if (syncException != null) { // Threw synchronously, let's respond. - jsRuntime.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(syncException.SourceException, "InvocationFailure")); } else if (syncResult is Task task) { @@ -111,23 +107,27 @@ namespace Microsoft.JSInterop.Infrastructure { if (t.Exception != null) { - var exception = t.Exception.GetBaseException(); - - jsRuntime.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); + var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(t.Exception.GetBaseException()); + var dispatchResult = new DotNetInvocationResult(exceptionDispatchInfo.SourceException, "InvocationFailure"); + jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult); } var result = TaskGenericsUtil.GetTaskResult(task); - jsRuntime.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(result)); }, TaskScheduler.Current); } else { - jsRuntime.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); + var dispatchResult = new DotNetInvocationResult(syncResult); + jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult); } } - private static object InvokeSynchronously(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) + private static object InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference objectReference, string argsJson) { + var assemblyName = callInfo.AssemblyName; + var methodIdentifier = callInfo.MethodIdentifier; + AssemblyKey assemblyKey; if (objectReference is null) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs new file mode 100644 index 0000000000..942fc34da0 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs @@ -0,0 +1,48 @@ +// 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. + +namespace Microsoft.JSInterop.Infrastructure +{ + /// + /// Information about a JSInterop call from JavaScript to .NET. + /// + public readonly struct DotNetInvocationInfo + { + /// + /// Initializes a new instance of . + /// + /// The name of the assembly containing the method. + /// The identifier of the method to be invoked. + /// The object identifier for instance method calls. + /// The call identifier. + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) + { + CallId = callId; + AssemblyName = assemblyName; + MethodIdentifier = methodIdentifier; + DotNetObjectId = dotNetObjectId; + } + + /// + /// Gets the name of the assembly containing the method. + /// Only one of or may be specified. + /// + public string AssemblyName { get; } + + /// + /// Gets the identifier of the method to be invoked. This is the value specified in the . + /// + public string MethodIdentifier { get; } + + /// + /// Gets the object identifier for instance method calls. + /// Only one of or may be specified. + /// + public long DotNetObjectId { get; } + + /// + /// Gets the call identifier. This value is when the client does not expect a value to be returned. + /// + public string CallId { get; } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs new file mode 100644 index 0000000000..d62dd532ee --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs @@ -0,0 +1,55 @@ +using System; + +namespace Microsoft.JSInterop.Infrastructure +{ + /// + /// Result of a .NET invocation that is returned to JavaScript. + /// + public readonly struct DotNetInvocationResult + { + /// + /// Constructor for a failed invocation. + /// + /// The that caused the failure. + /// The error kind. + public DotNetInvocationResult(Exception exception, string errorKind) + { + Result = default; + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + ErrorKind = errorKind; + Success = false; + } + + /// + /// Constructor for a successful invocation. + /// + /// The result. + public DotNetInvocationResult(object result) + { + Result = result; + Exception = default; + ErrorKind = default; + Success = true; + } + + /// + /// Gets the that caused the failure. + /// + public Exception Exception { get; } + + /// + /// Gets the error kind. + /// + public string ErrorKind { get; } + + /// + /// Gets the result of a successful invocation. + /// + public object Result { get; } + + /// + /// if the invocation succeeded, otherwise . + /// + public bool Success { get; } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index ba411b72db..4dca7a5db3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -150,19 +150,11 @@ namespace Microsoft.JSInterop /// /// Completes an async JS interop call from JavaScript to .NET /// - /// 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. + /// The . + /// The . protected internal abstract void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId); + DotNetInvocationInfo invocationInfo, + in DotNetInvocationResult invocationResult); internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader) { diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 2d8208b7a6..7e82a47a89 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -20,7 +20,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), " ", "SomeMethod", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(" ", "SomeMethod", default, default), "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -32,7 +32,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), "SomeAssembly", " ", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo("SomeAssembly", " ", default, default), "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -45,7 +45,7 @@ namespace Microsoft.JSInterop.Infrastructure var assemblyName = "Some.Fake.Assembly"; var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), assemblyName, "SomeMethod", default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(assemblyName, "SomeMethod", default, default), null); }); Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); @@ -67,7 +67,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), thisAssemblyName, methodIdentifier, default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, default, default), null); }); Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); @@ -79,7 +79,7 @@ namespace Microsoft.JSInterop.Infrastructure // Arrange/Act var jsRuntime = new TestJSRuntime(); SomePublicType.DidInvokeMyInvocableStaticVoid = false; - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticVoid", default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticVoid", default, default), null); // Assert Assert.Null(resultJson); @@ -91,7 +91,7 @@ namespace Microsoft.JSInterop.Infrastructure { // Arrange/Act var jsRuntime = new TestJSRuntime(); - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticNonVoid", default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticNonVoid", default, default), null); var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert @@ -104,7 +104,7 @@ namespace Microsoft.JSInterop.Infrastructure { // Arrange/Act var jsRuntime = new TestJSRuntime(); - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, default), null); var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert @@ -130,7 +130,7 @@ namespace Microsoft.JSInterop.Infrastructure }, jsRuntime.JsonSerializerOptions); // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson); var result = JsonDocument.Parse(resultJson); var root = result.RootElement; @@ -171,7 +171,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act & Assert var ex = Assert.Throws(() => - DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, method, default, argsJson)); + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, method, default, default), argsJson)); Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef' to receive the incoming value.", ex.Message); } @@ -185,7 +185,7 @@ namespace Microsoft.JSInterop.Infrastructure jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null); // Assert Assert.Null(resultJson); @@ -202,7 +202,7 @@ namespace Microsoft.JSInterop.Infrastructure jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "BaseClassInvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "BaseClassInvokableInstanceVoid", 1, default), null); // Assert Assert.Null(resultJson); @@ -219,7 +219,7 @@ namespace Microsoft.JSInterop.Infrastructure jsRuntime.Invoke("unimportant", objectRef); // Act - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, null, null, "__Dispose", objectRef.ObjectId, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "__Dispose", objectRef.ObjectId, default), null); // Assert Assert.True(objectRef.Disposed); @@ -240,7 +240,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); } @@ -259,7 +259,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); } @@ -346,7 +346,7 @@ namespace Microsoft.JSInterop.Infrastructure var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceMethod", 1, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceMethod", 1, default), argsJson); // Assert Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); @@ -369,7 +369,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson); }); Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message); @@ -392,7 +392,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson); }); Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message); @@ -419,13 +419,13 @@ namespace Microsoft.JSInterop.Infrastructure // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableAsyncMethod", 1, argsJson); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableAsyncMethod", 1, callId), argsJson); await resultTask; // Assert: Correct completion information Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.True(jsRuntime.LastCompletionStatus); - var result = Assert.IsType(jsRuntime.LastCompletionResult); + Assert.True(jsRuntime.LastCompletionResult.Success); + var result = Assert.IsType(jsRuntime.LastCompletionResult.Result); var resultDto1 = Assert.IsType(result[0]); Assert.Equal("STRING VIA JSON", resultDto1.StringVal); @@ -447,18 +447,17 @@ namespace Microsoft.JSInterop.Infrastructure // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, callId), default); await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.False(jsRuntime.LastCompletionStatus); // Fails + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; - Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); + Assert.Contains(nameof(ThrowingClass.ThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString()); } [Fact] @@ -470,18 +469,17 @@ namespace Microsoft.JSInterop.Infrastructure // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, callId), default); await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.False(jsRuntime.LastCompletionStatus); // Fails + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; - Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); + Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString()); } [Fact] @@ -491,15 +489,15 @@ namespace Microsoft.JSInterop.Infrastructure var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, callId), "not json"); await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert 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()); + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails + var exception = jsRuntime.LastCompletionResult.Exception; + Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", exception.ToString()); } [Fact] @@ -509,13 +507,13 @@ namespace Microsoft.JSInterop.Infrastructure var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableInstanceVoid", 1, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, callId), null); // Assert 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 DotNetObjectReference instance was already disposed.", result.SourceException.ToString()); + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails + var exception = jsRuntime.LastCompletionResult.Exception; + Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", exception.ToString()); } [Theory] @@ -801,8 +799,7 @@ namespace Microsoft.JSInterop.Infrastructure public string LastInvocationArgsJson { get; private set; } public string LastCompletionCallId { get; private set; } - public bool LastCompletionStatus { get; private set; } - public object LastCompletionResult { get; private set; } + public DotNetInvocationResult LastCompletionResult { get; private set; } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { @@ -823,17 +820,10 @@ namespace Microsoft.JSInterop.Infrastructure return null; } - protected internal override void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId) + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { - LastCompletionCallId = callId; - LastCompletionStatus = success; - LastCompletionResult = resultOrError; + LastCompletionCallId = invocationInfo.CallId; + LastCompletionResult = invocationResult; _nextInvocationTcs.SetResult(null); _nextInvocationTcs = new TaskCompletionSource(); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index a1caff595b..f42e0801a0 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.JSInterop.Infrastructure; using Xunit; namespace Microsoft.JSInterop @@ -113,8 +114,8 @@ namespace Microsoft.JSInterop 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"); + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + => throw new NotImplementedException("This test only covers sync calls"); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index b102ecc0b5..66e0033d2a 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.JSInterop.Infrastructure; using Xunit; namespace Microsoft.JSInterop @@ -296,18 +297,20 @@ namespace Microsoft.JSInterop var expectedMessage = "An error ocurred while invoking '[Assembly]::Method'. Swapping to 'Development' environment will " + "display more detailed information about the error that occurred."; - string GetMessage(string assembly, string method) => $"An error ocurred while invoking '[{assembly}]::{method}'. Swapping to 'Development' environment will " + + string GetMessage(DotNetInvocationInfo info) => $"An error ocurred while invoking '[{info.AssemblyName}]::{info.MethodIdentifier}'. Swapping to 'Development' environment will " + "display more detailed information about the error that occurred."; var runtime = new TestJSRuntime() { - OnDotNetException = (e, a, m) => new JSError { Message = GetMessage(a, m) } + OnDotNetException = (invocationInfo) => new JSError { Message = GetMessage(invocationInfo) } }; var exception = new Exception("Some really sensitive data in here"); + var invocation = new DotNetInvocationInfo("Assembly", "Method", 0, "0"); + var result = new DotNetInvocationResult(exception, default); // Act - runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method", 0); + runtime.EndInvokeDotNet(invocation, result); // Assert var call = runtime.EndInvokeDotNetCalls.Single(); @@ -356,20 +359,21 @@ namespace Microsoft.JSInterop public object ResultOrError { get; set; } } - public Func OnDotNetException { get; set; } + public Func OnDotNetException { get; set; } - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { - if (OnDotNetException != null && !success) + var resultOrError = invocationResult.Success ? invocationResult.Result : invocationResult.Exception; + if (OnDotNetException != null && !invocationResult.Success) { - resultOrError = OnDotNetException(resultOrError as Exception, assemblyName, methodIdentifier); + resultOrError = OnDotNetException(invocationInfo); } EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs { - CallId = callId, - Success = success, - ResultOrError = resultOrError + CallId = invocationInfo.CallId, + Success = invocationResult.Success, + ResultOrError = resultOrError, }); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index 740f02b8da..db9c5ddd36 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop { @@ -12,7 +13,7 @@ namespace Microsoft.JSInterop throw new NotImplementedException(); } - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { throw new NotImplementedException(); } 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 0996795ca3..8dd70b946a 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,7 +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 void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo callInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult dispatchResult) { } protected static void Initialize(Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime jsRuntime) { } protected override string InvokeJS(string identifier, string argsJson) { throw null; } public TRes InvokeUnmarshalled(string identifier) { throw null; } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs index b6b01d754d..654263a123 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -2,7 +2,6 @@ // 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 Microsoft.JSInterop.Infrastructure; @@ -53,7 +52,10 @@ namespace Mono.WebAssembly.Interop // Invoked via Mono's JS interop mechanism (invoke_method) private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) - => DotNetDispatcher.Invoke(Instance, assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + { + var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), callId: null); + return DotNetDispatcher.Invoke(Instance, callInfo, argsJson); + } // Invoked via Mono's JS interop mechanism (invoke_method) private static void EndInvokeJS(string argsJson) @@ -78,32 +80,20 @@ namespace Mono.WebAssembly.Interop assemblyName = assemblyNameOrDotNetObjectId; } - DotNetDispatcher.BeginInvokeDotNet(Instance, callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId); + DotNetDispatcher.BeginInvokeDotNet(Instance, callInfo, argsJson); } - protected override void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId) + protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult) { // 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(); - } + var resultOrError = dispatchResult.Success ? dispatchResult.Result : dispatchResult.Exception.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 }, JsonSerializerOptions); + var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions); BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); }