diff --git a/src/Microsoft/JSInterop/0.Abstractions/src/IJSObjectReferenceFacade.cs b/src/Microsoft/JSInterop/0.Abstractions/src/IJSObjectReferenceFacade.cs index 049ceec1..1810583b 100644 --- a/src/Microsoft/JSInterop/0.Abstractions/src/IJSObjectReferenceFacade.cs +++ b/src/Microsoft/JSInterop/0.Abstractions/src/IJSObjectReferenceFacade.cs @@ -11,11 +11,11 @@ public interface IJSObjectReferenceFacade IJSObjectReference JSObjectReference { get; } ValueTask InvokeAsync(string identifier, [Accommodatable] object?[] arguments); - ValueTask InvokeAsync(string identifier, [Cancellable] CancellationToken cancellationToken, [Accommodatable] object?[] args); - ValueTask InvokeAsync(string identifier, [Cancellable] TimeSpan timeout, [Accommodatable] object?[] args); + ValueTask InvokeAsync(string identifier, [Cancellable] CancellationToken cancellationToken, [Accommodatable] object?[] arguments); + ValueTask InvokeAsync(string identifier, [Cancellable] TimeSpan timeout, [Accommodatable] object?[] arguments); - ValueTask InvokeVoidAsync(string identifier, [Accommodatable] object?[] args); - ValueTask InvokeVoidAsync(string identifier, [Cancellable] CancellationToken cancellationToken, [Accommodatable] object?[] args); - ValueTask InvokeVoidAsync(string identifier, [Cancellable] TimeSpan timeout, [Accommodatable] object?[] args); + ValueTask InvokeVoidAsync(string identifier, [Accommodatable] object?[] arguments); + ValueTask InvokeVoidAsync(string identifier, [Cancellable] CancellationToken cancellationToken, [Accommodatable] object?[] arguments); + ValueTask InvokeVoidAsync(string identifier, [Cancellable] TimeSpan timeout, [Accommodatable] object?[] arguments); } } diff --git a/src/Microsoft/JSInterop/0.Core/src/GetOrBuildJSFunctionalObjectDelegate.cs b/src/Microsoft/JSInterop/0.Core/src/GetOrBuildJSFunctionalObjectDelegate.cs index 2f915d35..77828201 100644 --- a/src/Microsoft/JSInterop/0.Core/src/GetOrBuildJSFunctionalObjectDelegate.cs +++ b/src/Microsoft/JSInterop/0.Core/src/GetOrBuildJSFunctionalObjectDelegate.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Teronis.Microsoft.JSInterop +namespace Teronis.Microsoft.JSInterop { public delegate IJSFunctionalObject GetOrBuildJSFunctionalObjectDelegate(); } diff --git a/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObject.cs b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObject.cs deleted file mode 100644 index cb13b35c..00000000 --- a/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObject.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.JSInterop; - -namespace Teronis.Microsoft.JSInterop.Dynamic -{ - public class JSDynamicObject : DynamicObject, IJSDynamicObject - { - public IJSObjectReference JSObjectReference => - jsObjectReference; - - private readonly IJSObjectReference jsObjectReference; - private readonly MethodDictionary methodDictionary; - private readonly IJSFunctionalObject jsFunctionalObject; - - internal JSDynamicObject(IJSObjectReference jsObjectReference, MethodDictionary methodDictionary, IJSFunctionalObject jsFunctionalObject) - { - this.jsObjectReference = jsObjectReference ?? throw new ArgumentNullException(nameof(jsObjectReference)); - this.methodDictionary = methodDictionary ?? throw new ArgumentNullException(nameof(methodDictionary)); - this.jsFunctionalObject = jsFunctionalObject; - } - - public override bool TryGetMember(GetMemberBinder binder, out object? result) => base.TryGetMember(binder, out result); - - private static string?[] GetPositionalArgumentNames(int numberOfArguments, IReadOnlyList argumentNames) - { - var numberOfArgumentNames = argumentNames.Count; - - var positionalArgumentNames = new string?[numberOfArguments]; - var currentArgumentPosition = 0; - - for (; currentArgumentPosition < numberOfArguments - numberOfArgumentNames; currentArgumentPosition++) { - positionalArgumentNames[currentArgumentPosition] = null; - } - - foreach (var argumentName in argumentNames) { - positionalArgumentNames[currentArgumentPosition] = argumentName; - currentArgumentPosition++; - } - - return positionalArgumentNames; - } - - public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? arguments, out object? result) - { - arguments ??= new object[0]; - IReadOnlyList? genericParameterTypes; - - if (DynamicObjectUtils.TryGetInvokeMemberTypeArguments(binder, out var genericParameterTypeList)) { - genericParameterTypes = genericParameterTypeList.ToList(); - } else { - genericParameterTypes = null; - } - - var positionalArgumentNames = GetPositionalArgumentNames(binder.CallInfo.ArgumentCount, binder.CallInfo.ArgumentNames); - - if (!methodDictionary.TryFindMethod(binder.Name, positionalArgumentNames, out var method)) { - goto exit; - } - - result = method.Invoke(jsFunctionalObject, jsObjectReference, genericParameterTypes, arguments); - return true; - - exit: - return base.TryInvokeMember(binder, arguments, out result); - } - - public ValueTask InvokeAsync(string identifier, object?[] arguments) => - jsFunctionalObject.InvokeAsync(jsObjectReference, identifier, arguments); - - public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, params object?[] args) => - jsFunctionalObject.InvokeAsync(jsObjectReference, identifier, cancellationToken, args); - - public ValueTask InvokeAsync(string identifier, TimeSpan timeout, params object?[] args) => - jsFunctionalObject.InvokeAsync(jsObjectReference, identifier, timeout, args); - - public ValueTask InvokeVoidAsync(string identifier, object?[] args) => - jsFunctionalObject.InvokeVoidAsync(jsObjectReference, identifier, args); - - public ValueTask InvokeVoidAsync(string identifier, CancellationToken cancellationToken, object?[] args) => - jsFunctionalObject.InvokeVoidAsync(jsObjectReference, identifier, cancellationToken, args); - - public ValueTask InvokeVoidAsync(string identifier, TimeSpan timeout, object?[] args) => - jsFunctionalObject.InvokeVoidAsync(jsObjectReference, identifier, timeout, args); - - public ValueTask DisposeAsync() => - jsObjectReference.DisposeAsync(); - } -} diff --git a/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectActivator.cs b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectActivator.cs index 96b75d93..dc4cd00e 100644 --- a/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectActivator.cs +++ b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectActivator.cs @@ -1,6 +1,6 @@ using System; using System.Reflection; -using ImpromptuInterface; +using Castle.DynamicProxy; using Microsoft.JSInterop; namespace Teronis.Microsoft.JSInterop.Dynamic @@ -57,9 +57,18 @@ public T CreateInstance(IJSObjectReference jsObjectReference) where T : class, IJSDynamicObject { var mainInterfaceType = typeof(T); - var methods = CreateMethodDictionary(mainInterfaceType, typeof(IJSObjectReferenceFacade)); - var jsDynamicObject = new JSDynamicObject(jsObjectReference, methods, getOrBuildJSFunctionalObjectDelegate()); - return jsDynamicObject.ActLike(/*other interfaces*/); + var methodDictionary = CreateMethodDictionary(mainInterfaceType); // The idea is to forward all not lookup methods + var jsDynamicObjectProxy = new JSDynamicObjectProxy(jsObjectReference, getOrBuildJSFunctionalObjectDelegate()); + var proxyGenerator = new ProxyGenerator(); + + var jsDynamicObjectInterceptor = new JSDynamicObjectInterceptor( + jsDynamicObjectProxy, + methodDictionary, + getOrBuildJSFunctionalObjectDelegate()); + + return (T)proxyGenerator.CreateInterfaceProxyWithoutTarget( + mainInterfaceType, + jsDynamicObjectInterceptor); } } } diff --git a/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectInterceptor.cs b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectInterceptor.cs new file mode 100644 index 00000000..10cf4578 --- /dev/null +++ b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectInterceptor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Castle.DynamicProxy; +using Dynamitey; +using Microsoft.JSInterop; +using DynamiteyDynamic = Dynamitey.Dynamic; + +namespace Teronis.Microsoft.JSInterop.Dynamic +{ + public class JSDynamicObjectInterceptor : IInterceptor + { + private readonly JSDynamicObjectProxy jsDynamicObjectProxy; + private readonly MethodDictionary methodDictionary; + private readonly IJSFunctionalObject jsFunctionalObject; + + internal JSDynamicObjectInterceptor(JSDynamicObjectProxy jsDynamicObjectProxy, MethodDictionary methodDictionary, IJSFunctionalObject jsFunctionalObject) + { + this.jsDynamicObjectProxy = jsDynamicObjectProxy ?? throw new ArgumentNullException(nameof(jsDynamicObjectProxy)); + this.methodDictionary = methodDictionary ?? throw new ArgumentNullException(nameof(methodDictionary)); + this.jsFunctionalObject = jsFunctionalObject; + } + + private static string?[] GetPositionalArgumentNames(int numberOfArguments, IReadOnlyList argumentNames) + { + var numberOfArgumentNames = argumentNames.Count; + + var positionalArgumentNames = new string?[numberOfArguments]; + var currentArgumentPosition = 0; + + for (; currentArgumentPosition < numberOfArguments - numberOfArgumentNames; currentArgumentPosition++) { + positionalArgumentNames[currentArgumentPosition] = null; + } + + foreach (var argumentName in argumentNames) { + positionalArgumentNames[currentArgumentPosition] = argumentName; + currentArgumentPosition++; + } + + return positionalArgumentNames; + } + + public void Intercept(IInvocation invocation) { + var name = invocation.Method.Name; + var arguments = invocation.Arguments ?? new object[0]; + var genericParameterTypes = invocation.GenericArguments; + var positionalArgumentNames = invocation.Method.GetParameters().Select(x => x.Name); + + if (methodDictionary.TryFindMethod(invocation.Method.Name, positionalArgumentNames, out var method)) { + invocation.ReturnValue = method.Invoke(jsFunctionalObject, jsDynamicObjectProxy.JSObjectReference, genericParameterTypes, arguments); + return; // We have our return value set. + } + + invocation.ReturnValue = DynamiteyDynamic.InvokeMember(jsDynamicObjectProxy, InvokeMemberName.Create(name, genericParameterTypes), arguments); + } + } +} diff --git a/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectProxy.cs b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectProxy.cs new file mode 100644 index 00000000..1f26fbdc --- /dev/null +++ b/src/Microsoft/JSInterop/Dynamic/0/src/JSDynamicObjectProxy.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Teronis.Microsoft.JSInterop.Dynamic +{ + public class JSDynamicObjectProxy : IJSDynamicObject + { + public IJSObjectReference JSObjectReference => + jsObjectReference; + + private readonly IJSObjectReference jsObjectReference; + private readonly IJSFunctionalObject jsFunctionalObject; + + internal JSDynamicObjectProxy(IJSObjectReference jsObjectReference, IJSFunctionalObject jsFunctionalObject) + { + this.jsObjectReference = jsObjectReference ?? throw new ArgumentNullException(nameof(jsObjectReference)); + this.jsFunctionalObject = jsFunctionalObject; + } + + public ValueTask InvokeAsync(string identifier, object?[] arguments) => + jsFunctionalObject.InvokeAsync(jsObjectReference, identifier, arguments); + + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, params object?[] args) => + jsFunctionalObject.InvokeAsync(jsObjectReference, identifier, cancellationToken, args); + + public ValueTask InvokeAsync(string identifier, TimeSpan timeout, params object?[] args) => + jsFunctionalObject.InvokeAsync(jsObjectReference, identifier, timeout, args); + + public ValueTask InvokeVoidAsync(string identifier, object?[] args) => + jsFunctionalObject.InvokeVoidAsync(jsObjectReference, identifier, args); + + public ValueTask InvokeVoidAsync(string identifier, CancellationToken cancellationToken, object?[] args) => + jsFunctionalObject.InvokeVoidAsync(jsObjectReference, identifier, cancellationToken, args); + + public ValueTask InvokeVoidAsync(string identifier, TimeSpan timeout, object?[] args) => + jsFunctionalObject.InvokeVoidAsync(jsObjectReference, identifier, timeout, args); + + public ValueTask DisposeAsync() => + jsObjectReference.DisposeAsync(); + } +} diff --git a/src/Microsoft/JSInterop/Dynamic/0/src/Teronis.Microsoft.JSInterop.Dynamic.csproj b/src/Microsoft/JSInterop/Dynamic/0/src/Teronis.Microsoft.JSInterop.Dynamic.csproj index 08ec6a25..db5bd069 100644 --- a/src/Microsoft/JSInterop/Dynamic/0/src/Teronis.Microsoft.JSInterop.Dynamic.csproj +++ b/src/Microsoft/JSInterop/Dynamic/0/src/Teronis.Microsoft.JSInterop.Dynamic.csproj @@ -6,7 +6,8 @@ - + + diff --git a/src/Microsoft/JSInterop/Dynamic/0/test/0/JSDynamicObjectTests.cs b/src/Microsoft/JSInterop/Dynamic/0/test/0/JSDynamicObjectTests.cs index cf49f0ca..d00d6750 100644 --- a/src/Microsoft/JSInterop/Dynamic/0/test/0/JSDynamicObjectTests.cs +++ b/src/Microsoft/JSInterop/Dynamic/0/test/0/JSDynamicObjectTests.cs @@ -17,7 +17,7 @@ public JSDynamicObjectTests() => JSDynamicObjectActivator = new JSDynamicObjectActivator(); [Fact] - public async Task Should_dispose() + public async Task Proxy_Dispose() { // Arrange var jsObjectReference = new IdentifierPromisingObjectReference(); @@ -31,23 +31,23 @@ public async Task Should_dispose() } [Fact] - public async Task Should_expect_equal_input_output() + public async Task Proxy_Expect_equal_input_output() { // Arrange var jsObjectReference = new JSArgumentsPromisingObjectReference(); var emptyDynamicObject = JSDynamicObjectActivator.CreateInstance(jsObjectReference); // Act - var expectedContent = nameof(Should_expect_equal_input_output); - // The extension methods get precedence over dynamic object method calls. - var resultedArguments = await emptyDynamicObject.InvokeAsync(expectedContent); + var expectedContent = nameof(Proxy_Expect_equal_input_output); + // The extension methods get precedence over target method calls. + var resultedArguments = await emptyDynamicObject.InvokeAsync(identifier: string.Empty, expectedContent); // Assert Assert.Equal(new object[] { expectedContent }, resultedArguments); } [Fact] - public void Should_throw_not_of_type_value_task_exception() + public void Activation_Throw_not_of_type_value_task_exception() { // Arrange var jsObjectReference = new JSEmptyObjectReference(); @@ -58,7 +58,7 @@ public void Should_throw_not_of_type_value_task_exception() } [Fact] - public async Task Should_expect_identifier_equal_invoked_method() + public async Task Dynamic_Expect_identifier_equal_invoked_method() { // Arrange var jsObjectReference = new IdentifierPromisingObjectReference(); @@ -73,7 +73,7 @@ public async Task Should_expect_identifier_equal_invoked_method() } [Fact] - public async Task Should_throw_token_cancellation() + public async Task Proxy_Throw_token_cancellation() { // Arrange var jsObjectReference = new CancellableObjectReference(); @@ -86,8 +86,7 @@ public async Task Should_throw_token_cancellation() // Assert await Assert.ThrowsAsync(async () => - //await jsDynamicObject.InvokeAsync(nameof(Should_expect_cancellation_through_cancellation_token), cancellationToken, args: null)); - await jsDynamicObject.InvokeAsync(nameof(Should_throw_token_cancellation), cancellationToken)); + await jsDynamicObject.InvokeAsync(nameof(Proxy_Throw_token_cancellation), cancellationToken)); } public async Task AssertCancellableObjectIsCancelledAsync( @@ -114,7 +113,7 @@ await getCallback(jsDynamicObject)( } [Fact] - public async Task Should_throw_token_cancellation_via_annotation() + public async Task Dynamic_Throw_token_cancellation_via_annotation() { // Arrange using var cancellationTokenSource = new CancellationTokenSource(); @@ -126,7 +125,7 @@ public async Task Should_throw_token_cancellation_via_annotation() } [Fact] - public async Task Should_throw_timeout_cancellation_via_annotation() + public async Task Dynamic_Throw_timeout_cancellation_via_annotation() { // Arrange var timeout = TimeSpan.Zero; @@ -136,7 +135,7 @@ public async Task Should_throw_timeout_cancellation_via_annotation() } [Fact] - public async Task Should_expect_accommodated_arguments() + public async Task Dynamic_Expect_accommodated_arguments() { // Arrange var jsObjectReference = new JSArgumentsPromisingObjectReference(); @@ -154,7 +153,7 @@ public async Task Should_expect_accommodated_arguments() } [Fact] - public void Should_throw_parameter_after_accommodatable_annotated_parameter_exception() + public void Activation_Throw_parameter_after_accommodatable_annotated_parameter_exception() { // Arrange var jsObjectReference = new JSEmptyObjectReference(); @@ -164,7 +163,7 @@ public void Should_throw_parameter_after_accommodatable_annotated_parameter_exce } [Fact] - public void Should_throw_too_many_cancellable_annotated_parameter_exception() + public void Activation_Throw_too_many_cancellable_annotated_parameter_exception() { // Arrange var jsObjectReference = new JSEmptyObjectReference(); @@ -174,7 +173,7 @@ public void Should_throw_too_many_cancellable_annotated_parameter_exception() } [Fact] - public async Task Should_get_identifier_via_annotation() + public async Task Dynamic_Get_identifier_via_annotation() { // Arrange var jsObjectReference = new IdentifierPromisingObjectReference();