From 1066bc463255e9f8a2af1c2bd81c65435f4a3fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 24 Apr 2025 00:02:55 +0200 Subject: [PATCH 1/4] Add AsFunction extension method to IJSObjectReference --- .../src/Infrastructure/JSFunctionReference.cs | 197 ++++++++++++++++++ .../src/JSObjectReferenceExtensions.cs | 16 ++ .../src/PublicAPI.Unshipped.txt | 1 + 3 files changed, 214 insertions(+) create mode 100644 src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs new file mode 100644 index 000000000000..1da1fe9f75c2 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using static Microsoft.AspNetCore.Internal.LinkerFlags; + +namespace Microsoft.JSInterop.Infrastructure; + +/// +/// TODO(OR): Document this. +/// +internal readonly struct JSFunctionReference +{ + private static readonly ConcurrentDictionary _methodInfoCache = new(); + + private readonly IJSObjectReference _jsObjectReference; + + /// + /// Caches previously constructed MethodInfo instances for various delegate types. + /// + public static ConcurrentDictionary MethodInfoCache => _methodInfoCache; + + public JSFunctionReference(IJSObjectReference jsObjectReference) + { + _jsObjectReference = jsObjectReference; + } + + /// + /// TODO(OR): Document this. + /// + public static T CreateInvocationDelegate(IJSObjectReference jsObjectReference) where T : Delegate + { + Type delegateType = typeof(T); + + if (MethodInfoCache.TryGetValue(delegateType, out var wrapperMethod)) + { + var wrapper = new JSFunctionReference(jsObjectReference); + return (T)Delegate.CreateDelegate(delegateType, wrapper, wrapperMethod); + } + + if (!delegateType.IsGenericType) + { + throw new ArgumentException("The delegate type must be a Func."); + } + + var returnTypeCandidate = delegateType.GenericTypeArguments[^1]; + + if (returnTypeCandidate == typeof(ValueTask)) + { + var methodName = GetVoidMethodName(delegateType.GetGenericTypeDefinition()); + return CreateVoidDelegate(delegateType, jsObjectReference, methodName); + } + else if (returnTypeCandidate == typeof(Task)) + { + var methodName = GetVoidTaskMethodName(delegateType.GetGenericTypeDefinition()); + return CreateVoidDelegate(delegateType, jsObjectReference, methodName); + } + else + { + var returnTypeGenericTypeDefinition = returnTypeCandidate.GetGenericTypeDefinition(); + + if (returnTypeGenericTypeDefinition == typeof(ValueTask<>)) + { + var methodName = GetMethodName(delegateType.GetGenericTypeDefinition()); + var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; + return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); + } + + else if (returnTypeGenericTypeDefinition == typeof(Task<>)) + { + var methodName = GetTaskMethodName(delegateType.GetGenericTypeDefinition()); + var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; + return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); + } + else + { + throw new ArgumentException("The delegate return type must be Task or ValueTask."); + } + } + } + + private static T CreateDelegate(Type delegateType, Type returnType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate + { + var wrapper = new JSFunctionReference(jsObjectReference); + var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; + Type[] genericArguments = [.. delegateType.GenericTypeArguments[..^1], returnType]; + +#pragma warning disable IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. + var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments); +#pragma warning restore IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. + + MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + + return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); + } + + private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate + { + var wrapper = new JSFunctionReference(jsObjectReference); + var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; + Type[] genericArguments = delegateType.GenericTypeArguments[..^1]; + +#pragma warning disable IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. + var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments); +#pragma warning restore IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. + + MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + + return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); + } + + private static string GetMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + { + var gd when gd == typeof(Func<>) => nameof(Invoke0), + var gd when gd == typeof(Func<,>) => nameof(Invoke1), + var gd when gd == typeof(Func<,,>) => nameof(Invoke2), + var gd when gd == typeof(Func<,,,>) => nameof(Invoke3), + var gd when gd == typeof(Func<,,,,>) => nameof(Invoke4), + var gd when gd == typeof(Func<,,,,,>) => nameof(Invoke5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(Invoke6), + _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + }; + + private static string GetTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + { + var gd when gd == typeof(Func<>) => nameof(InvokeTask0), + var gd when gd == typeof(Func<,>) => nameof(InvokeTask1), + var gd when gd == typeof(Func<,,>) => nameof(InvokeTask2), + var gd when gd == typeof(Func<,,,>) => nameof(InvokeTask3), + var gd when gd == typeof(Func<,,,,>) => nameof(InvokeTask4), + var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeTask5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeTask6), + _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + }; + + private static string GetVoidMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + { + var gd when gd == typeof(Func<>) => nameof(InvokeVoid0), + var gd when gd == typeof(Func<,>) => nameof(InvokeVoid1), + var gd when gd == typeof(Func<,,>) => nameof(InvokeVoid2), + var gd when gd == typeof(Func<,,,>) => nameof(InvokeVoid3), + var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoid4), + var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoid5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoid6), + _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + }; + + private static string GetVoidTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + { + var gd when gd == typeof(Func<>) => nameof(InvokeVoidTask0), + var gd when gd == typeof(Func<,>) => nameof(InvokeVoidTask1), + var gd when gd == typeof(Func<,,>) => nameof(InvokeVoidTask2), + var gd when gd == typeof(Func<,,,>) => nameof(InvokeVoidTask3), + var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoidTask4), + var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoidTask5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoidTask6), + _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + }; + + // Variants returning ValueTask using InvokeAsync + public ValueTask Invoke0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync(string.Empty, []); + public ValueTask Invoke1(T1 arg1) => _jsObjectReference.InvokeAsync(string.Empty, [arg1]); + public ValueTask Invoke2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2]); + public ValueTask Invoke3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3]); + public ValueTask Invoke4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4]); + public ValueTask Invoke5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]); + public ValueTask Invoke6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]); + + // Variants returning ValueTask using InvokeVoidAsync + public ValueTask InvokeVoid0() => _jsObjectReference.InvokeVoidAsync(string.Empty); + public ValueTask InvokeVoid1(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]); + public ValueTask InvokeVoid2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]); + public ValueTask InvokeVoid3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]); + public ValueTask InvokeVoid4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]); + public ValueTask InvokeVoid5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]); + public ValueTask InvokeVoid6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]); + + // Variants returning Task using InvokeAsync + public Task InvokeTask0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync(string.Empty, []).AsTask(); + public Task InvokeTask1(T1 arg1) => _jsObjectReference.InvokeAsync(string.Empty, [arg1]).AsTask(); + public Task InvokeTask2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2]).AsTask(); + public Task InvokeTask3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3]).AsTask(); + public Task InvokeTask4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask(); + public Task InvokeTask5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask(); + public Task InvokeTask6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask(); + + // Variants returning Task using InvokeVoidAsync + public Task InvokeVoidTask0() => _jsObjectReference.InvokeVoidAsync(string.Empty).AsTask(); + public Task InvokeVoidTask1(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]).AsTask(); + public Task InvokeVoidTask2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]).AsTask(); + public Task InvokeVoidTask3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]).AsTask(); + public Task InvokeVoidTask4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask(); + public Task InvokeVoidTask5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask(); + public Task InvokeVoidTask6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask(); +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index 4ddcc358ef2f..a1af2aada3ff 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -167,4 +169,18 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args); } + + /// + /// TODO(OR): Document this. + /// + /// + /// + /// + /// + public static T AsFunction(this IJSObjectReference jsObjectReference) where T : Delegate + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return JSFunctionReference.CreateInvocationDelegate(jsObjectReference); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 3270d74b01a0..46e2ba836893 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T! static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask From 04f62b68daaf9bab9bccb7e6a3fcaa13da905804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 24 Apr 2025 14:29:06 +0200 Subject: [PATCH 2/4] Add unit tests, improve error handling --- .../src/Infrastructure/JSFunctionReference.cs | 58 +++--- .../src/JSObjectReferenceExtensions.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 2 +- .../test/JSObjectReferenceExtensionsTest.cs | 192 ++++++++++++++++++ 4 files changed, 224 insertions(+), 30 deletions(-) create mode 100644 src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs index 1da1fe9f75c2..b4ea2d0b9913 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs @@ -13,14 +13,12 @@ namespace Microsoft.JSInterop.Infrastructure; /// internal readonly struct JSFunctionReference { - private static readonly ConcurrentDictionary _methodInfoCache = new(); - - private readonly IJSObjectReference _jsObjectReference; - /// /// Caches previously constructed MethodInfo instances for various delegate types. /// - public static ConcurrentDictionary MethodInfoCache => _methodInfoCache; + private static readonly ConcurrentDictionary _methodInfoCache = new(); + + private readonly IJSObjectReference _jsObjectReference; public JSFunctionReference(IJSObjectReference jsObjectReference) { @@ -34,7 +32,7 @@ public static T CreateInvocationDelegate(IJSObjectReference jsObjectReference { Type delegateType = typeof(T); - if (MethodInfoCache.TryGetValue(delegateType, out var wrapperMethod)) + if (_methodInfoCache.TryGetValue(delegateType, out var wrapperMethod)) { var wrapper = new JSFunctionReference(jsObjectReference); return (T)Delegate.CreateDelegate(delegateType, wrapper, wrapperMethod); @@ -42,48 +40,45 @@ public static T CreateInvocationDelegate(IJSObjectReference jsObjectReference if (!delegateType.IsGenericType) { - throw new ArgumentException("The delegate type must be a Func."); + throw CreateInvalidTypeParameterException(delegateType); } var returnTypeCandidate = delegateType.GenericTypeArguments[^1]; if (returnTypeCandidate == typeof(ValueTask)) { - var methodName = GetVoidMethodName(delegateType.GetGenericTypeDefinition()); + var methodName = GetVoidMethodName(delegateType); return CreateVoidDelegate(delegateType, jsObjectReference, methodName); } else if (returnTypeCandidate == typeof(Task)) { - var methodName = GetVoidTaskMethodName(delegateType.GetGenericTypeDefinition()); + var methodName = GetVoidTaskMethodName(delegateType); return CreateVoidDelegate(delegateType, jsObjectReference, methodName); } - else + else if (returnTypeCandidate.IsGenericType) { var returnTypeGenericTypeDefinition = returnTypeCandidate.GetGenericTypeDefinition(); if (returnTypeGenericTypeDefinition == typeof(ValueTask<>)) { - var methodName = GetMethodName(delegateType.GetGenericTypeDefinition()); + var methodName = GetMethodName(delegateType); var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); } else if (returnTypeGenericTypeDefinition == typeof(Task<>)) { - var methodName = GetTaskMethodName(delegateType.GetGenericTypeDefinition()); + var methodName = GetTaskMethodName(delegateType); var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); } - else - { - throw new ArgumentException("The delegate return type must be Task or ValueTask."); - } } + + throw CreateInvalidTypeParameterException(delegateType); } private static T CreateDelegate(Type delegateType, Type returnType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate { - var wrapper = new JSFunctionReference(jsObjectReference); var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; Type[] genericArguments = [.. delegateType.GenericTypeArguments[..^1], returnType]; @@ -91,14 +86,14 @@ private static T CreateDelegate(Type delegateType, Type returnType, IJSObject var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments); #pragma warning restore IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. - MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + _methodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + var wrapper = new JSFunctionReference(jsObjectReference); return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); } private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate { - var wrapper = new JSFunctionReference(jsObjectReference); var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; Type[] genericArguments = delegateType.GenericTypeArguments[..^1]; @@ -106,12 +101,19 @@ private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsO var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments); #pragma warning restore IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. - MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + _methodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + var wrapper = new JSFunctionReference(jsObjectReference); return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); } - private static string GetMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + private static InvalidOperationException CreateInvalidTypeParameterException(Type delegateType) + { + return new InvalidOperationException( + $"The type {delegateType} is not supported as the type parameter of '{nameof(JSObjectReferenceExtensions.AsAsyncFunction)}'. 'T' must be Func with the return type Task or ValueTask."); + } + + private static string GetMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch { var gd when gd == typeof(Func<>) => nameof(Invoke0), var gd when gd == typeof(Func<,>) => nameof(Invoke1), @@ -120,10 +122,10 @@ private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsO var gd when gd == typeof(Func<,,,,>) => nameof(Invoke4), var gd when gd == typeof(Func<,,,,,>) => nameof(Invoke5), var gd when gd == typeof(Func<,,,,,,>) => nameof(Invoke6), - _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + _ => throw CreateInvalidTypeParameterException(delegateType) }; - private static string GetTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + private static string GetTaskMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch { var gd when gd == typeof(Func<>) => nameof(InvokeTask0), var gd when gd == typeof(Func<,>) => nameof(InvokeTask1), @@ -132,10 +134,10 @@ private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsO var gd when gd == typeof(Func<,,,,>) => nameof(InvokeTask4), var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeTask5), var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeTask6), - _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + _ => throw CreateInvalidTypeParameterException(delegateType) }; - private static string GetVoidMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + private static string GetVoidMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch { var gd when gd == typeof(Func<>) => nameof(InvokeVoid0), var gd when gd == typeof(Func<,>) => nameof(InvokeVoid1), @@ -144,10 +146,10 @@ private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsO var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoid4), var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoid5), var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoid6), - _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + _ => throw CreateInvalidTypeParameterException(delegateType) }; - private static string GetVoidTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch + private static string GetVoidTaskMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch { var gd when gd == typeof(Func<>) => nameof(InvokeVoidTask0), var gd when gd == typeof(Func<,>) => nameof(InvokeVoidTask1), @@ -156,7 +158,7 @@ private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsO var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoidTask4), var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoidTask5), var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoidTask6), - _ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.") + _ => throw CreateInvalidTypeParameterException(delegateType) }; // Variants returning ValueTask using InvokeAsync diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index a1af2aada3ff..bd18e540af85 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -177,7 +177,7 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen /// /// /// - public static T AsFunction(this IJSObjectReference jsObjectReference) where T : Delegate + public static T AsAsyncFunction(this IJSObjectReference jsObjectReference) where T : Delegate { ArgumentNullException.ThrowIfNull(jsObjectReference); diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 46e2ba836893..cab579e8a24e 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -56,7 +56,7 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsAsyncFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T! static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs new file mode 100644 index 000000000000..b170a0ad3737 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.JSInterop.Implementation; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.JSInterop.Tests; + +public class JSObjectReferenceExtensionsTest +{ + [Fact] + public void AsAsyncFunction_WithVoidValueTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>(func); + } + + [Fact] + public void AsAsyncFunction_WithVoidTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>(func); + } + + [Fact] + public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>>(func); + } + + [Fact] + public void AsAsyncFunction_WithTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>>(func); + } + + [Fact] + public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc_ThatInvokesInterop() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42)); + var reader = new Utf8JsonReader(bytes); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + ValueTask task = func(1); + + jsRuntime.EndInvokeJS( + jsRuntime.InvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + + // Assert + Assert.True(task.IsCompleted); +#pragma warning disable xUnit1031 // Do not use blocking task operations in test method + Assert.Equal(42, task.Result); +#pragma warning restore xUnit1031 // Do not use blocking task operations in test method + } + + [Fact] + public void AsAsyncFunction_WithTaskFunc_ReturnsFunc_ThatInvokesInterop() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42)); + var reader = new Utf8JsonReader(bytes); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + Task task = func(1); + + jsRuntime.EndInvokeJS( + jsRuntime.InvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + + // Assert + Assert.True(task.IsCompleted); +#pragma warning disable xUnit1031 // Do not use blocking task operations in test method + Assert.Equal(42, task.Result); +#pragma warning restore xUnit1031 // Do not use blocking task operations in test method + } + + [Fact] + public void AsAsyncFunction_WithEventHandlerDelegate_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction); + } + + [Fact] + public void AsAsyncFunction_WithActionDelegate_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction>); + } + + [Fact] + public void AsAsyncFunction_WithFuncWithInvalidReturnType_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction>); + } + + [Fact] + public void AsAsyncFunction_WithFuncWithTooManyParams_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction>); + } + + class TestJSRuntime : JSInProcessRuntime + { + public List InvokeCalls { get; set; } = []; + + public string? NextResultJson { get; set; } + + protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + protected override string? InvokeJS(in JSInvocationInfo invocationInfo) + { + InvokeCalls.Add(invocationInfo); + return NextResultJson; + } + + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + => throw new NotImplementedException("This test only covers sync calls"); + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) + { + InvokeCalls.Add(invocationInfo); + } + + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + => throw new NotImplementedException("This test only covers sync calls"); + } +} From bd2728c341ab8d2e81a4cad1b7715bc6f1269fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 24 Apr 2025 14:33:05 +0200 Subject: [PATCH 3/4] Extend support to 8 arguments --- .../src/Infrastructure/JSFunctionReference.cs | 16 ++++++++++++++++ .../test/JSObjectReferenceExtensionsTest.cs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs index b4ea2d0b9913..a77741ef1799 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs @@ -122,6 +122,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ var gd when gd == typeof(Func<,,,,>) => nameof(Invoke4), var gd when gd == typeof(Func<,,,,,>) => nameof(Invoke5), var gd when gd == typeof(Func<,,,,,,>) => nameof(Invoke6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(Invoke7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(Invoke8), _ => throw CreateInvalidTypeParameterException(delegateType) }; @@ -134,6 +136,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ var gd when gd == typeof(Func<,,,,>) => nameof(InvokeTask4), var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeTask5), var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeTask6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeTask7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeTask8), _ => throw CreateInvalidTypeParameterException(delegateType) }; @@ -146,6 +150,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoid4), var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoid5), var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoid6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeVoid7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeVoid8), _ => throw CreateInvalidTypeParameterException(delegateType) }; @@ -158,6 +164,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoidTask4), var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoidTask5), var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoidTask6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeVoidTask7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeVoidTask8), _ => throw CreateInvalidTypeParameterException(delegateType) }; @@ -169,6 +177,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ public ValueTask Invoke4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4]); public ValueTask Invoke5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]); public ValueTask Invoke6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]); + public ValueTask Invoke7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]); + public ValueTask Invoke8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]); // Variants returning ValueTask using InvokeVoidAsync public ValueTask InvokeVoid0() => _jsObjectReference.InvokeVoidAsync(string.Empty); @@ -178,6 +188,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ public ValueTask InvokeVoid4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]); public ValueTask InvokeVoid5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]); public ValueTask InvokeVoid6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]); + public ValueTask InvokeVoid7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]); + public ValueTask InvokeVoid8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]); // Variants returning Task using InvokeAsync public Task InvokeTask0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync(string.Empty, []).AsTask(); @@ -187,6 +199,8 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ public Task InvokeTask4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask(); public Task InvokeTask5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask(); public Task InvokeTask6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask(); + public Task InvokeTask7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]).AsTask(); + public Task InvokeTask8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]).AsTask(); // Variants returning Task using InvokeVoidAsync public Task InvokeVoidTask0() => _jsObjectReference.InvokeVoidAsync(string.Empty).AsTask(); @@ -196,4 +210,6 @@ private static InvalidOperationException CreateInvalidTypeParameterException(Typ public Task InvokeVoidTask4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask(); public Task InvokeVoidTask5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask(); public Task InvokeVoidTask6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask(); + public Task InvokeVoidTask7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]).AsTask(); + public Task InvokeVoidTask8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]).AsTask(); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs index b170a0ad3737..a879dc54b6c0 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs @@ -158,7 +158,7 @@ public void AsAsyncFunction_WithFuncWithTooManyParams_Throws() var jsObjectReference = new JSObjectReference(jsRuntime, 1); // Act/Assert - Assert.Throws(jsObjectReference.AsAsyncFunction>); + Assert.Throws(jsObjectReference.AsAsyncFunction>); } class TestJSRuntime : JSInProcessRuntime From 50fb6ee2c73c541e80d471b4da97f267c92a6914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 24 Apr 2025 17:12:25 +0200 Subject: [PATCH 4/4] Fix handling function references on JS side --- .../test/E2ETest/Tests/InteropTest.cs | 3 ++- .../BasicTestApp/InteropComponent.razor | 3 +++ .../src/src/Microsoft.JSInterop.ts | 17 +++++++++++++++-- .../src/Infrastructure/JSFunctionReference.cs | 5 +---- .../src/JSObjectReferenceExtensions.cs | 13 ++++++------- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index b98fb69d72f1..17cefe6d94e3 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -108,7 +108,8 @@ public void CanInvokeInteropMethods() ["invokeNewWithClassConstructorAsync.function"] = "6", ["invokeNewWithNonConstructorAsync"] = "Success", // Function reference tests - ["changeFunctionViaObjectReferenceAsync"] = "42" + ["changeFunctionViaObjectReferenceAsync"] = "42", + ["invokeDelegateFromAsAsyncFunction"] = "42" }; var expectedSyncValues = new Dictionary diff --git a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor index 92d845d42571..781dd84d1286 100644 --- a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor @@ -614,6 +614,9 @@ var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka"); await testClassRef.SetValueAsync("getTextLength", funcRef); ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync("getTextLength")).ToString(); + + var funcDelegate = funcRef.AsAsyncFunction>>(); + ReturnValues["invokeDelegateFromAsAsyncFunction"] = (await funcDelegate()).ToString(); } private void FunctionReferenceTests() diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index fdd7a4ed65ec..cc26b2dcd0a2 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -562,7 +562,17 @@ export module DotNet { const targetInstance = cachedJSObjectsById[targetInstanceId]; if (targetInstance) { - return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); + if (identifier) { + return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); + } else { + const wrappedObject = targetInstance.getWrappedObject(); + + if (wrappedObject instanceof Function) { + return wrappedObject; + } else { + throw new Error(`JS object instance with ID ${targetInstanceId} is not a function.`); + } + } } throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`); @@ -626,7 +636,10 @@ export module DotNet { if (!isReadableProperty(parent, memberName)) { throw new Error(`The property '${identifier}' is not defined or is not readable.`); } - return () => parent[memberName]; + + return parent[memberName] instanceof Function + ? () => parent[memberName].bind(parent) + : () => parent[memberName]; case JSCallType.SetValue: if (!isWritableProperty(parent, memberName)) { throw new Error(`The property '${identifier}' is not writable.`); diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs index a77741ef1799..d8d2b7b52a65 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs @@ -9,7 +9,7 @@ namespace Microsoft.JSInterop.Infrastructure; /// -/// TODO(OR): Document this. +/// Helper for constructing a Func delegate that wraps interop call to a JavaScript function referenced via . /// internal readonly struct JSFunctionReference { @@ -25,9 +25,6 @@ public JSFunctionReference(IJSObjectReference jsObjectReference) _jsObjectReference = jsObjectReference; } - /// - /// TODO(OR): Document this. - /// public static T CreateInvocationDelegate(IJSObjectReference jsObjectReference) where T : Delegate { Type delegateType = typeof(T); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index bd18e540af85..823b0e2f2743 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -171,12 +169,13 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen } /// - /// TODO(OR): Document this. + /// Converts a JavaScript function reference into a .NET delegate of the specified type. /// - /// - /// - /// - /// + /// The type of the delegate to create. Must be a Func with the result type , , , or . + /// The JavaScript object reference that represents the function to be invoked. + /// A Func delegate of type that can be used to invoke the JavaScript function. + /// Thrown when is null. + /// Thrown when is not a valid Func type. public static T AsAsyncFunction(this IJSObjectReference jsObjectReference) where T : Delegate { ArgumentNullException.ThrowIfNull(jsObjectReference);