Allow passing DotNetObjectRef to JS in interop calls, and invoking

instance methods on it
This commit is contained in:
Steve Sanderson 2018-07-13 15:03:06 +01:00
parent 38b3051d09
commit 154289ed3d
30 changed files with 1423 additions and 291 deletions

View File

@ -32,8 +32,8 @@ function boot() {
connection.start() connection.start()
.then(async () => { .then(async () => {
DotNet.attachDispatcher({ DotNet.attachDispatcher({
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, argsJson) => { beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, argsJson); connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
} }
}); });

View File

@ -292,27 +292,34 @@ function getArrayDataPointer<T>(array: System_Array<T>): number {
} }
function attachInteropInvoker() { function attachInteropInvoker() {
const dotNetDispatcherInvokeMethodHandle = findMethod('Microsoft.JSInterop', 'Microsoft.JSInterop', 'DotNetDispatcher', 'Invoke'); const dotNetDispatcherInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'InvokeDotNet');
const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Microsoft.JSInterop', 'Microsoft.JSInterop', 'DotNetDispatcher', 'BeginInvoke'); const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet');
DotNet.attachDispatcher({ 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, [ monoPlatform.callMethod(dotNetDispatcherBeginInvokeMethodHandle, null, [
callId ? monoPlatform.toDotNetString(callId.toString()) : null, callId ? monoPlatform.toDotNetString(callId.toString()) : null,
monoPlatform.toDotNetString(assemblyName), monoPlatform.toDotNetString(assemblyNameOrDotNetObjectId!),
monoPlatform.toDotNetString(methodIdentifier), monoPlatform.toDotNetString(methodIdentifier),
monoPlatform.toDotNetString(argsJson) monoPlatform.toDotNetString(argsJson)
]); ]);
}, },
invokeDotNetFromJS: (assemblyName, methodIdentifier, argsJson) => { invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
const resultJsonStringPtr = monoPlatform.callMethod(dotNetDispatcherInvokeMethodHandle, null, [ const resultJsonStringPtr = monoPlatform.callMethod(dotNetDispatcherInvokeMethodHandle, null, [
monoPlatform.toDotNetString(assemblyName), assemblyName ? monoPlatform.toDotNetString(assemblyName) : null,
monoPlatform.toDotNetString(methodIdentifier), monoPlatform.toDotNetString(methodIdentifier),
dotNetObjectId ? monoPlatform.toDotNetString(dotNetObjectId.toString()) : null,
monoPlatform.toDotNetString(argsJson) monoPlatform.toDotNetString(argsJson)
]) as System_String; ]) as System_String;
return resultJsonStringPtr return resultJsonStringPtr
? JSON.parse(monoPlatform.toJavaScriptString(resultJsonStringPtr)) ? monoPlatform.toJavaScriptString(resultJsonStringPtr)
: null; : null;
}, },
}); });

View File

@ -51,9 +51,9 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
CircuitHost = circuitHost; 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) private async void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)

View File

@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
_isInitialized = true; _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(); AssertInitialized();
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
{ {
SetCurrentCircuitHost(this); SetCurrentCircuitHost(this);
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, argsJson); DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
}); });
} }
catch (Exception ex) catch (Exception ex)

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor
/// <summary> /// <summary>
/// Represents a reference to a rendered element. /// Represents a reference to a rendered element.
/// </summary> /// </summary>
public readonly struct ElementRef : ICustomJsonSerializer public readonly struct ElementRef : ICustomArgSerializer
{ {
static long _nextIdForWebAssemblyOnly = 1; static long _nextIdForWebAssemblyOnly = 1;
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Blazor
internal static ElementRef CreateWithUniqueId() internal static ElementRef CreateWithUniqueId()
=> new ElementRef(CreateUniqueId()); => new ElementRef(CreateUniqueId());
object ICustomJsonSerializer.ToJsonPrimitive() object ICustomArgSerializer.ToJsonPrimitive()
{ {
return new Dictionary<string, object> return new Dictionary<string, object>
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -23,17 +24,27 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
/// <param name="assemblyName">The assembly containing the method to be invoked.</param> /// <param name="assemblyName">The assembly containing the method to be invoked.</param>
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param> /// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
/// <param name="argsJson">A JSON representation of the parameters.</param> /// <param name="argsJson">A JSON representation of the parameters.</param>
/// <returns>A JSON representation of the return value, or null.</returns> /// <returns>A JSON representation of the return value, or null.</returns>
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 // 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 // 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, // 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. // 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.
return syncResult == null ? null : Json.Serialize(syncResult); 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);
} }
/// <summary> /// <summary>
@ -42,16 +53,26 @@ namespace Microsoft.JSInterop
/// <param name="callId">A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.</param> /// <param name="callId">A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.</param>
/// <param name="assemblyName">The assembly containing the method to be invoked.</param> /// <param name="assemblyName">The assembly containing the method to be invoked.</param>
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param> /// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
/// <param name="argsJson">A JSON representation of the parameters.</param> /// <param name="argsJson">A JSON representation of the parameters.</param>
/// <returns>A JSON representation of the return value, or null.</returns> /// <returns>A JSON representation of the return value, or null.</returns>
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 // 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 // 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, // 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. // 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 there was no callId, the caller does not want to be notified about the result
if (callId != null) if (callId != null)
@ -61,11 +82,6 @@ namespace Microsoft.JSInterop
var task = syncResult is Task syncResultTask ? syncResultTask : Task.FromResult(syncResult); var task = syncResult is Task syncResultTask ? syncResultTask : Task.FromResult(syncResult);
task.ContinueWith(completedTask => 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 try
{ {
var result = TaskGenericsUtil.GetTaskResult(completedTask); 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); var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier);
// There's no direct way to say we want to deserialize as an array with heterogenous // 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 // 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++) for (var i = 0; i < suppliedArgsLength; i++)
{ {
suppliedArgs[i] = serializerStrategy.DeserializeObject( if (parameterTypes[i] == typeof(JSAsyncCallResult))
suppliedArgs[i], parameterTypes[i]); {
// 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 try
{ {
return methodInfo.Invoke(null, suppliedArgs); return methodInfo.Invoke(targetInstance, suppliedArgs);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -124,10 +160,28 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
/// <param name="asyncHandle">The identifier for the function invocation.</param> /// <param name="asyncHandle">The identifier for the function invocation.</param>
/// <param name="succeeded">A flag to indicate whether the invocation succeeded.</param> /// <param name="succeeded">A flag to indicate whether the invocation succeeded.</param>
/// <param name="resultOrException">If <paramref name="succeeded"/> is <c>true</c>, specifies the invocation result. If <paramref name="succeeded"/> is <c>false</c>, gives the <see cref="Exception"/> corresponding to the invocation failure.</param> /// <param name="result">If <paramref name="succeeded"/> is <c>true</c>, specifies the invocation result. If <paramref name="succeeded"/> is <c>false</c>, gives the <see cref="Exception"/> corresponding to the invocation failure.</param>
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))]
public static void EndInvoke(long asyncHandle, bool succeeded, object resultOrException) public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result)
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, resultOrException); => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException);
/// <summary>
/// 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.
/// </summary>
/// <param name="dotNetObjectId">The identifier previously passed to JavaScript code.</param>
[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) 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, // TODO: Consider looking first for assembly-level attributes (i.e., if there are any,
// only use those) to avoid scanning, especially for framework assemblies. // only use those) to avoid scanning, especially for framework assemblies.
return GetRequiredLoadedAssembly(assemblyName) var result = new Dictionary<string, (MethodInfo, Type[])>();
var invokableMethods = GetRequiredLoadedAssembly(assemblyName)
.GetExportedTypes() .GetExportedTypes()
.SelectMany(type => type.GetMethods()) .SelectMany(type => type.GetMethods())
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false)) .Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
.ToDictionary( foreach (var method in invokableMethods)
method => method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier, {
method => (method, method.GetParameters().Select(p => p.ParameterType).ToArray()) var identifier = method.GetCustomAttribute<JSInvokableAttribute>(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) private static Assembly GetRequiredLoadedAssembly(string assemblyName)

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public class DotNetObjectRef : IDisposable
{
/// <summary>
/// Gets the object instance represented by this wrapper.
/// </summary>
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;
/// <summary>
/// Constructs an instance of <see cref="DotNetObjectRef"/>.
/// </summary>
/// <param name="value">The value being wrapped.</param>
public DotNetObjectRef(object value)
{
Value = value;
}
/// <summary>
/// Ensures the <see cref="DotNetObjectRef"/> is associated with the specified <see cref="IJSRuntime"/>.
/// Developers do not normally need to invoke this manually, since it is called automatically by
/// framework code.
/// </summary>
/// <param name="runtime">The <see cref="IJSRuntime"/>.</param>
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.");
}
}
/// <summary>
/// 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.
/// </summary>
public void Dispose()
{
_attachedToRuntime?.UntrackObjectRef(this);
}
}
}

