diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index f590e275dff7..9ad4162a54c6 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -61,6 +61,17 @@ public static class DotNetDispatcher return null; } + // A Task or ValueTask result means the targeted method is asynchronous and was invoked + // synchronously. Serializing the task instance produces confusing serialization-related + // errors, so fail with a message that explains the actual problem. + if (syncResult is Task + || (syncResult.GetType() is { IsGenericType: true } syncResultType + && syncResultType.GetGenericTypeDefinition() == typeof(ValueTask<>))) + { + throw new InvalidOperationException( + $"Cannot invoke the method '{invocationInfo.MethodIdentifier}' synchronously from JavaScript because it is asynchronous. Use 'invokeMethodAsync' instead of 'invokeMethod'."); + } + return JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 35233afd497d..692701300a79 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -616,6 +616,34 @@ public void CannotInvokeWithMoreParameters() Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message); } + [Fact] + public void CannotInvokeAsyncMethodReturningTaskSynchronously() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + + // Act & Assert + var ex = Assert.Throws(() => + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableAsyncMethodReturningTask), default, default), null)); + + Assert.Contains(nameof(SomePublicType.InvokableAsyncMethodReturningTask), ex.Message); + Assert.Contains("invokeMethodAsync", ex.Message); + } + + [Fact] + public void CannotInvokeAsyncMethodReturningGenericTaskSynchronously() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + + // Act & Assert + var ex = Assert.Throws(() => + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableAsyncMethodReturningTaskWithResult), default, default), null)); + + Assert.Contains(nameof(SomePublicType.InvokableAsyncMethodReturningTaskWithResult), ex.Message); + Assert.Contains("invokeMethodAsync", ex.Message); + } + [Fact] public async Task CanInvokeAsyncMethod() { @@ -709,6 +737,23 @@ public async Task CanInvokeAsyncMethodReturningNonGenericValueTask() Assert.True(jsRuntime.LastCompletionResult.Success); } + [Fact] + public async Task CanInvokeAsyncMethodReturningTask() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + + // Act + var callId = "123"; + var resultTask = jsRuntime.NextInvocationTask; + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableAsyncMethodReturningTask), default, callId), null); + await resultTask; // This won't throw, it sets properties on the jsRuntime. + + // Assert + Assert.Equal(callId, jsRuntime.LastCompletionCallId); + Assert.True(jsRuntime.LastCompletionResult.Success); + } + [Fact] public async Task CanInvokeSyncThrowingMethod() { @@ -1008,6 +1053,19 @@ public async ValueTask InvokableAsyncMethodReturningValueTaskNonGeneric() return; } + [JSInvokable] + public static async Task InvokableAsyncMethodReturningTask() + { + await Task.CompletedTask; + } + + [JSInvokable] + public static async Task InvokableAsyncMethodReturningTaskWithResult() + { + await Task.CompletedTask; + return new TestDTO { StringVal = "Test", IntVal = 123 }; + } + public class InvokableAsyncMethodResult { public TestDTO SomeDTO { get; set; }