Skip to content

Commit 365cbcf

Browse files
committed
Add error message when invoking async methods synchronously in JSInterop
1 parent d32513d commit 365cbcf

File tree

2 files changed

+41
-3
lines changed

2 files changed

+41
-3
lines changed

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.Reflection;
99
using System.Reflection.Metadata;
10+
using System.Runtime.CompilerServices;
1011
using System.Runtime.ExceptionServices;
1112
using System.Text;
1213
using System.Text.Json;
@@ -55,7 +56,7 @@ public static class DotNetDispatcher
5556
targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId);
5657
}
5758

58-
var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson);
59+
var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson, isAsyncContext: false);
5960
if (syncResult == null)
6061
{
6162
return null;
@@ -94,7 +95,7 @@ public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo i
9495
targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId);
9596
}
9697

97-
syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson);
98+
syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson, isAsyncContext: true);
9899
}
99100
catch (Exception ex)
100101
{
@@ -153,7 +154,7 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
153154
jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(resultJson));
154155
}
155156

156-
private static object? InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference? objectReference, string argsJson)
157+
private static object? InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference? objectReference, string argsJson, bool isAsyncContext)
157158
{
158159
var assemblyName = callInfo.AssemblyName;
159160
var methodIdentifier = callInfo.MethodIdentifier;
@@ -183,6 +184,13 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
183184
(methodInfo, parameterTypes) = GetCachedMethodInfo(objectReference, methodIdentifier);
184185
}
185186

187+
// If the method is async but is not called asynchronously, throw to indicate the misuse
188+
// We need to check the asyncContext flag since this method is used for both sync and async calls
189+
if (!isAsyncContext && IsAsyncMethod(methodInfo))
190+
{
191+
throw new InvalidOperationException($"The method '{methodIdentifier}' cannot be invoked synchronously because it is asynchronous. Use '{nameof(BeginInvokeDotNet)}' instead.");
192+
}
193+
186194
var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes);
187195

188196
try
@@ -211,6 +219,8 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
211219
}
212220
}
213221

222+
private static bool IsAsyncMethod(MethodInfo methodInfo) => methodInfo.GetCustomAttribute<AsyncStateMachineAttribute>() != null;
223+
214224
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We expect application code is configured to ensure return types of JSInvokable methods are retained.")]
215225
internal static object?[] ParseArguments(JSRuntime jsRuntime, string methodIdentifier, string arguments, Type[] parameterTypes)
216226
{

src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,34 @@ public void ReceiveByteArray_Works()
886886
Assert.Equal(byteArray, jsRuntime.ByteArraysToBeRevived.Buffer[0]);
887887
}
888888

889+
[Fact]
890+
public void CannotInvokeAsyncMethodSynchronously()
891+
{
892+
// Arrange: Track some instance plus another object we'll pass as a param
893+
var jsRuntime = new TestJSRuntime();
894+
var targetInstance = new SomePublicType();
895+
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
896+
var arg1Ref = DotNetObjectReference.Create(targetInstance);
897+
var arg2Ref = DotNetObjectReference.Create(arg2);
898+
jsRuntime.Invoke<object>("unimportant", arg1Ref, arg2Ref);
899+
900+
// Arrange: all args
901+
var argsJson = JsonSerializer.Serialize(new object[]
902+
{
903+
new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
904+
arg2Ref,
905+
}, jsRuntime.JsonSerializerOptions);
906+
907+
var callId = "123";
908+
909+
// Act/Assert
910+
var ex = Assert.Throws<InvalidOperationException>(() =>
911+
{
912+
DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableAsyncMethod", 1, callId), argsJson);
913+
});
914+
915+
Assert.Equal($"The method 'InvokableAsyncMethod' cannot be invoked synchronously because it is asynchronous. Use '{nameof(DotNetDispatcher.BeginInvokeDotNet)}' instead.", ex.Message);
916+
}
889917
internal class SomeInteralType
890918
{
891919
[JSInvokable("MethodOnInternalType")] public void MyMethod() { }

0 commit comments

Comments
 (0)