Skip to content

Commit 1066bc4

Browse files
committed
Add AsFunction extension method to IJSObjectReference
1 parent 0de0a76 commit 1066bc4

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Concurrent;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
7+
using static Microsoft.AspNetCore.Internal.LinkerFlags;
8+
9+
namespace Microsoft.JSInterop.Infrastructure;
10+
11+
/// <summary>
12+
/// TODO(OR): Document this.
13+
/// </summary>
14+
internal readonly struct JSFunctionReference
15+
{
16+
private static readonly ConcurrentDictionary<Type, MethodInfo> _methodInfoCache = new();
17+
18+
private readonly IJSObjectReference _jsObjectReference;
19+
20+
/// <summary>
21+
/// Caches previously constructed MethodInfo instances for various delegate types.
22+
/// </summary>
23+
public static ConcurrentDictionary<Type, MethodInfo> MethodInfoCache => _methodInfoCache;
24+
25+
public JSFunctionReference(IJSObjectReference jsObjectReference)
26+
{
27+
_jsObjectReference = jsObjectReference;
28+
}
29+
30+
/// <summary>
31+
/// TODO(OR): Document this.
32+
/// </summary>
33+
public static T CreateInvocationDelegate<T>(IJSObjectReference jsObjectReference) where T : Delegate
34+
{
35+
Type delegateType = typeof(T);
36+
37+
if (MethodInfoCache.TryGetValue(delegateType, out var wrapperMethod))
38+
{
39+
var wrapper = new JSFunctionReference(jsObjectReference);
40+
return (T)Delegate.CreateDelegate(delegateType, wrapper, wrapperMethod);
41+
}
42+
43+
if (!delegateType.IsGenericType)
44+
{
45+
throw new ArgumentException("The delegate type must be a Func.");
46+
}
47+
48+
var returnTypeCandidate = delegateType.GenericTypeArguments[^1];
49+
50+
if (returnTypeCandidate == typeof(ValueTask))
51+
{
52+
var methodName = GetVoidMethodName(delegateType.GetGenericTypeDefinition());
53+
return CreateVoidDelegate<T>(delegateType, jsObjectReference, methodName);
54+
}
55+
else if (returnTypeCandidate == typeof(Task))
56+
{
57+
var methodName = GetVoidTaskMethodName(delegateType.GetGenericTypeDefinition());
58+
return CreateVoidDelegate<T>(delegateType, jsObjectReference, methodName);
59+
}
60+
else
61+
{
62+
var returnTypeGenericTypeDefinition = returnTypeCandidate.GetGenericTypeDefinition();
63+
64+
if (returnTypeGenericTypeDefinition == typeof(ValueTask<>))
65+
{
66+
var methodName = GetMethodName(delegateType.GetGenericTypeDefinition());
67+
var innerReturnType = returnTypeCandidate.GenericTypeArguments[0];
68+
return CreateDelegate<T>(delegateType, innerReturnType, jsObjectReference, methodName);
69+
}
70+
71+
else if (returnTypeGenericTypeDefinition == typeof(Task<>))
72+
{
73+
var methodName = GetTaskMethodName(delegateType.GetGenericTypeDefinition());
74+
var innerReturnType = returnTypeCandidate.GenericTypeArguments[0];
75+
return CreateDelegate<T>(delegateType, innerReturnType, jsObjectReference, methodName);
76+
}
77+
else
78+
{
79+
throw new ArgumentException("The delegate return type must be Task<TResult> or ValueTask<TResult>.");
80+
}
81+
}
82+
}
83+
84+
private static T CreateDelegate<T>(Type delegateType, Type returnType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate
85+
{
86+
var wrapper = new JSFunctionReference(jsObjectReference);
87+
var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!;
88+
Type[] genericArguments = [.. delegateType.GenericTypeArguments[..^1], returnType];
89+
90+
#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.
91+
var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments);
92+
#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.
93+
94+
MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod);
95+
96+
return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod);
97+
}
98+
99+
private static T CreateVoidDelegate<T>(Type delegateType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate
100+
{
101+
var wrapper = new JSFunctionReference(jsObjectReference);
102+
var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!;
103+
Type[] genericArguments = delegateType.GenericTypeArguments[..^1];
104+
105+
#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.
106+
var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments);
107+
#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.
108+
109+
MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod);
110+
111+
return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod);
112+
}
113+
114+
private static string GetMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
115+
{
116+
var gd when gd == typeof(Func<>) => nameof(Invoke0),
117+
var gd when gd == typeof(Func<,>) => nameof(Invoke1),
118+
var gd when gd == typeof(Func<,,>) => nameof(Invoke2),
119+
var gd when gd == typeof(Func<,,,>) => nameof(Invoke3),
120+
var gd when gd == typeof(Func<,,,,>) => nameof(Invoke4),
121+
var gd when gd == typeof(Func<,,,,,>) => nameof(Invoke5),
122+
var gd when gd == typeof(Func<,,,,,,>) => nameof(Invoke6),
123+
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
124+
};
125+
126+
private static string GetTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
127+
{
128+
var gd when gd == typeof(Func<>) => nameof(InvokeTask0),
129+
var gd when gd == typeof(Func<,>) => nameof(InvokeTask1),
130+
var gd when gd == typeof(Func<,,>) => nameof(InvokeTask2),
131+
var gd when gd == typeof(Func<,,,>) => nameof(InvokeTask3),
132+
var gd when gd == typeof(Func<,,,,>) => nameof(InvokeTask4),
133+
var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeTask5),
134+
var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeTask6),
135+
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
136+
};
137+
138+
private static string GetVoidMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
139+
{
140+
var gd when gd == typeof(Func<>) => nameof(InvokeVoid0),
141+
var gd when gd == typeof(Func<,>) => nameof(InvokeVoid1),
142+
var gd when gd == typeof(Func<,,>) => nameof(InvokeVoid2),
143+
var gd when gd == typeof(Func<,,,>) => nameof(InvokeVoid3),
144+
var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoid4),
145+
var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoid5),
146+
var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoid6),
147+
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
148+
};
149+
150+
private static string GetVoidTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
151+
{
152+
var gd when gd == typeof(Func<>) => nameof(InvokeVoidTask0),
153+
var gd when gd == typeof(Func<,>) => nameof(InvokeVoidTask1),
154+
var gd when gd == typeof(Func<,,>) => nameof(InvokeVoidTask2),
155+
var gd when gd == typeof(Func<,,,>) => nameof(InvokeVoidTask3),
156+
var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoidTask4),
157+
var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoidTask5),
158+
var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoidTask6),
159+
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
160+
};
161+
162+
// Variants returning ValueTask<T> using InvokeAsync
163+
public ValueTask<TResult> Invoke0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync<TResult>(string.Empty, []);
164+
public ValueTask<TResult> Invoke1<T1, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1]);
165+
public ValueTask<TResult> Invoke2<T1, T2, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2]);
166+
public ValueTask<TResult> Invoke3<T1, T2, T3, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2, arg3]);
167+
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]);
168+
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]);
169+
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]);
170+
171+
// Variants returning ValueTask using InvokeVoidAsync
172+
public ValueTask InvokeVoid0() => _jsObjectReference.InvokeVoidAsync(string.Empty);
173+
public ValueTask InvokeVoid1<T1>(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]);
174+
public ValueTask InvokeVoid2<T1, T2>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]);
175+
public ValueTask InvokeVoid3<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]);
176+
public ValueTask InvokeVoid4<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]);
177+
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]);
178+
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]);
179+
180+
// Variants returning Task<T> using InvokeAsync
181+
public Task<TResult> InvokeTask0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync<TResult>(string.Empty, []).AsTask();
182+
public Task<TResult> InvokeTask1<T1, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1]).AsTask();
183+
public Task<TResult> InvokeTask2<T1, T2, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync<TResult>(string.Empty, [arg1, arg2]).AsTask();
184+
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();
185+
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();
186+
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();
187+
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();
188+
189+
// Variants returning Task using InvokeVoidAsync
190+
public Task InvokeVoidTask0() => _jsObjectReference.InvokeVoidAsync(string.Empty).AsTask();
191+
public Task InvokeVoidTask1<T1>(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]).AsTask();
192+
public Task InvokeVoidTask2<T1, T2>(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]).AsTask();
193+
public Task InvokeVoidTask3<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]).AsTask();
194+
public Task InvokeVoidTask4<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask();
195+
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();
196+
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();
197+
}

src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
57
using Microsoft.JSInterop.Infrastructure;
68
using static Microsoft.AspNetCore.Internal.LinkerFlags;
79

@@ -167,4 +169,18 @@ public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReferen
167169

168170
return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args);
169171
}
172+
173+
/// <summary>
174+
/// TODO(OR): Document this.
175+
/// </summary>
176+
/// <typeparam name="T"></typeparam>
177+
/// <param name="jsObjectReference"></param>
178+
/// <returns></returns>
179+
/// <exception cref="ArgumentException"></exception>
180+
public static T AsFunction<T>(this IJSObjectReference jsObjectReference) where T : Delegate
181+
{
182+
ArgumentNullException.ThrowIfNull(jsObjectReference);
183+
184+
return JSFunctionReference.CreateInvocationDelegate<T>(jsObjectReference);
185+
}
170186
}

src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args
5656
Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
5757
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask
5858
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
59+
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction<T>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T!
5960
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
6061
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
6162
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>

0 commit comments

Comments
 (0)