View File

@ -5,14 +5,14 @@ namespace Microsoft.JSInterop.Internal
{ {
// This is "soft" internal because we're trying to avoid expanding JsonUtil into a sophisticated // 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 // 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 // (it forces structs to be boxed, and returning a dictionary means lots more allocations
// and boxing of any value-typed properties). // and boxing of any value-typed properties).
/// <summary> /// <summary>
/// Internal. Intended for framework use only. /// Internal. Intended for framework use only.
/// </summary> /// </summary>
public interface ICustomJsonSerializer public interface ICustomArgSerializer
{ {
/// <summary> /// <summary>
/// Internal. Intended for framework use only. /// Internal. Intended for framework use only.

View File

@ -18,5 +18,15 @@ namespace Microsoft.JSInterop
/// <param name="args">JSON-serializable arguments.</param> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
Task<T> InvokeAsync<T>(string identifier, params object[] args); Task<T> InvokeAsync<T>(string identifier, params object[] args);
/// <summary>
/// Stops tracking the .NET object represented by the <see cref="DotNetObjectRef"/>.
/// 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.
/// </summary>
/// <param name="dotNetObjectRef">The reference to stop tracking.</param>
/// <remarks>This method is called automaticallly by <see cref="DotNetObjectRef.Dispose"/>.</remarks>
void UntrackObjectRef(DotNetObjectRef dotNetObjectRef);
} }
} }

View File

@ -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<long, DotNetObjectRef> _trackedRefsById = new Dictionary<long, DotNetObjectRef>();
private Dictionary<DotNetObjectRef, long> _trackedIdsByRef = new Dictionary<DotNetObjectRef, long>();
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));
}
}
/// <summary>
/// Stops tracking the specified .NET object reference.
/// This overload is typically invoked from JS code via JS interop.
/// </summary>
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectRef"/>.</param>
public void ReleaseDotNetObject(long dotNetObjectId)
{
lock (_storageLock)
{
if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef))
{
_trackedRefsById.Remove(dotNetObjectId);
_trackedIdsByRef.Remove(dotNetObjectRef);
}
}
}
/// <summary>
/// Stops tracking the specified .NET object reference.
/// This overload is typically invoked from .NET code by <see cref="DotNetObjectRef.Dispose"/>.
/// </summary>
/// <param name="dotNetObjectRef">The <see cref="DotNetObjectRef"/>.</param>
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);
}
}
}
}
}

View File

@ -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<T>. 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<T>.
//
// 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.
/// <summary>
/// Intended for framework use only.
/// </summary>
public class JSAsyncCallResult
{
internal object ResultOrException { get; }
/// <summary>
/// Constructs an instance of <see cref="JSAsyncCallResult"/>.
/// </summary>
/// <param name="resultOrException">The result of the call.</param>
internal JSAsyncCallResult(object resultOrException)
{
ResultOrException = resultOrException;
}
}
}

View File

@ -17,8 +17,8 @@ namespace Microsoft.JSInterop
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
public T Invoke<T>(string identifier, params object[] args) public T Invoke<T>(string identifier, params object[] args)
{ {
var resultJson = InvokeJS(identifier, Json.Serialize(args)); var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy));
return Json.Deserialize<T>(resultJson); return Json.Deserialize<T>(resultJson, ArgSerializerStrategy);
} }
/// <summary> /// <summary>

View File

