-
Notifications
You must be signed in to change notification settings - Fork 849
Add support for JSInvokable methods on generic types #2342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
0b5ecf8
098be37
d9edf12
a55cdc6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,9 @@ public static class DotNetDispatcher | |
| 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 @@ private static object InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocati | |
| 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 @@ private static object InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocati | |
| 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 @@ private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, | |
| } | ||
| 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 @@ private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, | |
| 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)); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase.containsgenericparameters?view=netframework-4.8, methods containing generic parameters cannot be invoked:
This gives us the opportunity to treat these pretty much the same as non-public methods with |
||
| 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; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 @@ public void CannotInvokeUnsuitableMethods(string methodIdentifier) | |
| 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 @@ public void CanInvokeInstanceMethodWithParams() | |
| 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 @@ public static async Task<string> AsyncThrowingMethod() | |
| } | ||
| } | ||
|
|
||
| public class GenericType<TValue> | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These scenarios work ✔️ |
||
| { | ||
| [JSInvokable] public string EchoStringParameter(string input) => input; | ||
| [JSInvokable] public TValue EchoParameter(TValue input) => input; | ||
| } | ||
|
|
||
| public class GenericMethodClass | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These scenarios throw ❌ |
||
| { | ||
| [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>(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would exposing the type name be problematic in any way? For instance methods, reporting the object id isn't useful.