diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.Server.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.Server.ts index b0a917b464..65cfd6226c 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.Server.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.Server.ts @@ -32,8 +32,8 @@ function boot() { connection.start() .then(async () => { DotNet.attachDispatcher({ - beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, argsJson) => { - connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, argsJson); + beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => { + connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson); } }); diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts index 188a297a84..ee78df1c62 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts @@ -292,27 +292,34 @@ function getArrayDataPointer(array: System_Array): number { } function attachInteropInvoker() { - const dotNetDispatcherInvokeMethodHandle = findMethod('Microsoft.JSInterop', 'Microsoft.JSInterop', 'DotNetDispatcher', 'Invoke'); - const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Microsoft.JSInterop', 'Microsoft.JSInterop', 'DotNetDispatcher', 'BeginInvoke'); + const dotNetDispatcherInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'InvokeDotNet'); + const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet'); DotNet.attachDispatcher({ - beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, argsJson) => { + beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => { + // As a current limitation, we can only pass 4 args. Fortunately we only need one of + // 'assemblyName' or 'dotNetObjectId', so overload them in a single slot + const assemblyNameOrDotNetObjectId = dotNetObjectId + ? dotNetObjectId.toString() + : assemblyName; + monoPlatform.callMethod(dotNetDispatcherBeginInvokeMethodHandle, null, [ callId ? monoPlatform.toDotNetString(callId.toString()) : null, - monoPlatform.toDotNetString(assemblyName), + monoPlatform.toDotNetString(assemblyNameOrDotNetObjectId!), monoPlatform.toDotNetString(methodIdentifier), monoPlatform.toDotNetString(argsJson) ]); }, - invokeDotNetFromJS: (assemblyName, methodIdentifier, argsJson) => { + invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => { const resultJsonStringPtr = monoPlatform.callMethod(dotNetDispatcherInvokeMethodHandle, null, [ - monoPlatform.toDotNetString(assemblyName), + assemblyName ? monoPlatform.toDotNetString(assemblyName) : null, monoPlatform.toDotNetString(methodIdentifier), + dotNetObjectId ? monoPlatform.toDotNetString(dotNetObjectId.toString()) : null, monoPlatform.toDotNetString(argsJson) ]) as System_String; return resultJsonStringPtr - ? JSON.parse(monoPlatform.toJavaScriptString(resultJsonStringPtr)) + ? monoPlatform.toJavaScriptString(resultJsonStringPtr) : null; }, }); diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs index e3be563f31..17989ec62b 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs @@ -51,9 +51,9 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits CircuitHost = circuitHost; } - public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, string argsJson) + public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { - EnsureCircuitHost().BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, argsJson); + EnsureCircuitHost().BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); } private async void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e) diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitHost.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitHost.cs index 9947bc2461..66fbcb9dcb 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitHost.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitHost.cs @@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits _isInitialized = true; } - public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, string argsJson) + public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { AssertInitialized(); @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits { SetCurrentCircuitHost(this); - DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, argsJson); + DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); }); } catch (Exception ex) diff --git a/src/Microsoft.AspNetCore.Blazor/ElementRef.cs b/src/Microsoft.AspNetCore.Blazor/ElementRef.cs index 011d71e523..9c82e7dbbc 100644 --- a/src/Microsoft.AspNetCore.Blazor/ElementRef.cs +++ b/src/Microsoft.AspNetCore.Blazor/ElementRef.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor /// /// Represents a reference to a rendered element. /// - public readonly struct ElementRef : ICustomJsonSerializer + public readonly struct ElementRef : ICustomArgSerializer { static long _nextIdForWebAssemblyOnly = 1; @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Blazor internal static ElementRef CreateWithUniqueId() => new ElementRef(CreateUniqueId()); - object ICustomJsonSerializer.ToJsonPrimitive() + object ICustomArgSerializer.ToJsonPrimitive() { return new Dictionary { diff --git a/src/Microsoft.JSInterop/DotNetDispatcher.cs b/src/Microsoft.JSInterop/DotNetDispatcher.cs index ec722e715a..d445c9519f 100644 --- a/src/Microsoft.JSInterop/DotNetDispatcher.cs +++ b/src/Microsoft.JSInterop/DotNetDispatcher.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.JSInterop.Internal; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -23,17 +24,27 @@ namespace Microsoft.JSInterop /// /// The assembly containing the method to be invoked. /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. + /// For instance method calls, identifies the target object. /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static string Invoke(string assemblyName, string methodIdentifier, string argsJson) + public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether // the targeted method has [JSInvokable]. It is not itself subject to that restriction, // because there would be nobody to police that. This method *is* the police. - var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, argsJson); - return syncResult == null ? null : Json.Serialize(syncResult); + // DotNetDispatcher only works with JSRuntimeBase instances. + var jsRuntime = (JSRuntimeBase)JSRuntime.Current; + + var targetInstance = (object)null; + if (dotNetObjectId != default) + { + targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); + } + + var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy); } /// @@ -42,16 +53,26 @@ namespace Microsoft.JSInterop /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. /// The assembly containing the method to be invoked. /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. + /// For instance method calls, identifies the target object. /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, string argsJson) + public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether // the targeted method has [JSInvokable]. It is not itself subject to that restriction, // because there would be nobody to police that. This method *is* the police. - var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, argsJson); + // DotNetDispatcher only works with JSRuntimeBase instances. + // If the developer wants to use a totally custom IJSRuntime, then their JS-side + // code has to implement its own way of returning async results. + var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current; + + var targetInstance = dotNetObjectId == default + ? null + : jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); + + var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); // If there was no callId, the caller does not want to be notified about the result if (callId != null) @@ -61,11 +82,6 @@ namespace Microsoft.JSInterop var task = syncResult is Task syncResultTask ? syncResultTask : Task.FromResult(syncResult); task.ContinueWith(completedTask => { - // DotNetDispatcher only works with JSRuntimeBase instances. - // If the developer wants to use a totally custom IJSRuntime, then their JS-side - // code has to implement its own way of returning async results. - var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current; - try { var result = TaskGenericsUtil.GetTaskResult(completedTask); @@ -80,8 +96,18 @@ namespace Microsoft.JSInterop } } - private static object InvokeSynchronously(string assemblyName, string methodIdentifier, string argsJson) + private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) { + if (targetInstance != null) + { + if (assemblyName != null) + { + throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'."); + } + + assemblyName = targetInstance.GetType().Assembly.GetName().Name; + } + var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier); // There's no direct way to say we want to deserialize as an array with heterogenous @@ -101,16 +127,26 @@ namespace Microsoft.JSInterop } // Second, convert each supplied value to the type expected by the method - var serializerStrategy = SimpleJson.SimpleJson.CurrentJsonSerializerStrategy; + var runtime = (JSRuntimeBase)JSRuntime.Current; + var serializerStrategy = runtime.ArgSerializerStrategy; for (var i = 0; i < suppliedArgsLength; i++) { - suppliedArgs[i] = serializerStrategy.DeserializeObject( - suppliedArgs[i], parameterTypes[i]); + if (parameterTypes[i] == typeof(JSAsyncCallResult)) + { + // For JS async call results, we have to defer the deserialization until + // later when we know what type it's meant to be deserialized as + suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]); + } + else + { + suppliedArgs[i] = serializerStrategy.DeserializeObject( + suppliedArgs[i], parameterTypes[i]); + } } try { - return methodInfo.Invoke(null, suppliedArgs); + return methodInfo.Invoke(targetInstance, suppliedArgs); } catch (Exception ex) { @@ -124,10 +160,28 @@ namespace Microsoft.JSInterop /// /// The identifier for the function invocation. /// A flag to indicate whether the invocation succeeded. - /// If is true, specifies the invocation result. If is false, gives the corresponding to the invocation failure. + /// If is true, specifies the invocation result. If is false, gives the corresponding to the invocation failure. [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] - public static void EndInvoke(long asyncHandle, bool succeeded, object resultOrException) - => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, resultOrException); + public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) + => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException); + + /// + /// Releases the reference to the specified .NET object. This allows the .NET runtime + /// to garbage collect that object if there are no other references to it. + /// + /// To avoid leaking memory, the JavaScript side code must call this for every .NET + /// object it obtains a reference to. The exception is if that object is used for + /// the entire lifetime of a given user's session, in which case it is released + /// automatically when the JavaScript runtime is disposed. + /// + /// The identifier previously passed to JavaScript code. + [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))] + public static void ReleaseDotNetObject(long dotNetObjectId) + { + // DotNetDispatcher only works with JSRuntimeBase instances. + var jsRuntime = (JSRuntimeBase)JSRuntime.Current; + jsRuntime.ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectId); + } private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier) { @@ -156,14 +210,37 @@ namespace Microsoft.JSInterop { // TODO: Consider looking first for assembly-level attributes (i.e., if there are any, // only use those) to avoid scanning, especially for framework assemblies. - return GetRequiredLoadedAssembly(assemblyName) + var result = new Dictionary(); + var invokableMethods = GetRequiredLoadedAssembly(assemblyName) .GetExportedTypes() .SelectMany(type => type.GetMethods()) - .Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false)) - .ToDictionary( - method => method.GetCustomAttribute(false).Identifier, - method => (method, method.GetParameters().Select(p => p.ParameterType).ToArray()) - ); + .Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false)); + foreach (var method in invokableMethods) + { + var identifier = method.GetCustomAttribute(false).Identifier ?? method.Name; + var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray(); + + try + { + result.Add(identifier, (method, parameterTypes)); + } + catch (ArgumentException) + { + if (result.ContainsKey(identifier)) + { + throw new InvalidOperationException($"The assembly '{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; + } + } + } + + return result; } private static Assembly GetRequiredLoadedAssembly(string assemblyName) diff --git a/src/Microsoft.JSInterop/DotNetObjectRef.cs b/src/Microsoft.JSInterop/DotNetObjectRef.cs new file mode 100644 index 0000000000..aa62bee341 --- /dev/null +++ b/src/Microsoft.JSInterop/DotNetObjectRef.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; + +namespace Microsoft.JSInterop +{ + /// + /// Wraps a JS interop argument, indicating that the value should not be serialized as JSON + /// but instead should be passed as a reference. + /// + /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. + /// + public class DotNetObjectRef : IDisposable + { + /// + /// Gets the object instance represented by this wrapper. + /// + public object Value { get; } + + // We track an associated IJSRuntime purely so that this class can be IDisposable + // in the normal way. Developers are more likely to use objectRef.Dispose() than + // some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef). + private IJSRuntime _attachedToRuntime; + + /// + /// Constructs an instance of . + /// + /// The value being wrapped. + public DotNetObjectRef(object value) + { + Value = value; + } + + /// + /// Ensures the is associated with the specified . + /// Developers do not normally need to invoke this manually, since it is called automatically by + /// framework code. + /// + /// The . + public void EnsureAttachedToJsRuntime(IJSRuntime runtime) + { + // The reason we populate _attachedToRuntime here rather than in the constructor + // is to ensure developers can't accidentally try to reuse DotNetObjectRef across + // different IJSRuntime instances. This method gets called as part of serializing + // the DotNetObjectRef during an interop call. + + var existingRuntime = Interlocked.CompareExchange(ref _attachedToRuntime, runtime, null); + if (existingRuntime != null && existingRuntime != runtime) + { + throw new InvalidOperationException($"The {nameof(DotNetObjectRef)} is already associated with a different {nameof(IJSRuntime)}. Do not attempt to re-use {nameof(DotNetObjectRef)} instances with multiple {nameof(IJSRuntime)} instances."); + } + } + + /// + /// Stops tracking this object reference, allowing it to be garbage collected + /// (if there are no other references to it). Once the instance is disposed, it + /// can no longer be used in interop calls from JavaScript code. + /// + public void Dispose() + { + _attachedToRuntime?.UntrackObjectRef(this); + } + } +} diff --git a/src/Microsoft.JSInterop/Json/ICustomJsonSerializer.cs b/src/Microsoft.JSInterop/ICustomArgSerializer.cs similarity index 85% rename from src/Microsoft.JSInterop/Json/ICustomJsonSerializer.cs rename to src/Microsoft.JSInterop/ICustomArgSerializer.cs index ed680ffc2f..f4012af8e9 100644 --- a/src/Microsoft.JSInterop/Json/ICustomJsonSerializer.cs +++ b/src/Microsoft.JSInterop/ICustomArgSerializer.cs @@ -5,14 +5,14 @@ namespace Microsoft.JSInterop.Internal { // This is "soft" internal because we're trying to avoid expanding JsonUtil into a sophisticated // API. Developers who want that would be better served by using a different JSON package - // instead. Also the perf implications of the ICustomJsonSerializer approach aren't ideal + // instead. Also the perf implications of the ICustomArgSerializer approach aren't ideal // (it forces structs to be boxed, and returning a dictionary means lots more allocations // and boxing of any value-typed properties). /// /// Internal. Intended for framework use only. /// - public interface ICustomJsonSerializer + public interface ICustomArgSerializer { /// /// Internal. Intended for framework use only. diff --git a/src/Microsoft.JSInterop/IJSRuntime.cs b/src/Microsoft.JSInterop/IJSRuntime.cs index 4b47a96532..9b825cb33a 100644 --- a/src/Microsoft.JSInterop/IJSRuntime.cs +++ b/src/Microsoft.JSInterop/IJSRuntime.cs @@ -18,5 +18,15 @@ namespace Microsoft.JSInterop /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. Task InvokeAsync(string identifier, params object[] args); + + /// + /// Stops tracking the .NET object represented by the . + /// This allows it to be garbage collected (if nothing else holds a reference to it) + /// and means the JS-side code can no longer invoke methods on the instance or pass + /// it as an argument to subsequent calls. + /// + /// The reference to stop tracking. + /// This method is called automaticallly by . + void UntrackObjectRef(DotNetObjectRef dotNetObjectRef); } } diff --git a/src/Microsoft.JSInterop/InteropArgSerializerStrategy.cs b/src/Microsoft.JSInterop/InteropArgSerializerStrategy.cs new file mode 100644 index 0000000000..663c1cf85a --- /dev/null +++ b/src/Microsoft.JSInterop/InteropArgSerializerStrategy.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.JSInterop.Internal; +using SimpleJson; +using System; +using System.Collections.Generic; + +namespace Microsoft.JSInterop +{ + internal class InteropArgSerializerStrategy : PocoJsonSerializerStrategy + { + private readonly JSRuntimeBase _jsRuntime; + private const string _dotNetObjectPrefix = "__dotNetObject:"; + private object _storageLock = new object(); + private long _nextId = 1; // Start at 1, because 0 signals "no object" + private Dictionary _trackedRefsById = new Dictionary(); + private Dictionary _trackedIdsByRef = new Dictionary(); + + public InteropArgSerializerStrategy(JSRuntimeBase jsRuntime) + { + _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); + } + + protected override bool TrySerializeKnownTypes(object input, out object output) + { + switch (input) + { + case DotNetObjectRef marshalByRefValue: + EnsureDotNetObjectTracked(marshalByRefValue, out var id); + + // Special value format recognized by the code in Microsoft.JSInterop.js + // If we have to make it more clash-resistant, we can do + output = _dotNetObjectPrefix + id; + + return true; + + case ICustomArgSerializer customArgSerializer: + output = customArgSerializer.ToJsonPrimitive(); + return true; + + default: + return base.TrySerializeKnownTypes(input, out output); + } + } + + public override object DeserializeObject(object value, Type type) + { + if (value is string valueString) + { + if (valueString.StartsWith(_dotNetObjectPrefix)) + { + var dotNetObjectId = long.Parse(valueString.Substring(_dotNetObjectPrefix.Length)); + return FindDotNetObject(dotNetObjectId); + } + } + + return base.DeserializeObject(value, type); + } + + public object FindDotNetObject(long dotNetObjectId) + { + lock (_storageLock) + { + return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) + ? dotNetObjectRef.Value + : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the reference was already released.", nameof(dotNetObjectId)); + } + } + + /// + /// Stops tracking the specified .NET object reference. + /// This overload is typically invoked from JS code via JS interop. + /// + /// The ID of the . + public void ReleaseDotNetObject(long dotNetObjectId) + { + lock (_storageLock) + { + if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)) + { + _trackedRefsById.Remove(dotNetObjectId); + _trackedIdsByRef.Remove(dotNetObjectRef); + } + } + } + + /// + /// Stops tracking the specified .NET object reference. + /// This overload is typically invoked from .NET code by . + /// + /// The . + public void ReleaseDotNetObject(DotNetObjectRef dotNetObjectRef) + { + lock (_storageLock) + { + if (_trackedIdsByRef.TryGetValue(dotNetObjectRef, out var dotNetObjectId)) + { + _trackedRefsById.Remove(dotNetObjectId); + _trackedIdsByRef.Remove(dotNetObjectRef); + } + } + } + + private void EnsureDotNetObjectTracked(DotNetObjectRef dotNetObjectRef, out long dotNetObjectId) + { + dotNetObjectRef.EnsureAttachedToJsRuntime(_jsRuntime); + + lock (_storageLock) + { + // Assign an ID only if it doesn't already have one + if (!_trackedIdsByRef.TryGetValue(dotNetObjectRef, out dotNetObjectId)) + { + dotNetObjectId = _nextId++; + _trackedRefsById.Add(dotNetObjectId, dotNetObjectRef); + _trackedIdsByRef.Add(dotNetObjectRef, dotNetObjectId); + } + } + } + } +} diff --git a/src/Microsoft.JSInterop/JSAsyncCallResult.cs b/src/Microsoft.JSInterop/JSAsyncCallResult.cs new file mode 100644 index 0000000000..d46517eddc --- /dev/null +++ b/src/Microsoft.JSInterop/JSAsyncCallResult.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.JSInterop.Internal +{ + // This type takes care of a special case in handling the result of an async call from + // .NET to JS. The information about what type the result should be exists only on the + // corresponding TaskCompletionSource. We don't have that information at the time + // that we deserialize the incoming argsJson before calling DotNetDispatcher.EndInvoke. + // Declaring the EndInvoke parameter type as JSAsyncCallResult defers the deserialization + // until later when we have access to the TaskCompletionSource. + // + // There's no reason why developers would need anything similar to this in user code, + // because this is the mechanism by which we resolve the incoming argsJson to the correct + // user types before completing calls. + // + // It's marked as 'public' only because it has to be for use as an argument on a + // [JSInvokable] method. + + /// + /// Intended for framework use only. + /// + public class JSAsyncCallResult + { + internal object ResultOrException { get; } + + /// + /// Constructs an instance of . + /// + /// The result of the call. + internal JSAsyncCallResult(object resultOrException) + { + ResultOrException = resultOrException; + } + } +} diff --git a/src/Microsoft.JSInterop/JSInProcessRuntimeBase.cs b/src/Microsoft.JSInterop/JSInProcessRuntimeBase.cs index cbef1b2afe..49a47d0595 100644 --- a/src/Microsoft.JSInterop/JSInProcessRuntimeBase.cs +++ b/src/Microsoft.JSInterop/JSInProcessRuntimeBase.cs @@ -17,8 +17,8 @@ namespace Microsoft.JSInterop /// An instance of obtained by JSON-deserializing the return value. public T Invoke(string identifier, params object[] args) { - var resultJson = InvokeJS(identifier, Json.Serialize(args)); - return Json.Deserialize(resultJson); + var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy)); + return Json.Deserialize(resultJson, ArgSerializerStrategy); } /// diff --git a/src/Microsoft.JSInterop/JSInvokableAttribute.cs b/src/Microsoft.JSInterop/JSInvokableAttribute.cs index 6b70b67142..e037078cba 100644 --- a/src/Microsoft.JSInterop/JSInvokableAttribute.cs +++ b/src/Microsoft.JSInterop/JSInvokableAttribute.cs @@ -16,11 +16,23 @@ namespace Microsoft.JSInterop /// /// Gets the identifier for the method. The identifier must be unique within the scope /// of an assembly. + /// + /// If not set, the identifier is taken from the name of the method. In this case the + /// method name must be unique within the assembly. /// public string Identifier { get; } /// - /// Constructs an instance of . + /// Constructs an instance of without setting + /// an identifier for the method. + /// + public JSInvokableAttribute() + { + } + + /// + /// Constructs an instance of using the specified + /// identifier. /// /// An identifier for the method, which must be unique within the scope of the assembly. public JSInvokableAttribute(string identifier) diff --git a/src/Microsoft.JSInterop/JSRuntimeBase.cs b/src/Microsoft.JSInterop/JSRuntimeBase.cs index 812558c124..db4796521f 100644 --- a/src/Microsoft.JSInterop/JSRuntimeBase.cs +++ b/src/Microsoft.JSInterop/JSRuntimeBase.cs @@ -17,6 +17,20 @@ namespace Microsoft.JSInterop private readonly ConcurrentDictionary _pendingTasks = new ConcurrentDictionary(); + internal InteropArgSerializerStrategy ArgSerializerStrategy { get; } + + /// + /// Constructs an instance of . + /// + public JSRuntimeBase() + { + ArgSerializerStrategy = new InteropArgSerializerStrategy(this); + } + + /// + public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) + => ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef); + /// /// Invokes the specified JavaScript function asynchronously. /// @@ -36,7 +50,10 @@ namespace Microsoft.JSInterop try { - BeginInvokeJS(taskId, identifier, args?.Length > 0 ? Json.Serialize(args) : null); + var argsJson = args?.Length > 0 + ? Json.Serialize(args, ArgSerializerStrategy) + : null; + BeginInvokeJS(taskId, identifier, argsJson); return tcs.Task; } catch @@ -70,7 +87,7 @@ namespace Microsoft.JSInterop callId, success, resultOrException - })); + }, ArgSerializerStrategy)); } internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException) @@ -82,7 +99,11 @@ namespace Microsoft.JSInterop if (succeeded) { - TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException); + var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); + var resultValue = resultOrException is SimpleJson.JsonObject + ? ArgSerializerStrategy.DeserializeObject(resultOrException, resultType) + : resultOrException; + TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultValue); } else { diff --git a/src/Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop.ts b/src/Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop.ts index 714ce6b12b..b18e501c86 100644 --- a/src/Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop.ts +++ b/src/Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop.ts @@ -40,13 +40,7 @@ module DotNet { * @returns The result of the operation. */ export function invokeMethod(assemblyName: string, methodIdentifier: string, ...args: any[]): T { - const dispatcher = getRequiredDispatcher(); - if (dispatcher.invokeDotNetFromJS) { - const argsJson = JSON.stringify(args); - return dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, argsJson); - } else { - throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeAsync instead.'); - } + return invokePossibleInstanceMethod(assemblyName, methodIdentifier, null, args); } /** @@ -58,14 +52,29 @@ module DotNet { * @returns A promise representing the result of the operation. */ export function invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise { + return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args); + } + + function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T { + const dispatcher = getRequiredDispatcher(); + if (dispatcher.invokeDotNetFromJS) { + const argsJson = JSON.stringify(args, argReplacer); + const resultJson = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson); + return resultJson ? parseJsonWithRevivers(resultJson) : null; + } else { + throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeAsync instead.'); + } + } + + function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise { const asyncCallId = nextAsyncCallId++; const resultPromise = new Promise((resolve, reject) => { pendingAsyncCalls[asyncCallId] = { resolve, reject }; }); try { - const argsJson = JSON.stringify(args); - getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, argsJson); + const argsJson = JSON.stringify(args, argReplacer); + getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); } catch(ex) { // Synchronous failure completePendingCall(asyncCallId, false, ex); @@ -108,22 +117,24 @@ module DotNet { /** * Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method. * - * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods. * @param argsJson JSON representation of arguments to pass to the method. - * @returns The result of the invocation. + * @returns JSON representation of the result of the invocation. */ - invokeDotNetFromJS?(assemblyName: string, methodIdentifier: string, argsJson: string): any; + invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): string | null; /** * Invoked by the runtime to begin an asynchronous call to a .NET method. * * @param callId A value identifying the asynchronous operation. This value should be passed back in a later call from .NET to JS. - * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null to call static methods. * @param argsJson JSON representation of arguments to pass to the method. */ - beginInvokeDotNetFromJS(callId: number, assemblyName: string, methodIdentifier: string, argsJson: string): void; + beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void; } /** @@ -149,7 +160,7 @@ module DotNet { const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); return result === null || result === undefined ? null - : JSON.stringify(result); + : JSON.stringify(result, argReplacer); }, /** @@ -172,8 +183,8 @@ module DotNet { // On completion, dispatch result back to .NET // Not using "await" because it codegens a lot of boilerplate promise.then( - result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', JSON.stringify([asyncHandle, true, result])), - error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', JSON.stringify([asyncHandle, false, formatError(error)])) + result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)), + error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)])) ); } }, @@ -231,4 +242,46 @@ module DotNet { throw new Error(`The value '${resultIdentifier}' is not a function.`); } } + + class DotNetObject { + constructor(private _id: number) { + } + + public invokeMethod(methodIdentifier: string, ...args: any[]): T { + return invokePossibleInstanceMethod(null, methodIdentifier, this._id, args); + } + + public invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise { + return invokePossibleInstanceMethodAsync(null, methodIdentifier, this._id, args); + } + + public dispose() { + const promise = invokeMethodAsync( + 'Microsoft.JSInterop', + 'DotNetDispatcher.ReleaseDotNetObject', + this._id); + promise.catch(error => console.error(error)); + } + + public serializeAsArg() { + return `__dotNetObject:${this._id}`; + } + } + + const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/; + attachReviver(function reviveDotNetObject(key: any, value: any) { + if (typeof value === 'string') { + const match = value.match(dotNetObjectValueFormat); + if (match) { + return new DotNetObject(parseInt(match[1])); + } + } + + // Unrecognized - let another reviver handle it + return value; + }); + + function argReplacer(key: string, value: any) { + return value instanceof DotNetObject ? value.serializeAsArg() : value; + } } diff --git a/src/Microsoft.JSInterop/Json/Json.cs b/src/Microsoft.JSInterop/Json/Json.cs index 0c520be1b0..7275dfe427 100644 --- a/src/Microsoft.JSInterop/Json/Json.cs +++ b/src/Microsoft.JSInterop/Json/Json.cs @@ -21,6 +21,9 @@ namespace Microsoft.JSInterop public static string Serialize(object value) => SimpleJson.SimpleJson.SerializeObject(value); + internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy) + => SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy); + /// /// Deserializes the JSON string, creating an object of the specified generic type. /// @@ -29,5 +32,8 @@ namespace Microsoft.JSInterop /// An object of the specified type. public static T Deserialize(string json) => SimpleJson.SimpleJson.DeserializeObject(json); + + internal static T Deserialize(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy) + => SimpleJson.SimpleJson.DeserializeObject(json, serializerStrategy); } } diff --git a/src/Microsoft.JSInterop/Json/SimpleJson/SimpleJson.cs b/src/Microsoft.JSInterop/Json/SimpleJson/SimpleJson.cs index 9a30575449..18e3bf7d0f 100644 --- a/src/Microsoft.JSInterop/Json/SimpleJson/SimpleJson.cs +++ b/src/Microsoft.JSInterop/Json/SimpleJson/SimpleJson.cs @@ -67,7 +67,6 @@ using System.Reflection; using System.Runtime.Serialization; using System.Text; using Microsoft.JSInterop; -using Microsoft.JSInterop.Internal; using SimpleJson.Reflection; // ReSharper disable LoopCanBeConvertedToQuery @@ -1538,8 +1537,6 @@ namespace SimpleJson output = input.ToString(); else if (input is TimeSpan) output = ((TimeSpan)input).ToString("c"); - else if (input is ICustomJsonSerializer customJsonSerializer) - output = customJsonSerializer.ToJsonPrimitive(); else { Enum inputEnum = input as Enum; diff --git a/src/Microsoft.JSInterop/TaskGenericsUtil.cs b/src/Microsoft.JSInterop/TaskGenericsUtil.cs index c9cdbc7a3c..734e9863b8 100644 --- a/src/Microsoft.JSInterop/TaskGenericsUtil.cs +++ b/src/Microsoft.JSInterop/TaskGenericsUtil.cs @@ -22,26 +22,41 @@ namespace Microsoft.JSInterop public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception) => CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception); + public static Type GetTaskCompletionSourceResultType(object taskCompletionSource) + => CreateResultSetter(taskCompletionSource).ResultType; + public static object GetTaskResult(Task task) { - var getter = _cachedResultGetters.GetOrAdd(task.GetType(), taskType => + var getter = _cachedResultGetters.GetOrAdd(task.GetType(), taskInstanceType => { - if (taskType.IsGenericType) - { - var resultType = taskType.GetGenericArguments().Single(); - return (ITaskResultGetter)Activator.CreateInstance( + var resultType = GetTaskResultType(taskInstanceType); + return resultType == null + ? new VoidTaskResultGetter() + : (ITaskResultGetter)Activator.CreateInstance( typeof(TaskResultGetter<>).MakeGenericType(resultType)); - } - else - { - return new VoidTaskResultGetter(); - } }); return getter.GetResult(task); } + private static Type GetTaskResultType(Type taskType) + { + // It might be something derived from Task or Task, so we have to scan + // up the inheritance hierarchy to find the Task or Task + while (taskType != typeof(Task) && + (!taskType.IsGenericType || taskType.GetGenericTypeDefinition() != typeof(Task<>))) + { + taskType = taskType.BaseType + ?? throw new ArgumentException($"The type '{taskType.FullName}' is not inherited from '{typeof(Task).FullName}'."); + } + + return taskType.IsGenericType + ? taskType.GetGenericArguments().Single() + : null; + } + interface ITcsResultSetter { + Type ResultType { get; } void SetResult(object taskCompletionSource, object result); void SetException(object taskCompletionSource, Exception exception); } @@ -67,10 +82,18 @@ namespace Microsoft.JSInterop private class TcsResultSetter : ITcsResultSetter { + public Type ResultType => typeof(T); + public void SetResult(object tcs, object result) { var typedTcs = (TaskCompletionSource)tcs; - typedTcs.SetResult((T)result); + + // If necessary, attempt a cast + var typedResult = result is T resultT + ? resultT + : (T)Convert.ChangeType(result, typeof(T)); + + typedTcs.SetResult(typedResult); } public void SetException(object tcs, Exception exception) diff --git a/src/Mono.WebAssembly.Interop/MonoWebAssemblyJSRuntime.cs b/src/Mono.WebAssembly.Interop/MonoWebAssemblyJSRuntime.cs index 19a2a568ef..6708c0406d 100644 --- a/src/Mono.WebAssembly.Interop/MonoWebAssemblyJSRuntime.cs +++ b/src/Mono.WebAssembly.Interop/MonoWebAssemblyJSRuntime.cs @@ -27,6 +27,32 @@ namespace Mono.WebAssembly.Interop InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson); } + // Invoked via Mono's JS interop mechanism (invoke_method) + private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) + => DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + + // Invoked via Mono's JS interop mechanism (invoke_method) + private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) + { + // Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID + // We only need one for any given call. This helps to work around the limitation that we can + // only pass a maximum of 4 args in a call from JS to Mono WebAssembly. + string assemblyName; + long dotNetObjectId; + if (char.IsDigit(assemblyNameOrDotNetObjectId[0])) + { + dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId); + assemblyName = null; + } + else + { + dotNetObjectId = default; + assemblyName = assemblyNameOrDotNetObjectId; + } + + DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + } + #region Custom MonoWebAssemblyJSRuntime methods /// diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/InteropTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/InteropTest.cs index 17e8891c72..c9bc666b99 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/InteropTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/InteropTest.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; -using System.Threading; using BasicTestApp; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; @@ -33,45 +31,63 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests ["VoidParameterless"] = "[]", ["VoidWithOneParameter"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["VoidWithTwoParameters"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]", - ["VoidWithThreeParameters"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]", - ["VoidWithFourParameters"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]", - ["VoidWithFiveParameters"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]", - ["VoidWithSixParameters"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]", - ["VoidWithSevenParameters"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", - ["VoidWithEightParameters"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", + ["VoidWithThreeParameters"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]", + ["VoidWithFourParameters"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]", + ["VoidWithFiveParameters"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]", + ["VoidWithSixParameters"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]", + ["VoidWithSevenParameters"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", + ["VoidWithEightParameters"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", ["VoidParameterlessAsync"] = "[]", ["VoidWithOneParameterAsync"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["VoidWithTwoParametersAsync"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]", - ["VoidWithThreeParametersAsync"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]", - ["VoidWithFourParametersAsync"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]", - ["VoidWithFiveParametersAsync"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]", - ["VoidWithSixParametersAsync"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]", - ["VoidWithSevenParametersAsync"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", - ["VoidWithEightParametersAsync"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", + ["VoidWithThreeParametersAsync"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]", + ["VoidWithFourParametersAsync"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]", + ["VoidWithFiveParametersAsync"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]", + ["VoidWithSixParametersAsync"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]", + ["VoidWithSevenParametersAsync"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", + ["VoidWithEightParametersAsync"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", ["result1"] = @"[0.1,0.2]", ["result2"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["result3"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]", - ["result4"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]", - ["result5"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]", - ["result6"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]", - ["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]", - ["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", - ["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", + ["result4"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]", + ["result5"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]", + ["result6"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]", + ["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]", + ["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", + ["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", ["result1Async"] = @"[0.1,0.2]", ["result2Async"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["result3Async"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]", - ["result4Async"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]", - ["result5Async"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]", - ["result6Async"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]", - ["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]", - ["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", - ["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", + ["result4Async"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]", + ["result5Async"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]", + ["result6Async"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]", + ["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]", + ["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", + ["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]", ["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!", ["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!", ["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!", ["ExceptionFromSyncMethod"] = "Function threw an exception!", ["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!", ["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!", + ["resultReturnDotNetObjectByRefSync"] = "1000", + ["resultReturnDotNetObjectByRefAsync"] = "1001", + ["instanceMethodThisTypeName"] = @"""JavaScriptInterop""", + ["instanceMethodStringValueUpper"] = @"""MY STRING""", + ["instanceMethodIncomingByRef"] = "123", + ["instanceMethodOutgoingByRef"] = "1234", + ["instanceMethodThisTypeNameAsync"] = @"""JavaScriptInterop""", + ["instanceMethodStringValueUpperAsync"] = @"""MY STRING""", + ["instanceMethodIncomingByRefAsync"] = "123", + ["instanceMethodOutgoingByRefAsync"] = "1234", + ["stringValueUpperSync"] = "MY STRING", + ["testDtoNonSerializedValueSync"] = "99999", + ["testDtoSync"] = "Same", + ["stringValueUpperAsync"] = "MY STRING", + ["testDtoNonSerializedValueAsync"] = "99999", + ["testDtoAsync"] = "Same", + ["returnPrimitive"] = "123", + ["returnPrimitiveAsync"] = "123", }; var actualValues = new Dictionary(); diff --git a/test/Microsoft.JSInterop.Test/DotNetDispatcherTest.cs b/test/Microsoft.JSInterop.Test/DotNetDispatcherTest.cs index 0bd1bd570e..315f26d832 100644 --- a/test/Microsoft.JSInterop.Test/DotNetDispatcherTest.cs +++ b/test/Microsoft.JSInterop.Test/DotNetDispatcherTest.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Microsoft.JSInterop.Test @@ -11,13 +12,15 @@ namespace Microsoft.JSInterop.Test { private readonly static string thisAssemblyName = typeof(DotNetDispatcherTest).Assembly.GetName().Name; + private readonly TestJSRuntime jsRuntime + = new TestJSRuntime(); [Fact] public void CannotInvokeWithEmptyAssemblyName() { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(" ", "SomeMethod", "[]"); + DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -29,7 +32,7 @@ namespace Microsoft.JSInterop.Test { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke("SomeAssembly", " ", "[]"); + DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -42,75 +45,172 @@ namespace Microsoft.JSInterop.Test var assemblyName = "Some.Fake.Assembly"; var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(assemblyName, "SomeMethod", null); + DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null); }); Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); } - // Note: Currently it's also not possible to invoke instance or generic methods. + // Note: Currently it's also not possible to invoke generic methods. // That's not something determined by DotNetDispatcher, but rather by the fact that we - // don't pass any 'target' or close over the generics in the reflection code. + // don't close over the generics in the reflection code. // Not defining this behavior through unit tests because the default outcome is - // fine (an exception stating what info is missing), plus we're likely to add support - // for invoking instance methods in the near future. + // fine (an exception stating what info is missing). [Theory] [InlineData("MethodOnInternalType")] [InlineData("PrivateMethod")] [InlineData("ProtectedMethod")] - [InlineData("MethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it + [InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it + [InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it public void CannotInvokeUnsuitableMethods(string methodIdentifier) { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, null); + DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null); }); Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); } [Fact] - public void CanInvokeStaticVoidMethod() + public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime => { // Arrange/Act - SomePublicType.DidInvokeMyInvocableVoid = false; - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", null); + SomePublicType.DidInvokeMyInvocableStaticVoid = false; + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null); // Assert Assert.Null(resultJson); - Assert.True(SomePublicType.DidInvokeMyInvocableVoid); - } + Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid); + }); [Fact] - public void CanInvokeStaticNonVoidMethod() + public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime => { // Arrange/Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", null); + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); var result = Json.Deserialize(resultJson); // Assert Assert.Equal("Test", result.StringVal); Assert.Equal(123, result.IntVal); - } + }); [Fact] - public void CanInvokeStaticWithParams() + public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime => { - // Arrange - var argsJson = Json.Serialize(new object[] { - new TestDTO { StringVal = "Another string", IntVal = 456 }, - new[] { 100, 200 } - }); - - // Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", argsJson); + // Arrange/Act + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); var result = Json.Deserialize(resultJson); // Assert - Assert.Equal("ANOTHER STRING", result.StringVal); - Assert.Equal(756, result.IntVal); - } + Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); + Assert.Equal(456, result.IntVal); + }); + + [Fact] + public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => + { + // Arrange: Track a .NET object to use as an arg + var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; + jsRuntime.Invoke("unimportant", new DotNetObjectRef(arg3)); + + // Arrange: Remaining args + var argsJson = Json.Serialize(new object[] { + new TestDTO { StringVal = "Another string", IntVal = 456 }, + new[] { 100, 200 }, + "__dotNetObject:1" + }); + + // Act + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + var result = Json.Deserialize(resultJson); + + // Assert: First result value marshalled via JSON + var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO)); + Assert.Equal("ANOTHER STRING", resultDto1.StringVal); + Assert.Equal(756, resultDto1.IntVal); + + // Assert: Second result value marshalled by ref + var resultDto2Ref = (string)result[1]; + Assert.Equal("__dotNetObject:2", resultDto2Ref); + var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2); + Assert.Equal("MY STRING", resultDto2.StringVal); + Assert.Equal(1299, resultDto2.IntVal); + }); + + [Fact] + public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => + { + // Arrange: Track some instance + var targetInstance = new SomePublicType(); + jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); + + // Act + var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); + + // Assert + Assert.Null(resultJson); + Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); + }); + + [Fact] + public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => + { + // This test addresses the case where the developer calls objectRef.Dispose() + // from .NET code, as opposed to .dispose() from JS code + + // Arrange: Track some instance, then dispose it + var targetInstance = new SomePublicType(); + var objectRef = new DotNetObjectRef(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); + objectRef.Dispose(); + + // Act/Assert + var ex = Assert.Throws( + () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); + }); + + [Fact] + public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime => + { + // This test addresses the case where the developer calls .dispose() + // from JS code, as opposed to objectRef.Dispose() from .NET code + + // Arrange: Track some instance, then dispose it + var targetInstance = new SomePublicType(); + var objectRef = new DotNetObjectRef(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); + DotNetDispatcher.ReleaseDotNetObject(1); + + // Act/Assert + var ex = Assert.Throws( + () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); + }); + + [Fact] + public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime => + { + // Arrange: Track some instance plus another object we'll pass as a param + var targetInstance = new SomePublicType(); + var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; + jsRuntime.Invoke("unimportant", + new DotNetObjectRef(targetInstance), + new DotNetObjectRef(arg2)); + var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]"; + + // Act + var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); + + // Assert + Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson); + var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); + Assert.Equal(1235, resultDto.IntVal); + Assert.Equal("MY STRING", resultDto.StringVal); + }); [Fact] public void CannotInvokeWithIncorrectNumberOfParams() @@ -121,10 +221,73 @@ namespace Microsoft.JSInterop.Test // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", argsJson); + DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); }); - Assert.Equal("In call to 'InvocableStaticWithParams', expected 2 parameters but received 4.", ex.Message); + Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message); + } + + [Fact] + public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime => + { + // Arrange: Track some instance plus another object we'll pass as a param + var targetInstance = new SomePublicType(); + var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; + jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance), new DotNetObjectRef(arg2)); + + // Arrange: all args + var argsJson = Json.Serialize(new object[] + { + new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, + "__dotNetObject:2" + }); + + // Act + var callId = "123"; + var resultTask = jsRuntime.NextInvocationTask; + DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); + await resultTask; + var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); + var resultValue = (SimpleJson.JsonArray)result[2]; + + // Assert: Correct info to complete the async call + Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET + Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier); + Assert.Equal(3, result.Count); + Assert.Equal(callId, result[0]); + Assert.True((bool)result[1]); // Success flag + + // Assert: First result value marshalled via JSON + var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO)); + Assert.Equal("STRING VIA JSON", resultDto1.StringVal); + Assert.Equal(2000, resultDto1.IntVal); + + // Assert: Second result value marshalled by ref + var resultDto2Ref = (string)resultValue[1]; + Assert.Equal("__dotNetObject:3", resultDto2Ref); + var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); + Assert.Equal("MY STRING", resultDto2.StringVal); + Assert.Equal(2468, resultDto2.IntVal); + }); + + Task WithJSRuntime(Action testCode) + { + return WithJSRuntime(jsRuntime => + { + testCode(jsRuntime); + return Task.CompletedTask; + }); + } + + async Task WithJSRuntime(Func testCode) + { + // Since the tests rely on the asynclocal JSRuntime.Current, ensure we + // are on a distinct async context with a non-null JSRuntime.Current + await Task.Yield(); + + var runtime = new TestJSRuntime(); + JSRuntime.SetCurrentJSRuntime(runtime); + await testCode(runtime); } internal class SomeInteralType @@ -134,15 +297,17 @@ namespace Microsoft.JSInterop.Test public class SomePublicType { - public static bool DidInvokeMyInvocableVoid; + public static bool DidInvokeMyInvocableStaticVoid; + public bool DidInvokeMyInvocableInstanceVoid; - [JSInvokable("PrivateMethod")] private void MyPrivateMethod() { } - [JSInvokable("ProtectedMethod")] protected void MyProtectedMethod() { } - protected void MethodWithoutAttribute() { } + [JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { } + [JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { } + protected static void StaticMethodWithoutAttribute() { } + protected static void InstanceMethodWithoutAttribute() { } [JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid() { - DidInvokeMyInvocableVoid = true; + DidInvokeMyInvocableStaticVoid = true; } [JSInvokable("InvocableStaticNonVoid")] @@ -150,8 +315,65 @@ namespace Microsoft.JSInterop.Test => new TestDTO { StringVal = "Test", IntVal = 123 }; [JSInvokable("InvocableStaticWithParams")] - public static TestDTO MyInvocableWithParams(TestDTO dto, int[] incrementAmounts) - => new TestDTO { StringVal = dto.StringVal.ToUpperInvariant(), IntVal = dto.IntVal + incrementAmounts.Sum() }; + public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef) + => new object[] + { + new TestDTO // Return via JSON marshalling + { + StringVal = dtoViaJson.StringVal.ToUpperInvariant(), + IntVal = dtoViaJson.IntVal + incrementAmounts.Sum() + }, + new DotNetObjectRef(new TestDTO // Return by ref + { + StringVal = dtoByRef.StringVal.ToUpperInvariant(), + IntVal = dtoByRef.IntVal + incrementAmounts.Sum() + }) + }; + + [JSInvokable] + public static TestDTO InvokableMethodWithoutCustomIdentifier() + => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 }; + + [JSInvokable] + public void InvokableInstanceVoid() + { + DidInvokeMyInvocableInstanceVoid = true; + } + + [JSInvokable] + public object[] InvokableInstanceMethod(string someString, TestDTO someDTO) + { + // Returning an array to make the point that object references + // can be embedded anywhere in the result + return new object[] + { + $"You passed {someString}", + new DotNetObjectRef(new TestDTO + { + IntVal = someDTO.IntVal + 1, + StringVal = someDTO.StringVal.ToUpperInvariant() + }) + }; + } + + [JSInvokable] + public async Task InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef) + { + await Task.Delay(50); + return new object[] + { + new TestDTO // Return via JSON + { + StringVal = dtoViaJson.StringVal.ToUpperInvariant(), + IntVal = dtoViaJson.IntVal * 2, + }, + new DotNetObjectRef(new TestDTO // Return by ref + { + StringVal = dtoByRef.StringVal.ToUpperInvariant(), + IntVal = dtoByRef.IntVal * 2, + }) + }; + } } public class TestDTO @@ -159,5 +381,33 @@ namespace Microsoft.JSInterop.Test public string StringVal { get; set; } public int IntVal { get; set; } } + + public class TestJSRuntime : JSInProcessRuntimeBase + { + private TaskCompletionSource _nextInvocationTcs = new TaskCompletionSource(); + public Task NextInvocationTask => _nextInvocationTcs.Task; + public long LastInvocationAsyncHandle { get; private set; } + public string LastInvocationIdentifier { get; private set; } + public string LastInvocationArgsJson { get; private set; } + + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + { + LastInvocationAsyncHandle = asyncHandle; + LastInvocationIdentifier = identifier; + LastInvocationArgsJson = argsJson; + _nextInvocationTcs.SetResult(null); + _nextInvocationTcs = new TaskCompletionSource(); + } + + protected override string InvokeJS(string identifier, string argsJson) + { + LastInvocationAsyncHandle = default; + LastInvocationIdentifier = identifier; + LastInvocationArgsJson = argsJson; + _nextInvocationTcs.SetResult(null); + _nextInvocationTcs = new TaskCompletionSource(); + return null; + } + } } } diff --git a/test/Microsoft.JSInterop.Test/DotNetObjectRefTest.cs b/test/Microsoft.JSInterop.Test/DotNetObjectRefTest.cs new file mode 100644 index 0000000000..969dcae79d --- /dev/null +++ b/test/Microsoft.JSInterop.Test/DotNetObjectRefTest.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.JSInterop.Test +{ + public class DotNetObjectRefTest + { + [Fact] + public void CanAccessValue() + { + var obj = new object(); + Assert.Same(obj, new DotNetObjectRef(obj).Value); + } + + [Fact] + public void CanAssociateWithSameRuntimeMultipleTimes() + { + var objRef = new DotNetObjectRef(new object()); + var jsRuntime = new TestJsRuntime(); + objRef.EnsureAttachedToJsRuntime(jsRuntime); + objRef.EnsureAttachedToJsRuntime(jsRuntime); + } + + [Fact] + public void CannotAssociateWithDifferentRuntimes() + { + var objRef = new DotNetObjectRef(new object()); + var jsRuntime1 = new TestJsRuntime(); + var jsRuntime2 = new TestJsRuntime(); + objRef.EnsureAttachedToJsRuntime(jsRuntime1); + + var ex = Assert.Throws( + () => objRef.EnsureAttachedToJsRuntime(jsRuntime2)); + Assert.Contains("Do not attempt to re-use", ex.Message); + } + + [Fact] + public void NotifiesAssociatedJsRuntimeOfDisposal() + { + // Arrange + var objRef = new DotNetObjectRef(new object()); + var jsRuntime = new TestJsRuntime(); + objRef.EnsureAttachedToJsRuntime(jsRuntime); + + // Act + objRef.Dispose(); + + // Assert + Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs); + } + + class TestJsRuntime : IJSRuntime + { + public List UntrackedRefs = new List(); + + public Task InvokeAsync(string identifier, params object[] args) + => throw new NotImplementedException(); + + public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) + => UntrackedRefs.Add(dotNetObjectRef); + } + } +} diff --git a/test/Microsoft.JSInterop.Test/JSInProcessRuntimeBaseTest.cs b/test/Microsoft.JSInterop.Test/JSInProcessRuntimeBaseTest.cs new file mode 100644 index 0000000000..d2e71f6eb2 --- /dev/null +++ b/test/Microsoft.JSInterop.Test/JSInProcessRuntimeBaseTest.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.JSInterop.Test +{ + public class JSInProcessRuntimeBaseTest + { + [Fact] + public void DispatchesSyncCallsAndDeserializesResults() + { + // Arrange + var runtime = new TestJSInProcessRuntime + { + NextResultJson = Json.Serialize( + new TestDTO { IntValue = 123, StringValue = "Hello" }) + }; + + // Act + var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true ); + var call = runtime.InvokeCalls.Single(); + + // Assert + Assert.Equal(123, syncResult.IntValue); + Assert.Equal("Hello", syncResult.StringValue); + Assert.Equal("test identifier 1", call.Identifier); + Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); + } + + [Fact] + public void SerializesDotNetObjectWrappersInKnownFormat() + { + // Arrange + var runtime = new TestJSInProcessRuntime { NextResultJson = null }; + var obj1 = new object(); + var obj2 = new object(); + var obj3 = new object(); + + // Act + // Showing we can pass the DotNetObject either as top-level args or nested + var syncResult = runtime.Invoke("test identifier", + new DotNetObjectRef(obj1), + new Dictionary + { + { "obj2", new DotNetObjectRef(obj2) }, + { "obj3", new DotNetObjectRef(obj3) } + }); + + // Assert: Handles null result string + Assert.Null(syncResult); + + // Assert: Serialized as expected + var call = runtime.InvokeCalls.Single(); + Assert.Equal("test identifier", call.Identifier); + Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\"}]", call.ArgsJson); + + // Assert: Objects were tracked + Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); + Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); + Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); + } + + [Fact] + public void SyncCallResultCanIncludeDotNetObjects() + { + // Arrange + var runtime = new TestJSInProcessRuntime + { + NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]" + }; + var obj1 = new object(); + var obj2 = new object(); + + // Act + var syncResult = runtime.Invoke("test identifier", + new DotNetObjectRef(obj1), + "some other arg", + new DotNetObjectRef(obj2)); + var call = runtime.InvokeCalls.Single(); + + // Assert + Assert.Equal(new[] { obj2, obj1 }, syncResult); + } + + class TestDTO + { + public int IntValue { get; set; } + public string StringValue { get; set; } + } + + class TestJSInProcessRuntime : JSInProcessRuntimeBase + { + public List InvokeCalls { get; set; } = new List(); + + public string NextResultJson { get; set; } + + protected override string InvokeJS(string identifier, string argsJson) + { + InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson }); + return NextResultJson; + } + + public class InvokeArgs + { + public string Identifier { get; set; } + public string ArgsJson { get; set; } + } + + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + => throw new NotImplementedException("This test only covers sync calls"); + } + } +} diff --git a/test/Microsoft.JSInterop.Test/JSRuntimeBaseTest.cs b/test/Microsoft.JSInterop.Test/JSRuntimeBaseTest.cs index 16168184e1..9193d6deb8 100644 --- a/test/Microsoft.JSInterop.Test/JSRuntimeBaseTest.cs +++ b/test/Microsoft.JSInterop.Test/JSRuntimeBaseTest.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.JSInterop.Internal; using System; using System.Collections.Generic; +using System.Linq; using Xunit; namespace Microsoft.JSInterop.Test @@ -98,7 +100,57 @@ namespace Microsoft.JSInterop.Test }); Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message); } - + + [Fact] + public void SerializesDotNetObjectWrappersInKnownFormat() + { + // Arrange + var runtime = new TestJSRuntime(); + var obj1 = new object(); + var obj2 = new object(); + var obj3 = new object(); + + // Act + // Showing we can pass the DotNetObject either as top-level args or nested + var obj1Ref = new DotNetObjectRef(obj1); + var obj1DifferentRef = new DotNetObjectRef(obj1); + runtime.InvokeAsync("test identifier", + obj1Ref, + new Dictionary + { + { "obj2", new DotNetObjectRef(obj2) }, + { "obj3", new DotNetObjectRef(obj3) }, + { "obj1SameRef", obj1Ref }, + { "obj1DifferentRef", obj1DifferentRef }, + }); + + // Assert: Serialized as expected + var call = runtime.BeginInvokeCalls.Single(); + Assert.Equal("test identifier", call.Identifier); + Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\",\"obj1SameRef\":\"__dotNetObject:1\",\"obj1DifferentRef\":\"__dotNetObject:4\"}]", call.ArgsJson); + + // Assert: Objects were tracked + Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); + Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); + Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); + Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4)); + } + + [Fact] + public void SupportsCustomSerializationForArguments() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Arrange/Act + runtime.InvokeAsync("test identifier", + new WithCustomArgSerializer()); + + // Asssert + var call = runtime.BeginInvokeCalls.Single(); + Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson); + } + class TestJSRuntime : JSRuntimeBase { public List BeginInvokeCalls = new List(); @@ -123,5 +175,17 @@ namespace Microsoft.JSInterop.Test public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException) => EndInvokeJS(asyncHandle, succeeded, resultOrException); } + + class WithCustomArgSerializer : ICustomArgSerializer + { + public object ToJsonPrimitive() + { + return new Dictionary + { + { "key1", "value1" }, + { "key2", 123 }, + }; + } + } } } diff --git a/test/Microsoft.JSInterop.Test/JSRuntimeTest.cs b/test/Microsoft.JSInterop.Test/JSRuntimeTest.cs index 4b5f272047..d5fed45ea4 100644 --- a/test/Microsoft.JSInterop.Test/JSRuntimeTest.cs +++ b/test/Microsoft.JSInterop.Test/JSRuntimeTest.cs @@ -29,6 +29,9 @@ namespace Microsoft.JSInterop.Test { public Task InvokeAsync(string identifier, params object[] args) => throw new NotImplementedException(); + + public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) + => throw new NotImplementedException(); } } } diff --git a/test/Microsoft.JSInterop.Test/JsonUtilTest.cs b/test/Microsoft.JSInterop.Test/JsonUtilTest.cs index ddd5d2f70f..0754924182 100644 --- a/test/Microsoft.JSInterop.Test/JsonUtilTest.cs +++ b/test/Microsoft.JSInterop.Test/JsonUtilTest.cs @@ -191,16 +191,6 @@ namespace Microsoft.JSInterop.Test exception.Message); } - [Fact] - public void SupportsInternalCustomSerializer() - { - // Arrange/Act - var json = Json.Serialize(new WithCustomSerializer()); - - // Asssert - Assert.Equal("{\"key1\":\"value1\",\"key2\":123}", json); - } - // Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41 // The only difference is that our logic doesn't have to handle space-separated words, // because we're only use this for camelcasing .NET member names @@ -273,18 +263,6 @@ namespace Microsoft.JSInterop.Test enum Hobbies { Reading = 1, Swordfighting = 2 } - class WithCustomSerializer : ICustomJsonSerializer - { - public object ToJsonPrimitive() - { - return new Dictionary - { - { "key1", "value1" }, - { "key2", 123 }, - }; - } - } - #pragma warning disable 0649 class ClashingProperties { diff --git a/test/testapps/BasicTestApp/InteropComponent.cshtml b/test/testapps/BasicTestApp/InteropComponent.cshtml index 463a172932..baea881e74 100644 --- a/test/testapps/BasicTestApp/InteropComponent.cshtml +++ b/test/testapps/BasicTestApp/InteropComponent.cshtml @@ -12,6 +12,20 @@ } +
+

.NET to JS calls: passing .NET object by ref, receiving .NET object by ref

+ @foreach (var kvp in ReceiveDotNetObjectByRefResult) + { +

@(kvp.Key)Sync

+

@kvp.Value

+ } + @foreach (var kvp in ReceiveDotNetObjectByRefAsyncResult) + { +

@(kvp.Key)Async

+

@kvp.Value

+ } +
+

Return values and exceptions thrown from .NET

@foreach (var returnValue in ReturnValues) @@ -44,14 +58,23 @@ public JSException SyncExceptionFromAsyncMethod { get; set; } public JSException AsyncExceptionFromAsyncMethod { get; set; } + public IDictionary ReceiveDotNetObjectByRefResult { get; set; } = new Dictionary(); + public IDictionary ReceiveDotNetObjectByRefAsyncResult { get; set; } = new Dictionary(); + public bool DoneWithInterop { get; set; } public async Task InvokeInteropAsync() { var inProcRuntime = ((IJSInProcessRuntime)JSRuntime.Current); + var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123); + + var instanceMethodsTarget = new JavaScriptInterop(); Console.WriteLine("Starting interop invocations."); - await JSRuntime.Current.InvokeAsync("jsInteropTests.invokeDotNetInteropMethodsAsync"); + await JSRuntime.Current.InvokeAsync( + "jsInteropTests.invokeDotNetInteropMethodsAsync", + new DotNetObjectRef(testDTOTOPassByRef), + new DotNetObjectRef(instanceMethodsTarget)); Console.WriteLine("Showing interop invocation results."); var collectResults = inProcRuntime.Invoke>("jsInteropTests.collectInteropResults"); @@ -91,6 +114,20 @@ AsyncExceptionFromAsyncMethod = e; } + var passDotNetObjectByRef = new TestDTO(99999); + var passDotNetObjectByRefArg = new Dictionary + { + { "stringValue", "My string" }, + { "testDto", new DotNetObjectRef(passDotNetObjectByRef) }, + }; + ReceiveDotNetObjectByRefResult = inProcRuntime.Invoke>("receiveDotNetObjectByRef", passDotNetObjectByRefArg); + ReceiveDotNetObjectByRefAsyncResult = await JSRuntime.Current.InvokeAsync>("receiveDotNetObjectByRefAsync", passDotNetObjectByRefArg); + ReceiveDotNetObjectByRefResult["testDto"] = ReceiveDotNetObjectByRefResult["testDto"] == passDotNetObjectByRef ? "Same" : "Different"; + ReceiveDotNetObjectByRefAsyncResult["testDto"] = ReceiveDotNetObjectByRefAsyncResult["testDto"] == passDotNetObjectByRef ? "Same" : "Different"; + + ReturnValues["returnPrimitive"] = inProcRuntime.Invoke("returnPrimitive").ToString(); + ReturnValues["returnPrimitiveAsync"] = (await JSRuntime.Current.InvokeAsync("returnPrimitiveAsync")).ToString(); + Invocations = invocations; DoneWithInterop = true; } diff --git a/test/testapps/BasicTestApp/InteropTest/JavaScriptInterop.cs b/test/testapps/BasicTestApp/InteropTest/JavaScriptInterop.cs index 5b6cc033a6..f1487c06e3 100644 --- a/test/testapps/BasicTestApp/InteropTest/JavaScriptInterop.cs +++ b/test/testapps/BasicTestApp/InteropTest/JavaScriptInterop.cs @@ -4,7 +4,6 @@ using Microsoft.JSInterop; using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; namespace BasicTestApp.InteropTest @@ -13,33 +12,33 @@ namespace BasicTestApp.InteropTest { public static IDictionary Invocations = new Dictionary(); - [JSInvokable(nameof(ThrowException))] + [JSInvokable] public static void ThrowException() => throw new InvalidOperationException("Threw an exception!"); - [JSInvokable(nameof(AsyncThrowSyncException))] + [JSInvokable] public static Task AsyncThrowSyncException() => throw new InvalidOperationException("Threw a sync exception!"); - [JSInvokable(nameof(AsyncThrowAsyncException))] + [JSInvokable] public static async Task AsyncThrowAsyncException() { await Task.Yield(); throw new InvalidOperationException("Threw an async exception!"); } - [JSInvokable(nameof(VoidParameterless))] + [JSInvokable] public static void VoidParameterless() { Invocations[nameof(VoidParameterless)] = new object[0]; } - [JSInvokable(nameof(VoidWithOneParameter))] + [JSInvokable] public static void VoidWithOneParameter(ComplexParameter parameter1) { Invocations[nameof(VoidWithOneParameter)] = new object[] { parameter1 }; } - [JSInvokable(nameof(VoidWithTwoParameters))] + [JSInvokable] public static void VoidWithTwoParameters( ComplexParameter parameter1, byte parameter2) @@ -47,88 +46,88 @@ namespace BasicTestApp.InteropTest Invocations[nameof(VoidWithTwoParameters)] = new object[] { parameter1, parameter2 }; } - [JSInvokable(nameof(VoidWithThreeParameters))] + [JSInvokable] public static void VoidWithThreeParameters( ComplexParameter parameter1, byte parameter2, - short parameter3) + TestDTO parameter3) { - Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3 }; + Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() }; } - [JSInvokable(nameof(VoidWithFourParameters))] + [JSInvokable] public static void VoidWithFourParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4) { - Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4 }; + Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 }; } - [JSInvokable(nameof(VoidWithFiveParameters))] + [JSInvokable] public static void VoidWithFiveParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5) { - Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 }; + Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 }; } - [JSInvokable(nameof(VoidWithSixParameters))] + [JSInvokable] public static void VoidWithSixParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6) { - Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 }; + Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 }; } - [JSInvokable(nameof(VoidWithSevenParameters))] + [JSInvokable] public static void VoidWithSevenParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7) { - Invocations[nameof(VoidWithSevenParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 }; + Invocations[nameof(VoidWithSevenParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7 }; } - [JSInvokable(nameof(VoidWithEightParameters))] + [JSInvokable] public static void VoidWithEightParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7, Segment parameter8) { - Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 }; + Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 }; } - [JSInvokable(nameof(ReturnArray))] + [JSInvokable] public static decimal[] ReturnArray() { return new decimal[] { 0.1M, 0.2M }; } - [JSInvokable(nameof(EchoOneParameter))] + [JSInvokable] public static object[] EchoOneParameter(ComplexParameter parameter1) { return new object[] { parameter1 }; } - [JSInvokable(nameof(EchoTwoParameters))] + [JSInvokable] public static object[] EchoTwoParameters( ComplexParameter parameter1, byte parameter2) @@ -136,88 +135,88 @@ namespace BasicTestApp.InteropTest return new object[] { parameter1, parameter2 }; } - [JSInvokable(nameof(EchoThreeParameters))] + [JSInvokable] public static object[] EchoThreeParameters( ComplexParameter parameter1, byte parameter2, - short parameter3) + TestDTO parameter3) { - return new object[] { parameter1, parameter2, parameter3 }; + return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() }; } - [JSInvokable(nameof(EchoFourParameters))] + [JSInvokable] public static object[] EchoFourParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4) { - return new object[] { parameter1, parameter2, parameter3, parameter4 }; + return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 }; } - [JSInvokable(nameof(EchoFiveParameters))] + [JSInvokable] public static object[] EchoFiveParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5) { - return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 }; + return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 }; } - [JSInvokable(nameof(EchoSixParameters))] + [JSInvokable] public static object[] EchoSixParameters(ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6) { - return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 }; + return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 }; } - [JSInvokable(nameof(EchoSevenParameters))] + [JSInvokable] public static object[] EchoSevenParameters(ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7) { - return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 }; + return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7 }; } - [JSInvokable(nameof(EchoEightParameters))] + [JSInvokable] public static object[] EchoEightParameters( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7, Segment parameter8) { - return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 }; + return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 }; } - [JSInvokable(nameof(VoidParameterlessAsync))] + [JSInvokable] public static Task VoidParameterlessAsync() { Invocations[nameof(VoidParameterlessAsync)] = new object[0]; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithOneParameterAsync))] + [JSInvokable] public static Task VoidWithOneParameterAsync(ComplexParameter parameter1) { Invocations[nameof(VoidWithOneParameterAsync)] = new object[] { parameter1 }; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithTwoParametersAsync))] + [JSInvokable] public static Task VoidWithTwoParametersAsync( ComplexParameter parameter1, byte parameter2) @@ -226,94 +225,94 @@ namespace BasicTestApp.InteropTest return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithThreeParametersAsync))] + [JSInvokable] public static Task VoidWithThreeParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3) + TestDTO parameter3) { - Invocations[nameof(VoidWithThreeParametersAsync)] = new object[] { parameter1, parameter2, parameter3 }; + Invocations[nameof(VoidWithThreeParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() }; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithFourParametersAsync))] + [JSInvokable] public static Task VoidWithFourParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4) { - Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4 }; + Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 }; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithFiveParametersAsync))] + [JSInvokable] public static Task VoidWithFiveParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5) { - Invocations[nameof(VoidWithFiveParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 }; + Invocations[nameof(VoidWithFiveParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 }; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithSixParametersAsync))] + [JSInvokable] public static Task VoidWithSixParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6) { - Invocations[nameof(VoidWithSixParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 }; + Invocations[nameof(VoidWithSixParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 }; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithSevenParametersAsync))] + [JSInvokable] public static Task VoidWithSevenParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7) { - Invocations[nameof(VoidWithSevenParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 }; + Invocations[nameof(VoidWithSevenParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7 }; return Task.CompletedTask; } - [JSInvokable(nameof(VoidWithEightParametersAsync))] + [JSInvokable] public static Task VoidWithEightParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7, Segment parameter8) { - Invocations[nameof(VoidWithEightParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 }; + Invocations[nameof(VoidWithEightParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 }; return Task.CompletedTask; } - [JSInvokable(nameof(ReturnArrayAsync))] + [JSInvokable] public static Task ReturnArrayAsync() { return Task.FromResult(new decimal[] { 0.1M, 0.2M }); } - [JSInvokable(nameof(EchoOneParameterAsync))] + [JSInvokable] public static Task EchoOneParameterAsync(ComplexParameter parameter1) { return Task.FromResult(new object[] { parameter1 }); } - [JSInvokable(nameof(EchoTwoParametersAsync))] + [JSInvokable] public static Task EchoTwoParametersAsync( ComplexParameter parameter1, byte parameter2) @@ -321,72 +320,128 @@ namespace BasicTestApp.InteropTest return Task.FromResult(new object[] { parameter1, parameter2 }); } - [JSInvokable(nameof(EchoThreeParametersAsync))] + [JSInvokable] public static Task EchoThreeParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3) + TestDTO parameter3) { - return Task.FromResult(new object[] { parameter1, parameter2, parameter3 }); + return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() }); } - [JSInvokable(nameof(EchoFourParametersAsync))] + [JSInvokable] public static Task EchoFourParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4) { - return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4 }); + return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 }); } - [JSInvokable(nameof(EchoFiveParametersAsync))] + [JSInvokable] public static Task EchoFiveParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5) { - return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 }); + return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 }); } - [JSInvokable(nameof(EchoSixParametersAsync))] + [JSInvokable] public static Task EchoSixParametersAsync(ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6) { - return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 }); + return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 }); } - [JSInvokable(nameof(EchoSevenParametersAsync))] + [JSInvokable] public static Task EchoSevenParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7) { - return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7 }); + return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7 }); } - [JSInvokable(nameof(EchoEightParametersAsync))] + [JSInvokable] public static Task EchoEightParametersAsync( ComplexParameter parameter1, byte parameter2, - short parameter3, + TestDTO parameter3, int parameter4, long parameter5, float parameter6, List parameter7, Segment parameter8) { - return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 }); + return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 }); + } + + [JSInvokable] + public static Dictionary ReturnDotNetObjectByRef() + { + return new Dictionary + { + { "Some sync instance", new DotNetObjectRef(new TestDTO(1000)) } + }; + } + + [JSInvokable] + public static async Task> ReturnDotNetObjectByRefAsync() + { + await Task.Yield(); + return new Dictionary + { + { "Some async instance", new DotNetObjectRef(new TestDTO(1001)) } + }; + } + + [JSInvokable] + public static int ExtractNonSerializedValue(TestDTO objectByRef) + { + return objectByRef.GetNonSerializedValue(); + } + + [JSInvokable] + public Dictionary InstanceMethod(Dictionary dict) + { + // This method shows we can pass in values marshalled both as JSON (the dict itself) + // and by ref (the incoming dtoByRef), plus that we can return values marshalled as + // JSON (the returned dictionary) and by ref (the outgoingByRef value) + return new Dictionary + { + { "thisTypeName", GetType().Name }, + { "stringValueUpper", ((string)dict["stringValue"]).ToUpperInvariant() }, + { "incomingByRef", ((TestDTO)dict["dtoByRef"]).GetNonSerializedValue() }, + { "outgoingByRef", new DotNetObjectRef(new TestDTO(1234)) }, + }; + } + + [JSInvokable] + public async Task> InstanceMethodAsync(Dictionary dict) + { + // This method shows we can pass in values marshalled both as JSON (the dict itself) + // and by ref (the incoming dtoByRef), plus that we can return values marshalled as + // JSON (the returned dictionary) and by ref (the outgoingByRef value) + await Task.Yield(); + return new Dictionary + { + { "thisTypeName", GetType().Name }, + { "stringValueUpper", ((string)dict["stringValue"]).ToUpperInvariant() }, + { "incomingByRef", ((TestDTO)dict["dtoByRef"]).GetNonSerializedValue() }, + { "outgoingByRef", new DotNetObjectRef(new TestDTO(1234)) }, + }; } } } diff --git a/test/testapps/BasicTestApp/InteropTest/TestDTO.cs b/test/testapps/BasicTestApp/InteropTest/TestDTO.cs new file mode 100644 index 0000000000..04b3462456 --- /dev/null +++ b/test/testapps/BasicTestApp/InteropTest/TestDTO.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace BasicTestApp.InteropTest +{ + public class TestDTO + { + // JSON serialization won't include this in its output, nor will the JSON + // deserializer be able to populate it. So if the value is retained, this + // shows we're passing the object by reference, not via JSON marshalling. + private readonly int _nonSerializedValue; + + public TestDTO(int nonSerializedValue) + { + _nonSerializedValue = nonSerializedValue; + } + + public int GetNonSerializedValue() + => _nonSerializedValue; + } +} diff --git a/test/testapps/BasicTestApp/wwwroot/js/jsinteroptests.js b/test/testapps/BasicTestApp/wwwroot/js/jsinteroptests.js index 7a8ad7d914..4bbd4aca3b 100644 --- a/test/testapps/BasicTestApp/wwwroot/js/jsinteroptests.js +++ b/test/testapps/BasicTestApp/wwwroot/js/jsinteroptests.js @@ -4,59 +4,85 @@ var results = {}; var assemblyName = 'BasicTestApp'; -function invokeDotNetInteropMethodsAsync() { +function invokeDotNetInteropMethodsAsync(dotNetObjectByRef, instanceMethodsTarget) { console.log('Invoking void sync methods.'); DotNet.invokeMethod(assemblyName, 'VoidParameterless'); - DotNet.invokeMethod(assemblyName, 'VoidWithOneParameter', ...createArgumentList(1)); - DotNet.invokeMethod(assemblyName, 'VoidWithTwoParameters', ...createArgumentList(2)); - DotNet.invokeMethod(assemblyName, 'VoidWithThreeParameters', ...createArgumentList(3)); - DotNet.invokeMethod(assemblyName, 'VoidWithFourParameters', ...createArgumentList(4)); - DotNet.invokeMethod(assemblyName, 'VoidWithFiveParameters', ...createArgumentList(5)); - DotNet.invokeMethod(assemblyName, 'VoidWithSixParameters', ...createArgumentList(6)); - DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7)); - DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8)); + DotNet.invokeMethod(assemblyName, 'VoidWithOneParameter', ...createArgumentList(1, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithTwoParameters', ...createArgumentList(2, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithThreeParameters', ...createArgumentList(3, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithFourParameters', ...createArgumentList(4, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithFiveParameters', ...createArgumentList(5, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithSixParameters', ...createArgumentList(6, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7, dotNetObjectByRef)); + DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8, dotNetObjectByRef)); console.log('Invoking returning sync methods.'); results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray'); - results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1)); - results['result3'] = DotNet.invokeMethod(assemblyName, 'EchoTwoParameters', ...createArgumentList(2)); - results['result4'] = DotNet.invokeMethod(assemblyName, 'EchoThreeParameters', ...createArgumentList(3)); - results['result5'] = DotNet.invokeMethod(assemblyName, 'EchoFourParameters', ...createArgumentList(4)); - results['result6'] = DotNet.invokeMethod(assemblyName, 'EchoFiveParameters', ...createArgumentList(5)); - results['result7'] = DotNet.invokeMethod(assemblyName, 'EchoSixParameters', ...createArgumentList(6)); - results['result8'] = DotNet.invokeMethod(assemblyName, 'EchoSevenParameters', ...createArgumentList(7)); - results['result9'] = DotNet.invokeMethod(assemblyName, 'EchoEightParameters', ...createArgumentList(8)); + results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1, dotNetObjectByRef)); + results['result3'] = DotNet.invokeMethod(assemblyName, 'EchoTwoParameters', ...createArgumentList(2, dotNetObjectByRef)); + results['result4'] = DotNet.invokeMethod(assemblyName, 'EchoThreeParameters', ...createArgumentList(3, dotNetObjectByRef)); + results['result5'] = DotNet.invokeMethod(assemblyName, 'EchoFourParameters', ...createArgumentList(4, dotNetObjectByRef)); + results['result6'] = DotNet.invokeMethod(assemblyName, 'EchoFiveParameters', ...createArgumentList(5, dotNetObjectByRef)); + results['result7'] = DotNet.invokeMethod(assemblyName, 'EchoSixParameters', ...createArgumentList(6, dotNetObjectByRef)); + results['result8'] = DotNet.invokeMethod(assemblyName, 'EchoSevenParameters', ...createArgumentList(7, dotNetObjectByRef)); + results['result9'] = DotNet.invokeMethod(assemblyName, 'EchoEightParameters', ...createArgumentList(8, dotNetObjectByRef)); + + var returnDotNetObjectByRefResult = DotNet.invokeMethod(assemblyName, 'ReturnDotNetObjectByRef'); + results['resultReturnDotNetObjectByRefSync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefResult['Some sync instance']); + + var instanceMethodResult = instanceMethodsTarget.invokeMethod('InstanceMethod', { + stringValue: 'My string', + dtoByRef: dotNetObjectByRef + }); + results['instanceMethodThisTypeName'] = instanceMethodResult.thisTypeName; + results['instanceMethodStringValueUpper'] = instanceMethodResult.stringValueUpper; + results['instanceMethodIncomingByRef'] = instanceMethodResult.incomingByRef; + results['instanceMethodOutgoingByRef'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', instanceMethodResult.outgoingByRef); console.log('Invoking void async methods.'); return DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync') - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7))) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef))) .then(() => { console.log('Invoking returning async methods.'); return DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync') .then(r => results['result1Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef))) .then(r => results['result2Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef))) .then(r => results['result3Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef))) .then(r => results['result4Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef))) .then(r => results['result5Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef))) .then(r => results['result6Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef))) .then(r => results['result7Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7))) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef))) .then(r => results['result8Async'] = r) - .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8))) - .then(r => results['result9Async'] = r); + .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef))) + .then(r => results['result9Async'] = r) + .then(() => DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync')) + .then(r => { + results['resultReturnDotNetObjectByRefAsync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', r['Some async instance']); + }) + .then(() => instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', { + stringValue: 'My string', + dtoByRef: dotNetObjectByRef + })) + .then(r => { + results['instanceMethodThisTypeNameAsync'] = r.thisTypeName; + results['instanceMethodStringValueUpperAsync'] = r.stringValueUpper; + results['instanceMethodIncomingByRefAsync'] = r.incomingByRef; + results['instanceMethodOutgoingByRefAsync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', r.outgoingByRef); + }) }) .then(() => { console.log('Invoking methods that throw exceptions'); @@ -79,7 +105,7 @@ function invokeDotNetInteropMethodsAsync() { }); } -function createArgumentList(argumentNumber){ +function createArgumentList(argumentNumber, dotNetObjectByRef){ const array = new Array(argumentNumber); if (argumentNumber === 0) { return []; @@ -101,7 +127,7 @@ function createArgumentList(argumentNumber){ array[i] = argumentNumber; break; case 2: - array[i] = argumentNumber * 2; + array[i] = dotNetObjectByRef; break; case 3: array[i] = argumentNumber * 4; @@ -136,9 +162,25 @@ window.jsInteropTests = { collectInteropResults: collectInteropResults, functionThrowsException: functionThrowsException, asyncFunctionThrowsSyncException: asyncFunctionThrowsSyncException, - asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException + asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException, + returnPrimitive: returnPrimitive, + returnPrimitiveAsync: returnPrimitiveAsync, + receiveDotNetObjectByRef: receiveDotNetObjectByRef, + receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync, }; +function returnPrimitive() { + return 123; +} + +function returnPrimitiveAsync() { + return new Promise((resolve, reject) => { + setTimeout(function () { + resolve(123); + }, 100); + }); +} + function functionThrowsException() { throw new Error('Function threw an exception!'); } @@ -163,3 +205,29 @@ function collectInteropResults() { return result; } + +function receiveDotNetObjectByRef(incomingData) { + const stringValue = incomingData.stringValue; + const testDto = incomingData.testDto; + + // To verify we received a proper reference to testDto, pass it back into .NET + // to have it evaluate something that only .NET can know + const testDtoNonSerializedValue = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', testDto); + + // To show we can return a .NET object by ref anywhere in a complex structure, + // return it among other values + return { + stringValueUpper: stringValue.toUpperCase(), + testDtoNonSerializedValue: testDtoNonSerializedValue, + testDto: testDto + }; +} + +function receiveDotNetObjectByRefAsync(incomingData) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + const promiseResult = receiveDotNetObjectByRef(incomingData); + resolve(promiseResult); + }, 100); + }); +}