Browse Source

JSInterop: refactor JSON result deserialization (#32768)

Steve Sanderson 4 years ago
parent
commit
1632a6f959

+ 9 - 10
src/Components/Server/src/Circuits/RemoteJSRuntime.cs

@@ -59,22 +59,21 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                     errorMessage += $". For more details turn on detailed exceptions in '{nameof(CircuitOptions)}.{nameof(CircuitOptions.DetailedErrors)}'";
                 }
 
-                EndInvokeDotNetCore(invocationInfo.CallId, success: false, errorMessage);
+                _clientProxy.SendAsync("JS.EndInvokeDotNet",
+                    invocationInfo.CallId,
+                    /* success */ false,
+                    errorMessage);
             }
             else
             {
                 Log.InvokeDotNetMethodSuccess(_logger, invocationInfo);
-                EndInvokeDotNetCore(invocationInfo.CallId, success: true, invocationResult.Result);
+                _clientProxy.SendAsync("JS.EndInvokeDotNet",
+                    invocationInfo.CallId,
+                    /* success */ true,
+                    invocationResult.ResultJson);
             }
         }
 
-        private void EndInvokeDotNetCore(string callId, bool success, object resultOrError)
-        {
-            _clientProxy.SendAsync(
-                "JS.EndInvokeDotNet",
-                JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions));
-        }
-
         protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
         {
             if (_clientProxy is null)
@@ -126,7 +125,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             internal static void BeginInvokeJS(ILogger logger, long asyncHandle, string identifier) =>
                 _beginInvokeJS(logger, asyncHandle, identifier, null);
 
-            internal static void InvokeDotNetMethodException(ILogger logger, in DotNetInvocationInfo invocationInfo , Exception exception)
+            internal static void InvokeDotNetMethodException(ILogger logger, in DotNetInvocationInfo invocationInfo, Exception exception)
             {
                 if (invocationInfo.AssemblyName != null)
                 {

File diff suppressed because it is too large
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.server.js


File diff suppressed because it is too large
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.webview.js


+ 1 - 1
src/Components/Web.JS/src/Boot.Server.ts

@@ -104,7 +104,7 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
 
   connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
   connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
-  connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(DotNet.parseJsonWithRevivers(args) as [string, boolean, unknown])));
+  connection.on('JS.EndInvokeDotNet', DotNet.jsCallDispatcher.endInvokeDotNetFromJS);
 
   const renderQueue = RenderQueue.getOrCreate(logger);
   connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {

+ 9 - 1
src/Components/Web.JS/src/Boot.WebAssembly.ts

@@ -9,7 +9,7 @@ import { setEventDispatcher } from './Rendering/Events/EventDispatcher';
 import { WebAssemblyResourceLoader } from './Platform/WebAssemblyResourceLoader';
 import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
 import { BootConfigResult } from './Platform/BootConfig';
-import { Pointer } from './Platform/Platform';
+import { Pointer, System_Boolean, System_String } from './Platform/Platform';
 import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
 import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
 import { discoverComponents, discoverPersistedState, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
@@ -44,6 +44,7 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
 
   // Configure JS interop
   Blazor._internal.invokeJSFromDotNet = invokeJSFromDotNet;
+  Blazor._internal.endInvokeDotNetFromJS = endInvokeDotNetFromJS;
 
   // Configure environment for execution under Mono WebAssembly with shared-memory rendering
   const platform = Environment.setPlatform(monoPlatform);
@@ -153,6 +154,13 @@ function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any):
   }
 }
 
