Add support for JSInvokable methods on generic types (dotnet/extensions#2342)
* Add support for JSInvokable methods on generic types
Prior to this change, DotNetDispatcher cached the MethodInfo on the
generic type definition. Using this would have required MethodInfo.MakeGenericMethod before the method was invoked.
We could separately cache the result of this to avoid the reflection cost per invocation.
Alternatively we could cache static and non-static MethodInfo instances separately which is what this change attempts to do.
The big difference in the outcome is that this requires instance (non-static) JSInvokable methods to be only unique named within
the type hierarchy as opposed to across all static and instance JSInvokable methods in an assembly.
Fixes https://github.com/aspnet/Extensions/issues/1360
Fixes https://github.com/aspnet/AspNetCore/issues/9061
\n\nCommit migrated from 659b604fb2
This commit is contained in:
parent
6371d93d18
commit
ef2dc5024f
|
|
@ -24,6 +24,9 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
||||
= new ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByType
|
||||
= new ConcurrentDictionary<Type, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
|
||||
|
||||
/// <summary>
|
||||
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
||||
/// </summary>
|
||||
|
|
@ -129,9 +132,12 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
var methodIdentifier = callInfo.MethodIdentifier;
|
||||
|
||||
AssemblyKey assemblyKey;
|
||||
MethodInfo methodInfo;
|
||||
Type[] parameterTypes;
|
||||
if (objectReference is null)
|
||||
{
|
||||
assemblyKey = new AssemblyKey(assemblyName);
|
||||
(methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -147,11 +153,9 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
return default;
|
||||
}
|
||||
|
||||
assemblyKey = new AssemblyKey(objectReference.Value.GetType().Assembly);
|
||||
(methodInfo, parameterTypes) = GetCachedMethodInfo(objectReference, methodIdentifier);
|
||||
}
|
||||
|
||||
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
|
||||
|
||||
var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes);
|
||||
|
||||
try
|
||||
|
|
@ -301,7 +305,47 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"The assembly '{assemblyKey.AssemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
|
||||
throw new ArgumentException($"The assembly '{assemblyKey.AssemblyName}' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
|
||||
}
|
||||
}
|
||||
|
||||
private static (MethodInfo methodInfo, Type[] parameterTypes) GetCachedMethodInfo(IDotNetObjectReference objectReference, string methodIdentifier)
|
||||
{
|
||||
var type = objectReference.Value.GetType();
|
||||
var assemblyMethods = _cachedMethodsByType.GetOrAdd(type, ScanTypeForCallableMethods);
|
||||
if (assemblyMethods.TryGetValue(methodIdentifier, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"The type '{type.Name}' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
|
||||
}
|
||||
|
||||
static Dictionary<string, (MethodInfo, Type[])> ScanTypeForCallableMethods(Type type)
|
||||
{
|
||||
var result = new Dictionary<string, (MethodInfo, Type[])>(StringComparer.Ordinal);
|
||||
var invokableMethods = type
|
||||
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(method => !method.ContainsGenericParameters && method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
|
||||
|
||||
foreach (var method in invokableMethods)
|
||||
{
|
||||
var identifier = method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier ?? method.Name;
|
||||
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
|
||||
|
||||
if (result.ContainsKey(identifier))
|
||||
{
|
||||
throw new InvalidOperationException($"The type {type.Name} contains more than one " +
|
||||
$"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " +
|
||||
$"type must have different identifiers. You can pass a custom identifier as a parameter to " +
|
||||
$"the [JSInvokable] attribute.");
|
||||
}
|
||||
|
||||
result.Add(identifier, (method, parameterTypes));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -312,35 +356,22 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
var result = new Dictionary<string, (MethodInfo, Type[])>(StringComparer.Ordinal);
|
||||
var invokableMethods = GetRequiredLoadedAssembly(assemblyKey)
|
||||
.GetExportedTypes()
|
||||
.SelectMany(type => type.GetMethods(
|
||||
BindingFlags.Public |
|
||||
BindingFlags.DeclaredOnly |
|
||||
BindingFlags.Instance |
|
||||
BindingFlags.Static))
|
||||
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
|
||||
.SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
.Where(method => !method.ContainsGenericParameters && method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
|
||||
foreach (var method in invokableMethods)
|
||||
{
|
||||
var identifier = method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier ?? method.Name;
|
||||
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
|
||||
|
||||
try
|
||||
if (result.ContainsKey(identifier))
|
||||
{
|
||||
result.Add(identifier, (method, parameterTypes));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
if (result.ContainsKey(identifier))
|
||||
{
|
||||
throw new InvalidOperationException($"The assembly '{assemblyKey.AssemblyName}' contains more than one " +
|
||||
$"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " +
|
||||
$"assembly must have different identifiers. You can pass a custom identifier as a parameter to " +
|
||||
$"the [JSInvokable] attribute.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
throw new InvalidOperationException($"The assembly '{assemblyKey.AssemblyName}' contains more than one " +
|
||||
$"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " +
|
||||
$"assembly must have different identifiers. You can pass a custom identifier as a parameter to " +
|
||||
$"the [JSInvokable] attribute.");
|
||||
}
|
||||
|
||||
result.Add(identifier, (method, parameterTypes));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -70,7 +69,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, default, default), null);
|
||||
});
|
||||
|
||||
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
|
||||
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public invokable method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -355,6 +354,78 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
Assert.Equal("MY STRING", resultDto.StringVal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanInvokeNonGenericInstanceMethodOnGenericType()
|
||||
{
|
||||
var jsRuntime = new TestJSRuntime();
|
||||
var targetInstance = new GenericType<int>();
|
||||
jsRuntime.Invoke<object>("_setup",
|
||||
DotNetObjectReference.Create(targetInstance));
|
||||
var argsJson = "[\"hello world\"]";
|
||||
|
||||
// Act
|
||||
var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<int>.EchoStringParameter), 1, default), argsJson);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("\"hello world\"", resultJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanInvokeMethodsThatAcceptGenericParametersOnGenericTypes()
|
||||
{
|
||||
var jsRuntime = new TestJSRuntime();
|
||||
var targetInstance = new GenericType<string>();
|
||||
jsRuntime.Invoke<object>("_setup",
|
||||
DotNetObjectReference.Create(targetInstance));
|
||||
var argsJson = "[\"hello world\"]";
|
||||
|
||||
// Act
|
||||
var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<string>.EchoParameter), 1, default), argsJson);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("\"hello world\"", resultJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotInvokeStaticOpenGenericMethods()
|
||||
{
|
||||
var methodIdentifier = "StaticGenericMethod";
|
||||
var jsRuntime = new TestJSRuntime();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<ArgumentException>(() => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, 0, default), "[7]"));
|
||||
Assert.Contains($"The assembly '{thisAssemblyName}' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotInvokeInstanceOpenGenericMethods()
|
||||
{
|
||||
var methodIdentifier = "InstanceGenericMethod";
|
||||
var targetInstance = new GenericType<int>();
|
||||
var jsRuntime = new TestJSRuntime();
|
||||
jsRuntime.Invoke<object>("_setup",
|
||||
DotNetObjectReference.Create(targetInstance));
|
||||
var argsJson = "[\"hello world\"]";
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<ArgumentException>(() => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, methodIdentifier, 1, default), argsJson));
|
||||
Assert.Contains($"The type 'GenericType`1' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotInvokeMethodsWithGenericParameters_IfTypesDoNotMatch()
|
||||
{
|
||||
var jsRuntime = new TestJSRuntime();
|
||||
var targetInstance = new GenericType<int>();
|
||||
jsRuntime.Invoke<object>("_setup",
|
||||
DotNetObjectReference.Create(targetInstance));
|
||||
var argsJson = "[\"hello world\"]";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<JsonException>(() =>
|
||||
DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<int>.EchoParameter), 1, default), argsJson));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotInvokeWithFewerNumberOfParameters()
|
||||
{
|
||||
|
|
@ -790,6 +861,18 @@ namespace Microsoft.JSInterop.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
public class GenericType<TValue>
|
||||
{
|
||||
[JSInvokable] public string EchoStringParameter(string input) => input;
|
||||
[JSInvokable] public TValue EchoParameter(TValue input) => input;
|
||||
}
|
||||
|
||||
public class GenericMethodClass
|
||||
{
|
||||
[JSInvokable("StaticGenericMethod")] public static string StaticGenericMethod<TValue>(TValue input) => input.ToString();
|
||||
[JSInvokable("InstanceGenericMethod")] public string GenericMethod<TValue>(TValue input) => input.ToString();
|
||||
}
|
||||
|
||||
public class TestJSRuntime : JSInProcessRuntime
|
||||
{
|
||||
private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
|
||||
|
|
|
|||
Loading…
Reference in New Issue