@ -16,11 +16,23 @@ namespace Microsoft.JSInterop
/// <summary> /// <summary>
/// Gets the identifier for the method. The identifier must be unique within the scope /// Gets the identifier for the method. The identifier must be unique within the scope
/// of an assembly. /// 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.
/// </summary> /// </summary>
public string Identifier { get; } public string Identifier { get; }
/// <summary> /// <summary>
/// Constructs an instance of <see cref="JSInvokableAttribute"/>. /// Constructs an instance of <see cref="JSInvokableAttribute"/> without setting
/// an identifier for the method.
/// </summary>
public JSInvokableAttribute()
{
}
/// <summary>
/// Constructs an instance of <see cref="JSInvokableAttribute"/> using the specified
/// identifier.
/// </summary> /// </summary>
/// <param name="identifier">An identifier for the method, which must be unique within the scope of the assembly.</param> /// <param name="identifier">An identifier for the method, which must be unique within the scope of the assembly.</param>
public JSInvokableAttribute(string identifier) public JSInvokableAttribute(string identifier)

View File

@ -17,6 +17,20 @@ namespace Microsoft.JSInterop
private readonly ConcurrentDictionary<long, object> _pendingTasks private readonly ConcurrentDictionary<long, object> _pendingTasks
= new ConcurrentDictionary<long, object>(); = new ConcurrentDictionary<long, object>();
internal InteropArgSerializerStrategy ArgSerializerStrategy { get; }
/// <summary>
/// Constructs an instance of <see cref="JSRuntimeBase"/>.
/// </summary>
public JSRuntimeBase()
{
ArgSerializerStrategy = new InteropArgSerializerStrategy(this);
}
/// <inheritdoc />
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef);
/// <summary> /// <summary>
/// Invokes the specified JavaScript function asynchronously. /// Invokes the specified JavaScript function asynchronously.
/// </summary> /// </summary>
@ -36,7 +50,10 @@ namespace Microsoft.JSInterop
try 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; return tcs.Task;
} }
catch catch
@ -70,7 +87,7 @@ namespace Microsoft.JSInterop
callId, callId,
success, success,
resultOrException resultOrException
})); }, ArgSerializerStrategy));
} }
internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException) internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException)
@ -82,7 +99,11 @@ namespace Microsoft.JSInterop
if (succeeded) 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 else
{ {

View File

@ -40,13 +40,7 @@ module DotNet {
* @returns The result of the operation. * @returns The result of the operation.
*/ */
export function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T { export function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T {
const dispatcher = getRequiredDispatcher(); return invokePossibleInstanceMethod<T>(assemblyName, methodIdentifier, null, args);
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.');
}
} }
/** /**
@ -58,14 +52,29 @@ module DotNet {
* @returns A promise representing the result of the operation. * @returns A promise representing the result of the operation.
*/ */
export function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T> { export function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T> {
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
}
function invokePossibleInstanceMethod<T>(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<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise<T> {
const asyncCallId = nextAsyncCallId++; const asyncCallId = nextAsyncCallId++;
const resultPromise = new Promise<T>((resolve, reject) => { const resultPromise = new Promise<T>((resolve, reject) => {
pendingAsyncCalls[asyncCallId] = { resolve, reject }; pendingAsyncCalls[asyncCallId] = { resolve, reject };
}); });
try { try {
const argsJson = JSON.stringify(args); const argsJson = JSON.stringify(args, argReplacer);
getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, argsJson); getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
} catch(ex) { } catch(ex) {
// Synchronous failure // Synchronous failure
completePendingCall(asyncCallId, false, ex); 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. * 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 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. * @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. * 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 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 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. * @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)); const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
return result === null || result === undefined return result === null || result === undefined
? null ? null
: JSON.stringify(result); : JSON.stringify(result, argReplacer);
}, },
/** /**
@ -172,8 +183,8 @@ module DotNet {
// On completion, dispatch result back to .NET // On completion, dispatch result back to .NET
// Not using "await" because it codegens a lot of boilerplate // Not using "await" because it codegens a lot of boilerplate
promise.then( promise.then(
result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', JSON.stringify([asyncHandle, true, result])), result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)),
error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', JSON.stringify([asyncHandle, false, formatError(error)])) 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.`); throw new Error(`The value '${resultIdentifier}' is not a function.`);
} }
} }
class DotNetObject {
constructor(private _id: number) {
}
public invokeMethod<T>(methodIdentifier: string, ...args: any[]): T {
return invokePossibleInstanceMethod<T>(null, methodIdentifier, this._id, args);
}
public invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T> {
return invokePossibleInstanceMethodAsync<T>(null, methodIdentifier, this._id, args);
}
public dispose() {
const promise = invokeMethodAsync<any>(
'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;
}
} }

View File

@ -21,6 +21,9 @@ namespace Microsoft.JSInterop
public static string Serialize(object value) public static string Serialize(object value)
=> SimpleJson.SimpleJson.SerializeObject(value); => SimpleJson.SimpleJson.SerializeObject(value);
internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy)
=> SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy);
/// <summary> /// <summary>
/// Deserializes the JSON string, creating an object of the specified generic type. /// Deserializes the JSON string, creating an object of the specified generic type.
/// </summary> /// </summary>
@ -29,5 +32,8 @@ namespace Microsoft.JSInterop
/// <returns>An object of the specified type.</returns> /// <returns>An object of the specified type.</returns>
public static T Deserialize<T>(string json) public static T Deserialize<T>(string json)
=> SimpleJson.SimpleJson.DeserializeObject<T>(json); => SimpleJson.SimpleJson.DeserializeObject<T>(json);
internal static T Deserialize<T>(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy)
=> SimpleJson.SimpleJson.DeserializeObject<T>(json, serializerStrategy);
} }
} }

View File

@ -67,7 +67,6 @@ using System.Reflection;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Microsoft.JSInterop.Internal;
using SimpleJson.Reflection; using SimpleJson.Reflection;
// ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable LoopCanBeConvertedToQuery
@ -1538,8 +1537,6 @@ namespace SimpleJson
output = input.ToString(); output = input.ToString();
else if (input is TimeSpan) else if (input is TimeSpan)
output = ((TimeSpan)input).ToString("c"); output = ((TimeSpan)input).ToString("c");
else if (input is ICustomJsonSerializer customJsonSerializer)
output = customJsonSerializer.ToJsonPrimitive();
else else
{ {
Enum inputEnum = input as Enum; Enum inputEnum = input as Enum;

View File

@ -22,26 +22,41 @@ namespace Microsoft.JSInterop
public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception) public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception)
=> CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception); => CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception);
public static Type GetTaskCompletionSourceResultType(object taskCompletionSource)
=> CreateResultSetter(taskCompletionSource).ResultType;
public static object GetTaskResult(Task task) 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 = GetTaskResultType(taskInstanceType);
{ return resultType == null
var resultType = taskType.GetGenericArguments().Single(); ? new VoidTaskResultGetter()
return (ITaskResultGetter)Activator.CreateInstance( : (ITaskResultGetter)Activator.CreateInstance(
typeof(TaskResultGetter<>).MakeGenericType(resultType)); typeof(TaskResultGetter<>).MakeGenericType(resultType));
}
else
{
return new VoidTaskResultGetter();
}
}); });
return getter.GetResult(task); return getter.GetResult(task);
} }
private static Type GetTaskResultType(Type taskType)
{
// It might be something derived from Task or Task<T>, so we have to scan
// up the inheritance hierarchy to find the Task or Task<T>
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 interface ITcsResultSetter
{ {
Type ResultType { get; }
void SetResult(object taskCompletionSource, object result); void SetResult(object taskCompletionSource, object result);
void SetException(object taskCompletionSource, Exception exception); void SetException(object taskCompletionSource, Exception exception);
} }
@ -67,10 +82,18 @@ namespace Microsoft.JSInterop
private class TcsResultSetter<T> : ITcsResultSetter private class TcsResultSetter<T> : ITcsResultSetter
{ {
public Type ResultType => typeof(T);
public void SetResult(object tcs, object result) public void SetResult(object tcs, object result)
{ {
var typedTcs = (TaskCompletionSource<T>)tcs; var typedTcs = (TaskCompletionSource<T>)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) public void SetException(object tcs, Exception exception)

View File

@ -27,6 +27,32 @@ namespace Mono.WebAssembly.Interop
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson); 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 #region Custom MonoWebAssemblyJSRuntime methods
/// <summary> /// <summary>

View File

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading;
using BasicTestApp; using BasicTestApp;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
@ -33,45 +31,63 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
["VoidParameterless"] = "[]", ["VoidParameterless"] = "[]",
["VoidWithOneParameter"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["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]", ["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]", ["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,8,16]", ["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,10,20,40]", ["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,12,24,48,6.25]", ["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,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", ["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,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}]", ["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"] = "[]", ["VoidParameterlessAsync"] = "[]",
["VoidWithOneParameterAsync"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["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]", ["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]", ["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,8,16]", ["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,10,20,40]", ["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,12,24,48,6.25]", ["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,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", ["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,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}]", ["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]", ["result1"] = @"[0.1,0.2]",
["result2"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["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]", ["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]", ["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,8,16]", ["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,10,20,40]", ["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,12,24,48,6.25]", ["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,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", ["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,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}]", ["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]", ["result1Async"] = @"[0.1,0.2]",
["result2Async"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]", ["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]", ["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]", ["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,8,16]", ["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,10,20,40]", ["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,12,24,48,6.25]", ["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,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]", ["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,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}]", ["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!", ["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!",
["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!", ["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!",
["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!", ["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!",
["ExceptionFromSyncMethod"] = "Function threw an exception!", ["ExceptionFromSyncMethod"] = "Function threw an exception!",
["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!", ["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!",
["AsyncExceptionFromAsyncMethod"] = "Function threw an async 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<string, string>(); var actualValues = new Dictionary<string, string>();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Xunit; using Xunit;
namespace Microsoft.JSInterop.Test namespace Microsoft.JSInterop.Test
@ -11,13 +12,15 @@ namespace Microsoft.JSInterop.Test
{ {
private readonly static string thisAssemblyName private readonly static string thisAssemblyName
= typeof(DotNetDispatcherTest).Assembly.GetName().Name; = typeof(DotNetDispatcherTest).Assembly.GetName().Name;
private readonly TestJSRuntime jsRuntime
= new TestJSRuntime();
[Fact] [Fact]
public void CannotInvokeWithEmptyAssemblyName() public void CannotInvokeWithEmptyAssemblyName()
{ {
var ex = Assert.Throws<ArgumentException>(() => var ex = Assert.Throws<ArgumentException>(() =>
{ {
DotNetDispatcher.Invoke(" ", "SomeMethod", "[]"); DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]");
}); });
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
@ -29,7 +32,7 @@ namespace Microsoft.JSInterop.Test
{ {
var ex = Assert.Throws<ArgumentException>(() => var ex = Assert.Throws<ArgumentException>(() =>
{ {
DotNetDispatcher.Invoke("SomeAssembly", " ", "[]"); DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]");
}); });
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
@ -42,75 +45,172 @@ namespace Microsoft.JSInterop.Test
var assemblyName = "Some.Fake.Assembly"; var assemblyName = "Some.Fake.Assembly";
var ex = Assert.Throws<ArgumentException>(() => var ex = Assert.Throws<ArgumentException>(() =>
{ {
DotNetDispatcher.Invoke(assemblyName, "SomeMethod", null); DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null);
}); });
Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); 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 // 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 // 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 // fine (an exception stating what info is missing).
// for invoking instance methods in the near future.
[Theory] [Theory]
[InlineData("MethodOnInternalType")] [InlineData("MethodOnInternalType")]
[InlineData("PrivateMethod")] [InlineData("PrivateMethod")]
[InlineData("ProtectedMethod")] [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) public void CannotInvokeUnsuitableMethods(string methodIdentifier)
{ {
var ex = Assert.Throws<ArgumentException>(() => var ex = Assert.Throws<ArgumentException>(() =>
{ {
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); Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
} }
[Fact] [Fact]
public void CanInvokeStaticVoidMethod() public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
{ {
// Arrange/Act // Arrange/Act
SomePublicType.DidInvokeMyInvocableVoid = false; SomePublicType.DidInvokeMyInvocableStaticVoid = false;
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", null); var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null);
// Assert // Assert
Assert.Null(resultJson); Assert.Null(resultJson);
Assert.True(SomePublicType.DidInvokeMyInvocableVoid); Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
} });
[Fact] [Fact]
public void CanInvokeStaticNonVoidMethod() public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime =>
{ {
// Arrange/Act // Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", null); var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
var result = Json.Deserialize<TestDTO>(resultJson); var result = Json.Deserialize<TestDTO>(resultJson);
// Assert // Assert
Assert.Equal("Test", result.StringVal); Assert.Equal("Test", result.StringVal);
Assert.Equal(123, result.IntVal); Assert.Equal(123, result.IntVal);
} });
[Fact] [Fact]
public void CanInvokeStaticWithParams() public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime =>
{ {
// Arrange // Arrange/Act
var argsJson = Json.Serialize(new object[] { var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
new TestDTO { StringVal = "Another string", IntVal = 456 },
new[] { 100, 200 }
});
// Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", argsJson);
var result = Json.Deserialize<TestDTO>(resultJson); var result = Json.Deserialize<TestDTO>(resultJson);
// Assert // Assert
Assert.Equal("ANOTHER STRING", result.StringVal); Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
Assert.Equal(756, result.IntVal); 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<object>("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<object[]>(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<object>("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<object>("unimportant", objectRef);
objectRef.Dispose();
// Act/Assert
var ex = Assert.Throws<ArgumentException>(
() => 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<object>("unimportant", objectRef);
DotNetDispatcher.ReleaseDotNetObject(1);
// Act/Assert
var ex = Assert.Throws<ArgumentException>(
() => 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<object>("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] [Fact]
public void CannotInvokeWithIncorrectNumberOfParams() public void CannotInvokeWithIncorrectNumberOfParams()
@ -121,10 +221,73 @@ namespace Microsoft.JSInterop.Test
// Act/Assert // Act/Assert
var ex = Assert.Throws<ArgumentException>(() => var ex = Assert.Throws<ArgumentException>(() =>
{ {
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<object>("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<SimpleJson.JsonArray>(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<TestJSRuntime> testCode)
{
return WithJSRuntime(jsRuntime =>
{
testCode(jsRuntime);
return Task.CompletedTask;
});
}
async Task WithJSRuntime(Func<TestJSRuntime, Task> 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 internal class SomeInteralType
@ -134,15 +297,17 @@ namespace Microsoft.JSInterop.Test
public class SomePublicType public class SomePublicType
{ {
public static bool DidInvokeMyInvocableVoid; public static bool DidInvokeMyInvocableStaticVoid;
public bool DidInvokeMyInvocableInstanceVoid;
[JSInvokable("PrivateMethod")] private void MyPrivateMethod() { } [JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { }
[JSInvokable("ProtectedMethod")] protected void MyProtectedMethod() { } [JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { }
protected void MethodWithoutAttribute() { } protected static void StaticMethodWithoutAttribute() { }
protected static void InstanceMethodWithoutAttribute() { }
[JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid() [JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid()
{ {
DidInvokeMyInvocableVoid = true; DidInvokeMyInvocableStaticVoid = true;
} }
[JSInvokable("InvocableStaticNonVoid")] [JSInvokable("InvocableStaticNonVoid")]
@ -150,8 +315,65 @@ namespace Microsoft.JSInterop.Test
=> new TestDTO { StringVal = "Test", IntVal = 123 }; => new TestDTO { StringVal = "Test", IntVal = 123 };
[JSInvokable("InvocableStaticWithParams")] [JSInvokable("InvocableStaticWithParams")]
public static TestDTO MyInvocableWithParams(TestDTO dto, int[] incrementAmounts) public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
=> new TestDTO { StringVal = dto.StringVal.ToUpperInvariant(), IntVal = dto.IntVal + incrementAmounts.Sum() }; => 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<object[]> 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 public class TestDTO
@ -159,5 +381,33 @@ namespace Microsoft.JSInterop.Test
public string StringVal { get; set; } public string StringVal { get; set; }
public int IntVal { get; set; } public int IntVal { get; set; }
} }
public class TestJSRuntime : JSInProcessRuntimeBase
{
private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
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<object>();
}
protected override string InvokeJS(string identifier, string argsJson)
{
LastInvocationAsyncHandle = default;
LastInvocationIdentifier = identifier;
LastInvocationArgsJson = argsJson;
_nextInvocationTcs.SetResult(null);
_nextInvocationTcs = new TaskCompletionSource<object>();
return null;
}
}
} }
} }

View File

@ -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<InvalidOperationException>(
() => 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<DotNetObjectRef> UntrackedRefs = new List<DotNetObjectRef>();
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
=> throw new NotImplementedException();
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> UntrackedRefs.Add(dotNetObjectRef);
}
}
}

View File

@ -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<TestDTO>("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<object>("test identifier",
new DotNetObjectRef(obj1),
new Dictionary<string, object>
{
{ "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<object[]>("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<InvokeArgs> InvokeCalls { get; set; } = new List<InvokeArgs>();
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");
}
}
}

View File

@ -1,8 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Xunit; using Xunit;
namespace Microsoft.JSInterop.Test namespace Microsoft.JSInterop.Test
@ -98,7 +100,57 @@ namespace Microsoft.JSInterop.Test
}); });
Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message); 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<object>("test identifier",
obj1Ref,
new Dictionary<string, object>
{
{ "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<object>("test identifier",
new WithCustomArgSerializer());
// Asssert
var call = runtime.BeginInvokeCalls.Single();
Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson);
}
class TestJSRuntime : JSRuntimeBase class TestJSRuntime : JSRuntimeBase
{ {
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>(); public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
@ -123,5 +175,17 @@ namespace Microsoft.JSInterop.Test
public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException) public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException)
=> EndInvokeJS(asyncHandle, succeeded, resultOrException); => EndInvokeJS(asyncHandle, succeeded, resultOrException);
} }
class WithCustomArgSerializer : ICustomArgSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 123 },
};
}
}
} }
} }

View File

@ -29,6 +29,9 @@ namespace Microsoft.JSInterop.Test
{ {
public Task<T> InvokeAsync<T>(string identifier, params object[] args) public Task<T> InvokeAsync<T>(string identifier, params object[] args)
=> throw new NotImplementedException(); => throw new NotImplementedException();
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> throw new NotImplementedException();
} }
} }
} }

View File

@ -191,16 +191,6 @@ namespace Microsoft.JSInterop.Test
exception.Message); 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 // 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, // 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 // 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 } enum Hobbies { Reading = 1, Swordfighting = 2 }
class WithCustomSerializer : ICustomJsonSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 123 },
};
}
}
#pragma warning disable 0649 #pragma warning disable 0649
class ClashingProperties class ClashingProperties
{ {

View File

@ -12,6 +12,20 @@
} }
</div> </div>
<div>
<h1>.NET to JS calls: passing .NET object by ref, receiving .NET object by ref</h1>
@foreach (var kvp in ReceiveDotNetObjectByRefResult)
{
<h2>@(kvp.Key)Sync</h2>
<p id="@(kvp.Key)Sync">@kvp.Value</p>
}
@foreach (var kvp in ReceiveDotNetObjectByRefAsyncResult)
{
<h2>@(kvp.Key)Async</h2>
<p id="@(kvp.Key)Async">@kvp.Value</p>
}
</div>
<div> <div>
<h1>Return values and exceptions thrown from .NET</h1> <h1>Return values and exceptions thrown from .NET</h1>
@foreach (var returnValue in ReturnValues) @foreach (var returnValue in ReturnValues)
@ -44,14 +58,23 @@
public JSException SyncExceptionFromAsyncMethod { get; set; } public JSException SyncExceptionFromAsyncMethod { get; set; }
public JSException AsyncExceptionFromAsyncMethod { get; set; } public JSException AsyncExceptionFromAsyncMethod { get; set; }
public IDictionary<string, object> ReceiveDotNetObjectByRefResult { get; set; } = new Dictionary<string, object>();
public IDictionary<string, object> ReceiveDotNetObjectByRefAsyncResult { get; set; } = new Dictionary<string, object>();
public bool DoneWithInterop { get; set; } public bool DoneWithInterop { get; set; }
public async Task InvokeInteropAsync() public async Task InvokeInteropAsync()
{ {
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime.Current); var inProcRuntime = ((IJSInProcessRuntime)JSRuntime.Current);
var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123);
var instanceMethodsTarget = new JavaScriptInterop();
Console.WriteLine("Starting interop invocations."); Console.WriteLine("Starting interop invocations.");
await JSRuntime.Current.InvokeAsync<object>("jsInteropTests.invokeDotNetInteropMethodsAsync"); await JSRuntime.Current.InvokeAsync<object>(
"jsInteropTests.invokeDotNetInteropMethodsAsync",
new DotNetObjectRef(testDTOTOPassByRef),
new DotNetObjectRef(instanceMethodsTarget));
Console.WriteLine("Showing interop invocation results."); Console.WriteLine("Showing interop invocation results.");
var collectResults = inProcRuntime.Invoke<Dictionary<string,string>>("jsInteropTests.collectInteropResults"); var collectResults = inProcRuntime.Invoke<Dictionary<string,string>>("jsInteropTests.collectInteropResults");
@ -91,6 +114,20 @@
AsyncExceptionFromAsyncMethod = e; AsyncExceptionFromAsyncMethod = e;
} }
var passDotNetObjectByRef = new TestDTO(99999);
var passDotNetObjectByRefArg = new Dictionary<string, object>
{
{ "stringValue", "My string" },
{ "testDto", new DotNetObjectRef(passDotNetObjectByRef) },
};
ReceiveDotNetObjectByRefResult = inProcRuntime.Invoke<Dictionary<string, object>>("receiveDotNetObjectByRef", passDotNetObjectByRefArg);
ReceiveDotNetObjectByRefAsyncResult = await JSRuntime.Current.InvokeAsync<Dictionary<string, object>>("receiveDotNetObjectByRefAsync", passDotNetObjectByRefArg);
ReceiveDotNetObjectByRefResult["testDto"] = ReceiveDotNetObjectByRefResult["testDto"] == passDotNetObjectByRef ? "Same" : "Different";
ReceiveDotNetObjectByRefAsyncResult["testDto"] = ReceiveDotNetObjectByRefAsyncResult["testDto"] == passDotNetObjectByRef ? "Same" : "Different";
ReturnValues["returnPrimitive"] = inProcRuntime.Invoke<int>("returnPrimitive").ToString();
ReturnValues["returnPrimitiveAsync"] = (await JSRuntime.Current.InvokeAsync<int>("returnPrimitiveAsync")).ToString();
Invocations = invocations; Invocations = invocations;
DoneWithInterop = true; DoneWithInterop = true;
} }

View File

@ -4,7 +4,6 @@
using Microsoft.JSInterop; using Microsoft.JSInterop;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace BasicTestApp.InteropTest namespace BasicTestApp.InteropTest
@ -13,33 +12,33 @@ namespace BasicTestApp.InteropTest
{ {
public static IDictionary<string, object[]> Invocations = new Dictionary<string, object[]>(); public static IDictionary<string, object[]> Invocations = new Dictionary<string, object[]>();
[JSInvokable(nameof(ThrowException))] [JSInvokable]
public static void ThrowException() => throw new InvalidOperationException("Threw an exception!"); public static void ThrowException() => throw new InvalidOperationException("Threw an exception!");
[JSInvokable(nameof(AsyncThrowSyncException))] [JSInvokable]
public static Task AsyncThrowSyncException() public static Task AsyncThrowSyncException()
=> throw new InvalidOperationException("Threw a sync exception!"); => throw new InvalidOperationException("Threw a sync exception!");
[JSInvokable(nameof(AsyncThrowAsyncException))] [JSInvokable]
public static async Task AsyncThrowAsyncException() public static async Task AsyncThrowAsyncException()
{ {
await Task.Yield(); await Task.Yield();
throw new InvalidOperationException("Threw an async exception!"); throw new InvalidOperationException("Threw an async exception!");
} }
[JSInvokable(nameof(VoidParameterless))] [JSInvokable]
public static void VoidParameterless() public static void VoidParameterless()
{ {
Invocations[nameof(VoidParameterless)] = new object[0]; Invocations[nameof(VoidParameterless)] = new object[0];
} }
[JSInvokable(nameof(VoidWithOneParameter))] [JSInvokable]
public static void VoidWithOneParameter(ComplexParameter parameter1) public static void VoidWithOneParameter(ComplexParameter parameter1)
{ {
Invocations[nameof(VoidWithOneParameter)] = new object[] { parameter1 }; Invocations[nameof(VoidWithOneParameter)] = new object[] { parameter1 };
} }
[JSInvokable(nameof(VoidWithTwoParameters))] [JSInvokable]
public static void VoidWithTwoParameters( public static void VoidWithTwoParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2) byte parameter2)
@ -47,88 +46,88 @@ namespace BasicTestApp.InteropTest
Invocations[nameof(VoidWithTwoParameters)] = new object[] { parameter1, parameter2 }; Invocations[nameof(VoidWithTwoParameters)] = new object[] { parameter1, parameter2 };
} }
[JSInvokable(nameof(VoidWithThreeParameters))] [JSInvokable]
public static void VoidWithThreeParameters( public static void VoidWithThreeParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, 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( public static void VoidWithFourParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4) 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( public static void VoidWithFiveParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5) 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( public static void VoidWithSixParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6) 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( public static void VoidWithSevenParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7) List<double> 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( public static void VoidWithEightParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7, List<double> parameter7,
Segment parameter8) 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() public static decimal[] ReturnArray()
{ {
return new decimal[] { 0.1M, 0.2M }; return new decimal[] { 0.1M, 0.2M };
} }
[JSInvokable(nameof(EchoOneParameter))] [JSInvokable]
public static object[] EchoOneParameter(ComplexParameter parameter1) public static object[] EchoOneParameter(ComplexParameter parameter1)
{ {
return new object[] { parameter1 }; return new object[] { parameter1 };
} }
[JSInvokable(nameof(EchoTwoParameters))] [JSInvokable]
public static object[] EchoTwoParameters( public static object[] EchoTwoParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2) byte parameter2)
@ -136,88 +135,88 @@ namespace BasicTestApp.InteropTest
return new object[] { parameter1, parameter2 }; return new object[] { parameter1, parameter2 };
} }
[JSInvokable(nameof(EchoThreeParameters))] [JSInvokable]
public static object[] EchoThreeParameters( public static object[] EchoThreeParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, 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( public static object[] EchoFourParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4) 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( public static object[] EchoFiveParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5) 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, public static object[] EchoSixParameters(ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6) 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, public static object[] EchoSevenParameters(ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7) List<double> 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( public static object[] EchoEightParameters(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7, List<double> parameter7,
Segment parameter8) 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() public static Task VoidParameterlessAsync()
{ {
Invocations[nameof(VoidParameterlessAsync)] = new object[0]; Invocations[nameof(VoidParameterlessAsync)] = new object[0];
return Task.CompletedTask; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithOneParameterAsync))] [JSInvokable]
public static Task VoidWithOneParameterAsync(ComplexParameter parameter1) public static Task VoidWithOneParameterAsync(ComplexParameter parameter1)
{ {
Invocations[nameof(VoidWithOneParameterAsync)] = new object[] { parameter1 }; Invocations[nameof(VoidWithOneParameterAsync)] = new object[] { parameter1 };
return Task.CompletedTask; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithTwoParametersAsync))] [JSInvokable]
public static Task VoidWithTwoParametersAsync( public static Task VoidWithTwoParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2) byte parameter2)
@ -226,94 +225,94 @@ namespace BasicTestApp.InteropTest
return Task.CompletedTask; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithThreeParametersAsync))] [JSInvokable]
public static Task VoidWithThreeParametersAsync( public static Task VoidWithThreeParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, 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; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithFourParametersAsync))] [JSInvokable]
public static Task VoidWithFourParametersAsync( public static Task VoidWithFourParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4) int parameter4)
{ {
Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4 }; Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 };
return Task.CompletedTask; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithFiveParametersAsync))] [JSInvokable]
public static Task VoidWithFiveParametersAsync( public static Task VoidWithFiveParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5) 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; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithSixParametersAsync))] [JSInvokable]
public static Task VoidWithSixParametersAsync( public static Task VoidWithSixParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6) 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; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithSevenParametersAsync))] [JSInvokable]
public static Task VoidWithSevenParametersAsync( public static Task VoidWithSevenParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7) List<double> 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; return Task.CompletedTask;
} }
[JSInvokable(nameof(VoidWithEightParametersAsync))] [JSInvokable]
public static Task VoidWithEightParametersAsync( public static Task VoidWithEightParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7, List<double> parameter7,
Segment parameter8) 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; return Task.CompletedTask;
} }
[JSInvokable(nameof(ReturnArrayAsync))] [JSInvokable]
public static Task<decimal[]> ReturnArrayAsync() public static Task<decimal[]> ReturnArrayAsync()
{ {
return Task.FromResult(new decimal[] { 0.1M, 0.2M }); return Task.FromResult(new decimal[] { 0.1M, 0.2M });
} }
[JSInvokable(nameof(EchoOneParameterAsync))] [JSInvokable]
public static Task<object[]> EchoOneParameterAsync(ComplexParameter parameter1) public static Task<object[]> EchoOneParameterAsync(ComplexParameter parameter1)
{ {
return Task.FromResult(new object[] { parameter1 }); return Task.FromResult(new object[] { parameter1 });
} }
[JSInvokable(nameof(EchoTwoParametersAsync))] [JSInvokable]
public static Task<object[]> EchoTwoParametersAsync( public static Task<object[]> EchoTwoParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2) byte parameter2)
@ -321,72 +320,128 @@ namespace BasicTestApp.InteropTest
return Task.FromResult(new object[] { parameter1, parameter2 }); return Task.FromResult(new object[] { parameter1, parameter2 });
} }
[JSInvokable(nameof(EchoThreeParametersAsync))] [JSInvokable]
public static Task<object[]> EchoThreeParametersAsync( public static Task<object[]> EchoThreeParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, 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<object[]> EchoFourParametersAsync( public static Task<object[]> EchoFourParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4) 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<object[]> EchoFiveParametersAsync( public static Task<object[]> EchoFiveParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5) 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<object[]> EchoSixParametersAsync(ComplexParameter parameter1, public static Task<object[]> EchoSixParametersAsync(ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6) 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<object[]> EchoSevenParametersAsync( public static Task<object[]> EchoSevenParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7) List<double> 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<object[]> EchoEightParametersAsync( public static Task<object[]> EchoEightParametersAsync(
ComplexParameter parameter1, ComplexParameter parameter1,
byte parameter2, byte parameter2,
short parameter3, TestDTO parameter3,
int parameter4, int parameter4,
long parameter5, long parameter5,
float parameter6, float parameter6,
List<double> parameter7, List<double> parameter7,
Segment parameter8) 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<string, object> ReturnDotNetObjectByRef()
{
return new Dictionary<string, object>
{
{ "Some sync instance", new DotNetObjectRef(new TestDTO(1000)) }
};
}
[JSInvokable]
public static async Task<Dictionary<string, object>> ReturnDotNetObjectByRefAsync()
{
await Task.Yield();
return new Dictionary<string, object>
{
{ "Some async instance", new DotNetObjectRef(new TestDTO(1001)) }
};
}
[JSInvokable]
public static int ExtractNonSerializedValue(TestDTO objectByRef)
{
return objectByRef.GetNonSerializedValue();
}
[JSInvokable]
public Dictionary<string, object> InstanceMethod(Dictionary<string, object> 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<string, object>
{
{ "thisTypeName", GetType().Name },
{ "stringValueUpper", ((string)dict["stringValue"]).ToUpperInvariant() },
{ "incomingByRef", ((TestDTO)dict["dtoByRef"]).GetNonSerializedValue() },
{ "outgoingByRef", new DotNetObjectRef(new TestDTO(1234)) },
};
}
[JSInvokable]
public async Task<Dictionary<string, object>> InstanceMethodAsync(Dictionary<string, object> 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<string, object>
{
{ "thisTypeName", GetType().Name },
{ "stringValueUpper", ((string)dict["stringValue"]).ToUpperInvariant() },
{ "incomingByRef", ((TestDTO)dict["dtoByRef"]).GetNonSerializedValue() },
{ "outgoingByRef", new DotNetObjectRef(new TestDTO(1234)) },
};
} }
} }
} }

View File

@ -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;
}
}

View File

@ -4,59 +4,85 @@
var results = {}; var results = {};
var assemblyName = 'BasicTestApp'; var assemblyName = 'BasicTestApp';
function invokeDotNetInteropMethodsAsync() { function invokeDotNetInteropMethodsAsync(dotNetObjectByRef, instanceMethodsTarget) {
console.log('Invoking void sync methods.'); console.log('Invoking void sync methods.');
DotNet.invokeMethod(assemblyName, 'VoidParameterless'); DotNet.invokeMethod(assemblyName, 'VoidParameterless');
DotNet.invokeMethod(assemblyName, 'VoidWithOneParameter', ...createArgumentList(1)); DotNet.invokeMethod(assemblyName, 'VoidWithOneParameter', ...createArgumentList(1, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithTwoParameters', ...createArgumentList(2)); DotNet.invokeMethod(assemblyName, 'VoidWithTwoParameters', ...createArgumentList(2, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithThreeParameters', ...createArgumentList(3)); DotNet.invokeMethod(assemblyName, 'VoidWithThreeParameters', ...createArgumentList(3, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithFourParameters', ...createArgumentList(4)); DotNet.invokeMethod(assemblyName, 'VoidWithFourParameters', ...createArgumentList(4, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithFiveParameters', ...createArgumentList(5)); DotNet.invokeMethod(assemblyName, 'VoidWithFiveParameters', ...createArgumentList(5, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithSixParameters', ...createArgumentList(6)); DotNet.invokeMethod(assemblyName, 'VoidWithSixParameters', ...createArgumentList(6, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7)); DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8)); DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8, dotNetObjectByRef));
console.log('Invoking returning sync methods.'); console.log('Invoking returning sync methods.');
results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray'); results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray');
results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1)); results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1, dotNetObjectByRef));
results['result3'] = DotNet.invokeMethod(assemblyName, 'EchoTwoParameters', ...createArgumentList(2)); results['result3'] = DotNet.invokeMethod(assemblyName, 'EchoTwoParameters', ...createArgumentList(2, dotNetObjectByRef));
results['result4'] = DotNet.invokeMethod(assemblyName, 'EchoThreeParameters', ...createArgumentList(3)); results['result4'] = DotNet.invokeMethod(assemblyName, 'EchoThreeParameters', ...createArgumentList(3, dotNetObjectByRef));
results['result5'] = DotNet.invokeMethod(assemblyName, 'EchoFourParameters', ...createArgumentList(4)); results['result5'] = DotNet.invokeMethod(assemblyName, 'EchoFourParameters', ...createArgumentList(4, dotNetObjectByRef));
results['result6'] = DotNet.invokeMethod(assemblyName, 'EchoFiveParameters', ...createArgumentList(5)); results['result6'] = DotNet.invokeMethod(assemblyName, 'EchoFiveParameters', ...createArgumentList(5, dotNetObjectByRef));
results['result7'] = DotNet.invokeMethod(assemblyName, 'EchoSixParameters', ...createArgumentList(6)); results['result7'] = DotNet.invokeMethod(assemblyName, 'EchoSixParameters', ...createArgumentList(6, dotNetObjectByRef));
results['result8'] = DotNet.invokeMethod(assemblyName, 'EchoSevenParameters', ...createArgumentList(7)); results['result8'] = DotNet.invokeMethod(assemblyName, 'EchoSevenParameters', ...createArgumentList(7, dotNetObjectByRef));
results['result9'] = DotNet.invokeMethod(assemblyName, 'EchoEightParameters', ...createArgumentList(8)); 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.'); console.log('Invoking void async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync') return DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync')
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(() => { .then(() => {
console.log('Invoking returning async methods.'); console.log('Invoking returning async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync') return DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync')
.then(r => results['result1Async'] = r) .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(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(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(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(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(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(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(r => results['result8Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8))) .then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(r => results['result9Async'] = r); .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(() => { .then(() => {
console.log('Invoking methods that throw exceptions'); 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); const array = new Array(argumentNumber);
if (argumentNumber === 0) { if (argumentNumber === 0) {
return []; return [];
@ -101,7 +127,7 @@ function createArgumentList(argumentNumber){
array[i] = argumentNumber; array[i] = argumentNumber;
break; break;
case 2: case 2:
array[i] = argumentNumber * 2; array[i] = dotNetObjectByRef;
break; break;
case 3: case 3:
array[i] = argumentNumber * 4; array[i] = argumentNumber * 4;
@ -136,9 +162,25 @@ window.jsInteropTests = {
collectInteropResults: collectInteropResults, collectInteropResults: collectInteropResults,
functionThrowsException: functionThrowsException, functionThrowsException: functionThrowsException,
asyncFunctionThrowsSyncException: asyncFunctionThrowsSyncException, 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() { function functionThrowsException() {
throw new Error('Function threw an exception!'); throw new Error('Function threw an exception!');
} }
@ -163,3 +205,29 @@ function collectInteropResults() {
return result; 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);
});
}