From d3d91da6f12de884f7d7e2d1005599b00642e5f5 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/7] 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 5b2ac84e34b1a632a8f0802164c00c7f79dc183b 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/7] 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 7dbebea17309d4514277083811a2cfbeecc19769 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/7] 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 c4a743da955d5be5f0bcbb5d3b964b936387ade9 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/7] 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); From 384285afe3843b7c323301611ff60eed275e0275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 28 Apr 2025 14:46:52 +0200 Subject: [PATCH 5/7] Add AsFunction extension methods to IJSObjectReference --- .../src/Infrastructure/JSFunctionReference.cs | 212 ------------------ .../src/JSObjectReferenceExtensions.cs | 74 +++++- .../src/PublicAPI.Unshipped.txt | 6 +- 3 files changed, 73 insertions(+), 219 deletions(-) delete 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 deleted file mode 100644 index d8d2b7b52a65..000000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs +++ /dev/null @@ -1,212 +0,0 @@ -// 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; - -/// -/// Helper for constructing a Func delegate that wraps interop call to a JavaScript function referenced via . -/// -internal readonly struct JSFunctionReference -{ - /// - /// Caches previously constructed MethodInfo instances for various delegate types. - /// - private static readonly ConcurrentDictionary _methodInfoCache = new(); - - private readonly IJSObjectReference _jsObjectReference; - - public JSFunctionReference(IJSObjectReference jsObjectReference) - { - _jsObjectReference = jsObjectReference; - } - - 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 CreateInvalidTypeParameterException(delegateType); - } - - var returnTypeCandidate = delegateType.GenericTypeArguments[^1]; - - if (returnTypeCandidate == typeof(ValueTask)) - { - var methodName = GetVoidMethodName(delegateType); - return CreateVoidDelegate(delegateType, jsObjectReference, methodName); - } - else if (returnTypeCandidate == typeof(Task)) - { - var methodName = GetVoidTaskMethodName(delegateType); - return CreateVoidDelegate(delegateType, jsObjectReference, methodName); - } - else if (returnTypeCandidate.IsGenericType) - { - var returnTypeGenericTypeDefinition = returnTypeCandidate.GetGenericTypeDefinition(); - - if (returnTypeGenericTypeDefinition == typeof(ValueTask<>)) - { - var methodName = GetMethodName(delegateType); - var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; - return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); - } - - else if (returnTypeGenericTypeDefinition == typeof(Task<>)) - { - var methodName = GetTaskMethodName(delegateType); - var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; - return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); - } - } - - throw CreateInvalidTypeParameterException(delegateType); - } - - private static T CreateDelegate(Type delegateType, Type returnType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate - { - 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); - - 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 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); - - var wrapper = new JSFunctionReference(jsObjectReference); - return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); - } - - 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), - 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), - var gd when gd == typeof(Func<,,,,,,,>) => nameof(Invoke7), - var gd when gd == typeof(Func<,,,,,,,,>) => nameof(Invoke8), - _ => throw CreateInvalidTypeParameterException(delegateType) - }; - - 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), - 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), - var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeTask7), - var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeTask8), - _ => throw CreateInvalidTypeParameterException(delegateType) - }; - - 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), - 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), - var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeVoid7), - var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeVoid8), - _ => throw CreateInvalidTypeParameterException(delegateType) - }; - - 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), - 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), - var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeVoidTask7), - var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeVoidTask8), - _ => throw CreateInvalidTypeParameterException(delegateType) - }; - - // 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]); - 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); - 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]); - 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(); - 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(); - 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(); - 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(); - 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/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index 823b0e2f2743..25e3e8713798 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using Microsoft.JSInterop.Implementation; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -169,17 +171,77 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen } /// - /// Converts a JavaScript function reference into a .NET delegate of the specified type. + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. /// - /// 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. + /// A delegate 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 + public static Func AsFunction(this IJSObjectReference jsObjectReference) { ArgumentNullException.ThrowIfNull(jsObjectReference); - return JSFunctionReference.CreateInvocationDelegate(jsObjectReference); + return async () => await jsObjectReference.InvokeVoidAsync(string.Empty); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction<[DynamicallyAccessedMembers(JsonSerialized)] TResult>(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async () => await jsObjectReference.InvokeAsync(string.Empty); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1) => await jsObjectReference.InvokeAsync(string.Empty, [arg1]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2) => await jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable type of the third argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func> AsFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2, T3 arg3) => await jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3]); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index cab579e8a24e..f8b66d38b2e4 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -56,7 +56,11 @@ 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.AsAsyncFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! 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 ba6ec8b5f9c6de86c68c461b5af0df5b38241d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 28 Apr 2025 15:10:06 +0200 Subject: [PATCH 6/7] Update test --- .../test/testassets/BasicTestApp/InteropComponent.razor | 2 +- .../Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor index 781dd84d1286..8c5d34a18c01 100644 --- a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor @@ -615,7 +615,7 @@ await testClassRef.SetValueAsync("getTextLength", funcRef); ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync("getTextLength")).ToString(); - var funcDelegate = funcRef.AsAsyncFunction>>(); + var funcDelegate = funcRef.AsFunction(); ReturnValues["invokeDelegateFromAsAsyncFunction"] = (await funcDelegate()).ToString(); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index 25e3e8713798..d17d05e8f6ce 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; -using Microsoft.JSInterop.Implementation; using Microsoft.JSInterop.Infrastructure; using static Microsoft.AspNetCore.Internal.LinkerFlags; From ea51c5f56200cd5669be5eb8fa6ca6193b91c2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 28 Apr 2025 15:29:47 +0200 Subject: [PATCH 7/7] Add void variants, update tests --- .../src/JSObjectReferenceExtensions.cs | 50 +++++- .../src/PublicAPI.Unshipped.txt | 5 +- .../test/JSObjectReferenceExtensionsTest.cs | 149 +----------------- 3 files changed, 57 insertions(+), 147 deletions(-) diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index d17d05e8f6ce..23a4dd5a7a69 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -174,13 +174,61 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen /// The JavaScript object reference that represents the function to be invoked. /// A delegate that can be used to invoke the JavaScript function. /// Thrown when is null. - public static Func AsFunction(this IJSObjectReference jsObjectReference) + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) { ArgumentNullException.ThrowIfNull(jsObjectReference); return async () => await jsObjectReference.InvokeVoidAsync(string.Empty); } + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]); + } + + /// + /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. + /// + /// The JSON-serializable type of the first argument. + /// The JSON-serializable type of the second argument. + /// The JSON-serializable type of the third argument. + /// The JSON-serializable return type. + /// The JavaScript object reference that represents the function to be invoked. + /// A delegate that can be used to invoke the JavaScript function. + /// Thrown when is null. + public static Func AsVoidFunction(this IJSObjectReference jsObjectReference) + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return async (T1 arg1, T2 arg2, T3 arg3) => await jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]); + } + /// /// Wraps the interop invocation of the JavaScript function referenced by as a .NET delegate. /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index f8b66d38b2e4..7a50ffbafe80 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -56,11 +56,14 @@ 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) -> System.Func! static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func>! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsVoidFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> System.Func! 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 index a879dc54b6c0..44e22e0ae8e3 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs @@ -14,73 +14,17 @@ 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() + public void AsFunction_ReturnsFunc_ThatInvokesInterop() { // Arrange - var jsRuntime = new TestJSRuntime(); + var jsRuntime = new RecordingTestJSRuntime(); var jsObjectReference = new JSObjectReference(jsRuntime, 1); var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42)); var reader = new Utf8JsonReader(bytes); // Act - var func = jsObjectReference.AsAsyncFunction>>(); + var func = jsObjectReference.AsFunction(); ValueTask task = func(1); jsRuntime.EndInvokeJS( @@ -95,98 +39,13 @@ public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc_ThatInvokesInterop() #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 + class RecordingTestJSRuntime : TestJSRuntime { 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"); } }