diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index 0624ee6a8457..1363a12d3fbc 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; @@ -55,7 +56,7 @@ public static class DotNetDispatcher targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); + var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson, isAsyncContext: false); if (syncResult == null) { return null; @@ -94,7 +95,7 @@ public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo i targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); + syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson, isAsyncContext: true); } catch (Exception ex) { @@ -153,7 +154,7 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(resultJson)); } - private static object? InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference? objectReference, string argsJson) + private static object? InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference? objectReference, string argsJson, bool isAsyncContext) { var assemblyName = callInfo.AssemblyName; var methodIdentifier = callInfo.MethodIdentifier; @@ -183,6 +184,13 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in (methodInfo, parameterTypes) = GetCachedMethodInfo(objectReference, methodIdentifier); } + // If the method is async but is not called asynchronously, throw to indicate the misuse + // We need to check the asyncContext flag since this method is used for both sync and async calls + if (!isAsyncContext && IsAsyncMethod(methodInfo)) + { + throw new InvalidOperationException($"The method '{methodIdentifier}' cannot be invoked synchronously because it is asynchronous. Use '{nameof(BeginInvokeDotNet)}' instead."); + } + var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes); try @@ -211,6 +219,8 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in } } + private static bool IsAsyncMethod(MethodInfo methodInfo) => methodInfo.GetCustomAttribute() != null; + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure return types of JSInvokable methods are retained.")] internal static object?[] ParseArguments(JSRuntime jsRuntime, string methodIdentifier, string arguments, Type[] parameterTypes) { diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 35233afd497d..e114b08602dc 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -886,6 +886,34 @@ public void ReceiveByteArray_Works() Assert.Equal(byteArray, jsRuntime.ByteArraysToBeRevived.Buffer[0]); } + [Fact] + public void CannotInvokeAsyncMethodSynchronously() + { + // Arrange: Track some instance plus another object we'll pass as a param + var jsRuntime = new TestJSRuntime(); + var targetInstance = new SomePublicType(); + var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; + var arg1Ref = DotNetObjectReference.Create(targetInstance); + var arg2Ref = DotNetObjectReference.Create(arg2); + jsRuntime.Invoke("unimportant", arg1Ref, arg2Ref); + + // Arrange: all args + var argsJson = JsonSerializer.Serialize(new object[] + { + new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, + arg2Ref, + }, jsRuntime.JsonSerializerOptions); + + var callId = "123"; + + // Act/Assert + var ex = Assert.Throws(() => + { + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableAsyncMethod", 1, callId), argsJson); + }); + + Assert.Equal($"The method 'InvokableAsyncMethod' cannot be invoked synchronously because it is asynchronous. Use '{nameof(DotNetDispatcher.BeginInvokeDotNet)}' instead.", ex.Message); + } internal class SomeInteralType { [JSInvokable("MethodOnInternalType")] public void MyMethod() { }