Merge branch 'release/3.1' => 'master' (dotnet/extensions#2368)
\n\nCommit migrated from 7433e7f07c
This commit is contained in:
commit
3ffb2ca7d2
|
|
@ -24,6 +24,9 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
||||||
= new ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
|
= 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>
|
/// <summary>
|
||||||
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -129,9 +132,12 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
var methodIdentifier = callInfo.MethodIdentifier;
|
var methodIdentifier = callInfo.MethodIdentifier;
|
||||||
|
|
||||||
AssemblyKey assemblyKey;
|
AssemblyKey assemblyKey;
|
||||||
|
MethodInfo methodInfo;
|
||||||
|
Type[] parameterTypes;
|
||||||
if (objectReference is null)
|
if (objectReference is null)
|
||||||
{
|
{
|
||||||
assemblyKey = new AssemblyKey(assemblyName);
|
assemblyKey = new AssemblyKey(assemblyName);
|
||||||
|
(methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -147,11 +153,9 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
return default;
|
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);
|
var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -301,7 +305,47 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
}
|
}
|
||||||
else
|
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 result = new Dictionary<string, (MethodInfo, Type[])>(StringComparer.Ordinal);
|
||||||
var invokableMethods = GetRequiredLoadedAssembly(assemblyKey)
|
var invokableMethods = GetRequiredLoadedAssembly(assemblyKey)
|
||||||
.GetExportedTypes()
|
.GetExportedTypes()
|
||||||
.SelectMany(type => type.GetMethods(
|
.SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||||
BindingFlags.Public |
|
.Where(method => !method.ContainsGenericParameters && method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
|
||||||
BindingFlags.DeclaredOnly |
|
|
||||||
BindingFlags.Instance |
|
|
||||||
BindingFlags.Static))
|
|
||||||
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
|
|
||||||
foreach (var method in invokableMethods)
|
foreach (var method in invokableMethods)
|
||||||
{
|
{
|
||||||
var identifier = method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier ?? method.Name;
|
var identifier = method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier ?? method.Name;
|
||||||
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
|
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
|
||||||
|
|
||||||
try
|
if (result.ContainsKey(identifier))
|
||||||
{
|
{
|
||||||
result.Add(identifier, (method, parameterTypes));
|
throw new InvalidOperationException($"The assembly '{assemblyKey.AssemblyName}' contains more than one " +
|
||||||
}
|
$"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " +
|
||||||
catch (ArgumentException)
|
$"assembly must have different identifiers. You can pass a custom identifier as a parameter to " +
|
||||||
{
|
$"the [JSInvokable] attribute.");
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.Add(identifier, (method, parameterTypes));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.ExceptionServices;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -70,7 +69,7 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, default, default), null);
|
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]
|
[Fact]
|
||||||
|
|
@ -355,6 +354,78 @@ namespace Microsoft.JSInterop.Infrastructure
|
||||||
Assert.Equal("MY STRING", resultDto.StringVal);
|
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]
|
[Fact]
|
||||||
public void CannotInvokeWithFewerNumberOfParameters()
|
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
|
public class TestJSRuntime : JSInProcessRuntime
|
||||||
{
|
{
|
||||||
private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
|
private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue