Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -211,6 +219,8 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
}
}

private static bool IsAsyncMethod(MethodInfo methodInfo) => methodInfo.GetCustomAttribute<AsyncStateMachineAttribute>() != 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>("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<InvalidOperationException>(() =>
{
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
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'SomeInteralType' to 'SomeInternalType'.

Suggested change
internal class SomeInteralType
internal class SomeInternalType

Copilot uses AI. Check for mistakes.
{
[JSInvokable("MethodOnInternalType")] public void MyMethod() { }
Expand Down
Loading