+function endInvokeDotNetFromJS(callId: System_String, success: System_Boolean, resultJsonOrErrorMessage: System_String): void {
+  const callIdString = BINDING.conv_string(callId)!;
+  const successBool = (success as any as number) !== 0;
+  const resultJsonOrErrorMessageString = BINDING.conv_string(resultJsonOrErrorMessage)!;
+  DotNet.jsCallDispatcher.endInvokeDotNetFromJS(callIdString, successBool, resultJsonOrErrorMessageString);
+}
+
 Blazor.start = boot;
 if (shouldAutoStart()) {
   boot().catch(error => {

+ 2 - 2
src/Components/Web.JS/src/GlobalExports.ts

@@ -7,8 +7,7 @@ import { InputFile } from './InputFile';
 import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
 import { CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
 import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
-import { Platform } from './Platform/Platform';
-import { Pointer, System_String, System_Array, System_Object } from './Platform/Platform';
+import { Platform, Pointer, System_String, System_Array, System_Object, System_Boolean } from './Platform/Platform';
 
 interface IBlazor {
   navigateTo: (uri: string, forceLoad: boolean, replace: boolean) => void;
@@ -27,6 +26,7 @@ interface IBlazor {
     forceCloseConnection?: () => Promise<void>;
     InputFile?: typeof InputFile,
     invokeJSFromDotNet?: (callInfo: Pointer, arg0: any, arg1: any, arg2: any) => any;
+    endInvokeDotNetFromJS?: (callId: System_String, success: System_Boolean, resultJsonOrErrorMessage: System_String) => void;
     getPersistedState?: () => System_String;
     attachRootComponentToElement?: (arg0: any, arg1: any, arg2: any) => void;
     registeredComponents?: {

+ 1 - 0
src/Components/Web.JS/src/Platform/Platform.ts

@@ -32,6 +32,7 @@ export interface HeapLock {
 // for compile-time checking, since TypeScript doesn't support nominal types.
 export interface MethodHandle { MethodHandle__DO_NOT_IMPLEMENT: any }
 export interface System_Object { System_Object__DO_NOT_IMPLEMENT: any }
+export interface System_Boolean { System_Boolean__DO_NOT_IMPLEMENT: any }
 export interface System_String extends System_Object { System_String__DO_NOT_IMPLEMENT: any }
 export interface System_Array<T> extends System_Object { System_Array__DO_NOT_IMPLEMENT: any }
 export interface Pointer { Pointer__DO_NOT_IMPLEMENT: any }

+ 1 - 4
src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts

@@ -31,10 +31,7 @@ export function startIpcReceiver() {
 
     'BeginInvokeJS': DotNet.jsCallDispatcher.beginInvokeJSFromDotNet,
 
-    'EndInvokeDotNet': (asyncCallId: string, success: boolean, invocationResultOrError: string) => {
-      const resultOrExceptionMessage: any = DotNet.parseJsonWithRevivers(invocationResultOrError);
-      DotNet.jsCallDispatcher.endInvokeDotNetFromJS(asyncCallId, success, resultOrExceptionMessage);
-    },
+    'EndInvokeDotNet': DotNet.jsCallDispatcher.endInvokeDotNetFromJS,
 
     'Navigate': navigationManagerFunctions.navigateTo,
   };

+ 5 - 9
src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs

@@ -61,15 +61,11 @@ namespace Microsoft.JSInterop.WebAssembly
         [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "TODO: This should be in the xml suppressions file, but can't be because https://github.com/mono/linker/issues/2006")]
         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.
-            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[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions);
-            BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args, JSCallResultType.Default, 0);
+            var resultJsonOrErrorMessage = dispatchResult.Success
+                ? dispatchResult.ResultJson!
+                : dispatchResult.Exception!.ToString();
+            InvokeUnmarshalled<string?, bool, string, object>("Blazor._internal.endInvokeDotNetFromJS",
+                callInfo.CallId, dispatchResult.Success, resultJsonOrErrorMessage);
         }
 
         internal TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2, long targetInstanceId)

+ 4 - 13
src/Components/WebView/WebView/src/Services/WebViewJSRuntime.cs

@@ -32,19 +32,10 @@ namespace Microsoft.AspNetCore.Components.WebView.Services
 
         protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
         {
-            if (!invocationResult.Success)
-            {
-                EndInvokeDotNetCore(invocationInfo.CallId, success: false, invocationResult.Exception.ToString());
-            }
-            else
-            {
-                EndInvokeDotNetCore(invocationInfo.CallId, success: true, invocationResult.Result);
-            }
-
-            void EndInvokeDotNetCore(string callId, bool success, object resultOrError)
-            {
-                _ipcSender.EndInvokeDotNet(callId, success, JsonSerializer.Serialize(resultOrError, JsonSerializerOptions));
-            }
+            var resultJsonOrErrorMessage = invocationResult.Success
+                ? invocationResult.ResultJson
+                : invocationResult.Exception.ToString();
+            _ipcSender.EndInvokeDotNet(invocationInfo.CallId, invocationResult.Success, resultJsonOrErrorMessage);
         }
     }
 }

+ 7 - 5
src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts

@@ -152,10 +152,10 @@ export module DotNet {
 
   /**
    * Parses the given JSON string using revivers to restore args passed from .NET to JS.
-   * 
+   *
    * @param json The JSON stirng to parse.
    */
-  export function parseJsonWithRevivers(json: string): any {
+  function parseJsonWithRevivers(json: string): any {
     return json ? JSON.parse(json, (key, initialValue) => {
       // Invoke each reviver in order, passing the output from the previous reviver,
       // so that each one gets a chance to transform the value
@@ -339,10 +339,12 @@ export module DotNet {
      * Receives notification that an async call from JS to .NET has completed.
      * @param asyncCallId The identifier supplied in an earlier call to beginInvokeDotNetFromJS.
      * @param success A flag to indicate whether the operation completed successfully.
-     * @param resultOrExceptionMessage Either the operation result or an error message.
+     * @param resultJsonOrExceptionMessage Either the operation result as JSON, or an error message.
      */
-    endInvokeDotNetFromJS: (asyncCallId: string, success: boolean, resultOrExceptionMessage: any): void => {
-      const resultOrError = success ? resultOrExceptionMessage : new Error(resultOrExceptionMessage);
+    endInvokeDotNetFromJS: (asyncCallId: string, success: boolean, resultJsonOrExceptionMessage: string): void => {
+      const resultOrError = success
+        ? parseJsonWithRevivers(resultJsonOrExceptionMessage)
+        : new Error(resultJsonOrExceptionMessage);
       completePendingCall(parseInt(asyncCallId), success, resultOrError);
     }
   }

+ 17 - 13
src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs

@@ -108,26 +108,30 @@ namespace Microsoft.JSInterop.Infrastructure
             {
                 // Returned a task - we need to continue that task and then report an exception
                 // or return the value.
-                task.ContinueWith(t =>
-                {
-                    if (t.Exception != null)
-                    {
-                        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(invocationInfo, new DotNetInvocationResult(result));
-                }, TaskScheduler.Current);
+                task.ContinueWith(t => EndInvokeDotNetAfterTask(t, jsRuntime, invocationInfo), TaskScheduler.Current);
             }
             else
             {
-                var dispatchResult = new DotNetInvocationResult(syncResult);
+                var syncResultJson = JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions);
+                var dispatchResult = new DotNetInvocationResult(syncResultJson);
                 jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult);
             }
         }
 
+        private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in DotNetInvocationInfo invocationInfo)
+        {
+            if (task.Exception != null)
+            {
+                var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(task.Exception.GetBaseException());
+                var dispatchResult = new DotNetInvocationResult(exceptionDispatchInfo.SourceException, "InvocationFailure");
+                jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult);
+            }
+
+            var result = TaskGenericsUtil.GetTaskResult(task);
+            var resultJson = JsonSerializer.Serialize(result, jsRuntime.JsonSerializerOptions);
+            jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(resultJson));
+        }
+
         private static object? InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference? objectReference, string argsJson)
         {
             var assemblyName = callInfo.AssemblyName;

+ 7 - 7
src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs

@@ -12,9 +12,9 @@ namespace Microsoft.JSInterop.Infrastructure
         /// </summary>
         /// <param name="exception">The <see cref="System.Exception"/> that caused the failure.</param>
         /// <param name="errorKind">The error kind.</param>
-        public DotNetInvocationResult(Exception exception, string? errorKind)
+        internal DotNetInvocationResult(Exception exception, string? errorKind)
         {
-            Result = default;
+            ResultJson = default;
             Exception = exception ?? throw new ArgumentNullException(nameof(exception));
             ErrorKind = errorKind;
             Success = false;
@@ -23,10 +23,10 @@ namespace Microsoft.JSInterop.Infrastructure
         /// <summary>
         /// Constructor for a successful invocation.
         /// </summary>
-        /// <param name="result">The result.</param>
-        public DotNetInvocationResult(object? result)
+        /// <param name="resultJson">The JSON representation of the result.</param>
+        internal DotNetInvocationResult(string? resultJson)
         {
-            Result = result;
+            ResultJson = resultJson;
             Exception = default;
             ErrorKind = default;
             Success = true;
@@ -43,9 +43,9 @@ namespace Microsoft.JSInterop.Infrastructure
         public string? ErrorKind { get; }
 
         /// <summary>
-        /// Gets the result of a successful invocation.
+        /// Gets a JSON representation of the result of a successful invocation.
         /// </summary>
-        public object? Result { get; }
+        public string? ResultJson { get; }
 
         /// <summary>
         /// <see langword="true"/> if the invocation succeeded, otherwise <see langword="false"/>.

+ 12 - 0
src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml

@@ -1,6 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <linker>
   <assembly fullname="Microsoft.JSInterop, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
+    <attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
+      <argument>ILLink</argument>
+      <argument>IL2026</argument>
+      <property name="Scope">member</property>
+      <property name="Target">M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime,Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo,System.String)</property>
+    </attribute>
+    <attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
+      <argument>ILLink</argument>
+      <argument>IL2026</argument>
+      <property name="Scope">member</property>
+      <property name="Target">M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.EndInvokeDotNetAfterTask(System.Threading.Tasks.Task,Microsoft.JSInterop.JSRuntime,Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo@)</property>
+    </attribute>
     <attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
       <argument>ILLink</argument>
       <argument>IL2026</argument>

+ 4 - 0
src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt

@@ -1,4 +1,8 @@
 #nullable enable
 Microsoft.JSInterop.Implementation.JSObjectReferenceJsonWorker
+Microsoft.JSInterop.Infrastructure.DotNetInvocationResult.ResultJson.get -> string?
 static Microsoft.JSInterop.Implementation.JSObjectReferenceJsonWorker.ReadJSObjectReferenceIdentifier(ref System.Text.Json.Utf8JsonReader reader) -> long
 static Microsoft.JSInterop.Implementation.JSObjectReferenceJsonWorker.WriteJSObjectReference(System.Text.Json.Utf8JsonWriter! writer, Microsoft.JSInterop.Implementation.JSObjectReference! objectReference) -> void
+*REMOVED*Microsoft.JSInterop.Infrastructure.DotNetInvocationResult.DotNetInvocationResult(System.Exception! exception, string? errorKind) -> void
+*REMOVED*Microsoft.JSInterop.Infrastructure.DotNetInvocationResult.DotNetInvocationResult(object? result) -> void
+*REMOVED*Microsoft.JSInterop.Infrastructure.DotNetInvocationResult.Result.get -> object?

+ 15 - 10
src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs

@@ -479,15 +479,14 @@ namespace Microsoft.JSInterop.Infrastructure
             // Assert: Correct completion information
             Assert.Equal(callId, jsRuntime.LastCompletionCallId);
             Assert.True(jsRuntime.LastCompletionResult.Success);
-            var result = Assert.IsType<object[]>(jsRuntime.LastCompletionResult.Result);
-            var resultDto1 = Assert.IsType<TestDTO>(result[0]);
+            var resultJson = Assert.IsType<string>(jsRuntime.LastCompletionResult.ResultJson);
+            var result = JsonSerializer.Deserialize<SomePublicType.InvokableAsyncMethodResult>(resultJson, jsRuntime.JsonSerializerOptions);
 
-            Assert.Equal("STRING VIA JSON", resultDto1.StringVal);
-            Assert.Equal(2000, resultDto1.IntVal);
+            Assert.Equal("STRING VIA JSON", result.SomeDTO.StringVal);
+            Assert.Equal(2000, result.SomeDTO.IntVal);
 
             // Assert: Second result value marshalled by ref
-            var resultDto2Ref = Assert.IsType<DotNetObjectReference<TestDTO>>(result[1]);
-            var resultDto2 = resultDto2Ref.Value;
+            var resultDto2 = result.SomeDTORef.Value;
             Assert.Equal("MY STRING", resultDto2.StringVal);
             Assert.Equal(2468, resultDto2.IntVal);
         }
@@ -787,24 +786,30 @@ namespace Microsoft.JSInterop.Infrastructure
             }
 
             [JSInvokable]
-            public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectReference<TestDTO> dtoByRefWrapper)
+            public async Task<InvokableAsyncMethodResult> InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectReference<TestDTO> dtoByRefWrapper)
             {
                 await Task.Delay(50);
                 var dtoByRef = dtoByRefWrapper.Value;
-                return new object[]
+                return new InvokableAsyncMethodResult
                 {
-                    new TestDTO // Return via JSON
+                    SomeDTO = new TestDTO // Return via JSON
                     {
                         StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
                         IntVal = dtoViaJson.IntVal * 2,
                     },
-                    DotNetObjectReference.Create(new TestDTO // Return by ref
+                    SomeDTORef = DotNetObjectReference.Create(new TestDTO // Return by ref
                     {
                         StringVal = dtoByRef.StringVal.ToUpperInvariant(),
                         IntVal = dtoByRef.IntVal * 2,
                     })
                 };
             }
+
+            public class InvokableAsyncMethodResult
+            {
+                public TestDTO SomeDTO { get; set; }
+                public DotNetObjectReference<TestDTO> SomeDTORef { get; set; }
+            }
         }
 
         public class BaseClass

