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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Components/test/E2ETest/Tests/InteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@
var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka");
await testClassRef.SetValueAsync("getTextLength", funcRef);
ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();

var funcDelegate = funcRef.AsAsyncFunction<Func<Task<int>>>();
ReturnValues["invokeDelegateFromAsAsyncFunction"] = (await funcDelegate()).ToString();
}

private void FunctionReferenceTests()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?).`);
Expand Down Expand Up @@ -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.`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// 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;

/// <summary>
/// Helper for constructing a Func delegate that wraps interop call to a JavaScript function referenced via <see cref="IJSObjectReference"/>.
/// </summary>
internal readonly struct JSFunctionReference
{
/// <summary>
/// Caches previously constructed MethodInfo instances for various delegate types.
/// </summary>
private static readonly ConcurrentDictionary<Type, MethodInfo> _methodInfoCache = new();

private readonly IJSObjectReference _jsObjectReference;

public JSFunctionReference(IJSObjectReference jsObjectReference)
{
_jsObjectReference = jsObjectReference;
}

public static T CreateInvocationDelegate<T>(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<T>(delegateType, jsObjectReference, methodName);
}
else if (returnTypeCandidate == typeof(Task))
{
var methodName = GetVoidTaskMethodName(delegateType);
return CreateVoidDelegate<T>(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<T>(delegateType, innerReturnType, jsObjectReference, methodName);
}

else if (returnTypeGenericTypeDefinition == typeof(Task<>))
{
var methodName = GetTaskMethodName(delegateType);
var innerReturnType = returnTypeCandidate.GenericTypeArguments[0];
return CreateDelegate<T>(delegateType, innerReturnType, jsObjectReference, methodName);
}
}

throw CreateInvalidTypeParameterException(delegateType);
}

private static T CreateDelegate<T>(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<T>(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<TResult> or ValueTask<TResult>.");
}

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<T> using InvokeAsync
public ValueTask<TResult> Invoke0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync<TResult>(string.Empty, []);
public ValueTask<TResult> Invoke1<T1, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1]);
public ValueTask<TResult> Invoke2<T1, T2, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2]);
public ValueTask<TResult> Invoke3<T1, T2, T3, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3]);
public ValueTask<TResult> Invoke4<T1, T2, T3, T4, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4]);
public ValueTask<TResult> Invoke5<T1, T2, T3, T4, T5, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4, arg5]);
public ValueTask<TResult> Invoke6<T1, T2, T3, T4, T5, T6, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]);
public ValueTask<TResult> Invoke7<T1, T2, T3, T4, T5, T6, T7, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]);
public ValueTask<TResult> Invoke8<T1, T2, T3, T4, T5, T6, T7, T8, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeAsync<TResult>(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>(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]);
public ValueTask InvokeVoid2<T1, T2>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]);
public ValueTask InvokeVoid3<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]);
public ValueTask InvokeVoid4<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]);
public ValueTask InvokeVoid5<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]);
public ValueTask InvokeVoid6<T1, T2, T3, T4, T5, T6>(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, T2, T3, T4, T5, T6, T7>(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, T2, T3, T4, T5, T6, T7, T8>(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<T> using InvokeAsync
public Task<TResult> InvokeTask0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync<TResult>(string.Empty, []).AsTask();
public Task<TResult> InvokeTask1<T1, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1]).AsTask();
public Task<TResult> InvokeTask2<T1, T2, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2]).AsTask();
public Task<TResult> InvokeTask3<T1, T2, T3, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3]).AsTask();
public Task<TResult> InvokeTask4<T1, T2, T3, T4, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4]).AsTask();
public Task<TResult> InvokeTask5<T1, T2, T3, T4, T5, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask();
public Task<TResult> InvokeTask6<T1, T2, T3, T4, T5, T6, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask();
public Task<TResult> InvokeTask7<T1, T2, T3, T4, T5, T6, T7, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]).AsTask();
public Task<TResult> InvokeTask8<T1, T2, T3, T4, T5, T6, T7, T8, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeAsync<TResult>(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>(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]).AsTask();
public Task InvokeVoidTask2<T1, T2>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]).AsTask();
public Task InvokeVoidTask3<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]).AsTask();
public Task InvokeVoidTask4<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask();
public Task InvokeVoidTask5<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask();
public Task InvokeVoidTask6<T1, T2, T3, T4, T5, T6>(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, T2, T3, T4, T5, T6, T7>(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, T2, T3, T4, T5, T6, T7, T8>(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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,19 @@ public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReferen

return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args);
}

/// <summary>
/// Converts a JavaScript function reference into a .NET delegate of the specified type.
/// </summary>
/// <typeparam name="T">The type of the delegate to create. Must be a Func with the result type <see cref="Task"/>, <see cref="Task{R}"/>, <see cref="ValueTask"/>, or <see cref="ValueTask{R}"/>.</typeparam>
/// <param name="jsObjectReference">The JavaScript object reference that represents the function to be invoked.</param>
/// <returns>A Func delegate of type <typeparamref name="T"/> that can be used to invoke the JavaScript function.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="jsObjectReference"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when <typeparamref name="T"/> is not a valid Func type.</exception>
public static T AsAsyncFunction<T>(this IJSObjectReference jsObjectReference) where T : Delegate
{
ArgumentNullException.ThrowIfNull(jsObjectReference);

return JSFunctionReference.CreateInvocationDelegate<T>(jsObjectReference);
}
}
Loading
Loading