+ 18 - 25
src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs

@@ -297,19 +297,9 @@ namespace Microsoft.JSInterop
         public void CanSanitizeDotNetInteropExceptions()
         {
             // Arrange
-            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(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 = (invocationInfo) => new JSError { Message = GetMessage(invocationInfo) }
-            };
-
+            var runtime = new TestJSRuntime();
             var exception = new Exception("Some really sensitive data in here");
-            var invocation = new DotNetInvocationInfo("Assembly", "Method", 0, "0");
+            var invocation = new DotNetInvocationInfo("TestAssembly", "TestMethod", 0, "0");
             var result = new DotNetInvocationResult(exception, default);
 
             // Act
@@ -319,13 +309,22 @@ namespace Microsoft.JSInterop
             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);
+
+            var error = Assert.IsType<JSError>(call.ResultError);
+            Assert.Same(exception, error.InnerException);
+            Assert.Equal(invocation, error.InvocationInfo);
         }
 
         private class JSError
         {
-            public string? Message { get; set; }
+            public DotNetInvocationInfo InvocationInfo { get; set; }
+            public Exception? InnerException { get; set; }
+
+            public JSError(DotNetInvocationInfo invocationInfo, Exception? innerException)
+            {
+                InvocationInfo = invocationInfo;
+                InnerException = innerException;
+            }
         }
 
         private class TestPoco
@@ -359,24 +358,18 @@ namespace Microsoft.JSInterop
             {
                 public string? CallId { get; set; }
                 public bool Success { get; set; }
-                public object? ResultOrError { get; set; }
+                public string? ResultJson { get; set; }
+                public JSError? ResultError { get; set; }
             }
 
-            public Func<DotNetInvocationInfo, object>? OnDotNetException { get; set; }
-
             protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
             {
-                var resultOrError = invocationResult.Success ? invocationResult.Result : invocationResult.Exception;
-                if (OnDotNetException != null && !invocationResult.Success)
-                {
-                    resultOrError = OnDotNetException(invocationInfo);
-                }
-
                 EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs
                 {
                     CallId = invocationInfo.CallId,
                     Success = invocationResult.Success,
-                    ResultOrError = resultOrError,
+                    ResultJson = invocationResult.ResultJson,
+                    ResultError = invocationResult.Success ? null : new JSError(invocationInfo, invocationResult.Exception),
                 });
             }
 

Some files were not shown because too many files changed in this diff