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()
.then(async () => {
DotNet.attachDispatcher({
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, argsJson) => {
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, argsJson);
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
}
});

View File

@ -292,27 +292,34 @@ function getArrayDataPointer<T>(array: System_Array<T>): number {
}
function attachInteropInvoker() {
const dotNetDispatcherInvokeMethodHandle = findMethod('Microsoft.JSInterop', 'Microsoft.JSInterop', 'DotNetDispatcher', 'Invoke');
const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Microsoft.JSInterop', 'Microsoft.JSInterop', 'DotNetDispatcher', 'BeginInvoke');
const dotNetDispatcherInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'InvokeDotNet');
const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet');
DotNet.attachDispatcher({
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, argsJson) => {
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
// As a current limitation, we can only pass 4 args. Fortunately we only need one of
// 'assemblyName' or 'dotNetObjectId', so overload them in a single slot
const assemblyNameOrDotNetObjectId = dotNetObjectId
? dotNetObjectId.toString()
: assemblyName;
monoPlatform.callMethod(dotNetDispatcherBeginInvokeMethodHandle, null, [
callId ? monoPlatform.toDotNetString(callId.toString()) : null,
monoPlatform.toDotNetString(assemblyName),
monoPlatform.toDotNetString(assemblyNameOrDotNetObjectId!),
monoPlatform.toDotNetString(methodIdentifier),
monoPlatform.toDotNetString(argsJson)
]);
},
invokeDotNetFromJS: (assemblyName, methodIdentifier, argsJson) => {
invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
const resultJsonStringPtr = monoPlatform.callMethod(dotNetDispatcherInvokeMethodHandle, null, [
monoPlatform.toDotNetString(assemblyName),
assemblyName ? monoPlatform.toDotNetString(assemblyName) : null,
monoPlatform.toDotNetString(methodIdentifier),
dotNetObjectId ? monoPlatform.toDotNetString(dotNetObjectId.toString()) : null,
monoPlatform.toDotNetString(argsJson)
]) as System_String;
return resultJsonStringPtr
? JSON.parse(monoPlatform.toJavaScriptString(resultJsonStringPtr))
? monoPlatform.toJavaScriptString(resultJsonStringPtr)
: null;
},
});

View File

@ -51,9 +51,9 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
CircuitHost = circuitHost;
}
public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, string argsJson)
public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
{
EnsureCircuitHost().BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, argsJson);
EnsureCircuitHost().BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
}
private async void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)

View File

@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
_isInitialized = true;
}
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, string argsJson)
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
{
AssertInitialized();
@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
{
SetCurrentCircuitHost(this);
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, argsJson);
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
});
}
catch (Exception ex)

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -23,17 +24,27 @@ namespace Microsoft.JSInterop
/// </summary>
/// <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="dotNetObjectId">For instance method calls, identifies the target object.</param>
/// <param name="argsJson">A JSON representation of the parameters.</param>
/// <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
// some way to dispatch calls here. The logic inside here is the thing that checks whether
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
// because there would be nobody to police that. This method *is* the police.
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, argsJson);
return syncResult == null ? null : Json.Serialize(syncResult);
// DotNetDispatcher only works with JSRuntimeBase instances.
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
var targetInstance = (object)null;
if (dotNetObjectId != default)
{
targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
}
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy);
}
/// <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="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="dotNetObjectId">For instance method calls, identifies the target object.</param>
/// <param name="argsJson">A JSON representation of the parameters.</param>
/// <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
// some way to dispatch calls here. The logic inside here is the thing that checks whether
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
// because there would be nobody to police that. This method *is* the police.
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, argsJson);
// DotNetDispatcher only works with JSRuntimeBase instances.
// If the developer wants to use a totally custom IJSRuntime, then their JS-side
// code has to implement its own way of returning async results.
var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current;
var targetInstance = dotNetObjectId == default
? null
: jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
// If there was no callId, the caller does not want to be notified about the result
if (callId != null)
@ -61,11 +82,6 @@ namespace Microsoft.JSInterop
var task = syncResult is Task syncResultTask ? syncResultTask : Task.FromResult(syncResult);
task.ContinueWith(completedTask =>
{
// DotNetDispatcher only works with JSRuntimeBase instances.
// If the developer wants to use a totally custom IJSRuntime, then their JS-side
// code has to implement its own way of returning async results.
var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current;
try
{
var result = TaskGenericsUtil.GetTaskResult(completedTask);
@ -80,8 +96,18 @@ namespace Microsoft.JSInterop
}
}
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, string argsJson)
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson)
{
if (targetInstance != null)
{
if (assemblyName != null)
{
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
}
assemblyName = targetInstance.GetType().Assembly.GetName().Name;
}
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier);
// There's no direct way to say we want to deserialize as an array with heterogenous
@ -101,16 +127,26 @@ namespace Microsoft.JSInterop
}
// Second, convert each supplied value to the type expected by the method
var serializerStrategy = SimpleJson.SimpleJson.CurrentJsonSerializerStrategy;
var runtime = (JSRuntimeBase)JSRuntime.Current;
var serializerStrategy = runtime.ArgSerializerStrategy;
for (var i = 0; i < suppliedArgsLength; i++)
{
suppliedArgs[i] = serializerStrategy.DeserializeObject(
suppliedArgs[i], parameterTypes[i]);
if (parameterTypes[i] == typeof(JSAsyncCallResult))
{
// For JS async call results, we have to defer the deserialization until
// later when we know what type it's meant to be deserialized as
suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]);
}
else
{
suppliedArgs[i] = serializerStrategy.DeserializeObject(
suppliedArgs[i], parameterTypes[i]);
}
}
try
{
return methodInfo.Invoke(null, suppliedArgs);
return methodInfo.Invoke(targetInstance, suppliedArgs);
}
catch (Exception ex)
{
@ -124,10 +160,28 @@ namespace Microsoft.JSInterop
/// </summary>
/// <param name="asyncHandle">The identifier for the function invocation.</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))]
public static void EndInvoke(long asyncHandle, bool succeeded, object resultOrException)
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, resultOrException);
public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result)
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException);
/// <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)
{
@ -156,14 +210,37 @@ namespace Microsoft.JSInterop
{
// TODO: Consider looking first for assembly-level attributes (i.e., if there are any,
// only use those) to avoid scanning, especially for framework assemblies.
return GetRequiredLoadedAssembly(assemblyName)
var result = new Dictionary<string, (MethodInfo, Type[])>();
var invokableMethods = GetRequiredLoadedAssembly(assemblyName)
.GetExportedTypes()
.SelectMany(type => type.GetMethods())
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false))
.ToDictionary(
method => method.GetCustomAttribute<JSInvokableAttribute>(false).Identifier,
method => (method, method.GetParameters().Select(p => p.ParameterType).ToArray())
);
.Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false));
foreach (var method in invokableMethods)
{
var identifier = method.GetCustomAttribute<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)

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
// API. Developers who want that would be better served by using a different JSON package
// instead. Also the perf implications of the ICustomJsonSerializer approach aren't ideal
// instead. Also the perf implications of the ICustomArgSerializer approach aren't ideal
// (it forces structs to be boxed, and returning a dictionary means lots more allocations
// and boxing of any value-typed properties).
/// <summary>
/// Internal. Intended for framework use only.
/// </summary>
public interface ICustomJsonSerializer
public interface ICustomArgSerializer
{
/// <summary>
/// Internal. Intended for framework use only.

View File

@ -18,5 +18,15 @@ namespace Microsoft.JSInterop
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
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>
public T Invoke<T>(string identifier, params object[] args)
{
var resultJson = InvokeJS(identifier, Json.Serialize(args));
return Json.Deserialize<T>(resultJson);
var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy));
return Json.Deserialize<T>(resultJson, ArgSerializerStrategy);
}
/// <summary>

View File

@ -16,11 +16,23 @@ namespace Microsoft.JSInterop
/// <summary>
/// Gets the identifier for the method. The identifier must be unique within the scope
/// of an assembly.
///
/// If not set, the identifier is taken from the name of the method. In this case the
/// method name must be unique within the assembly.
/// </summary>
public string Identifier { get; }
/// <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>
/// <param name="identifier">An identifier for the method, which must be unique within the scope of the assembly.</param>
public JSInvokableAttribute(string identifier)

View File

@ -17,6 +17,20 @@ namespace Microsoft.JSInterop
private readonly ConcurrentDictionary<long, object> _pendingTasks
= 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>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
@ -36,7 +50,10 @@ namespace Microsoft.JSInterop
try
{
BeginInvokeJS(taskId, identifier, args?.Length > 0 ? Json.Serialize(args) : null);
var argsJson = args?.Length > 0
? Json.Serialize(args, ArgSerializerStrategy)
: null;
BeginInvokeJS(taskId, identifier, argsJson);
return tcs.Task;
}
catch
@ -70,7 +87,7 @@ namespace Microsoft.JSInterop
callId,
success,
resultOrException
}));
}, ArgSerializerStrategy));
}
internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException)
@ -82,7 +99,11 @@ namespace Microsoft.JSInterop
if (succeeded)
{
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException);
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
var resultValue = resultOrException is SimpleJson.JsonObject
? ArgSerializerStrategy.DeserializeObject(resultOrException, resultType)
: resultOrException;
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultValue);
}
else
{

View File

@ -40,13 +40,7 @@ module DotNet {
* @returns The result of the operation.
*/
export function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T {
const dispatcher = getRequiredDispatcher();
if (dispatcher.invokeDotNetFromJS) {
const argsJson = JSON.stringify(args);
return dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, argsJson);
} else {
throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeAsync instead.');
}
return invokePossibleInstanceMethod<T>(assemblyName, methodIdentifier, null, args);
}
/**
@ -58,14 +52,29 @@ module DotNet {
* @returns A promise representing the result of the operation.
*/
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 resultPromise = new Promise<T>((resolve, reject) => {
pendingAsyncCalls[asyncCallId] = { resolve, reject };
});
try {
const argsJson = JSON.stringify(args);
getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, argsJson);
const argsJson = JSON.stringify(args, argReplacer);
getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
} catch(ex) {
// Synchronous failure
completePendingCall(asyncCallId, false, ex);
@ -108,22 +117,24 @@ module DotNet {
/**
* Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke.
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods.
* @param argsJson JSON representation of arguments to pass to the method.
* @returns The result of the invocation.
* @returns JSON representation of the result of the invocation.
*/
invokeDotNetFromJS?(assemblyName: string, methodIdentifier: string, argsJson: string): any;
invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): string | null;
/**
* Invoked by the runtime to begin an asynchronous call to a .NET method.
*
* @param callId A value identifying the asynchronous operation. This value should be passed back in a later call from .NET to JS.
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke.
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null to call static methods.
* @param argsJson JSON representation of arguments to pass to the method.
*/
beginInvokeDotNetFromJS(callId: number, assemblyName: string, methodIdentifier: string, argsJson: string): void;
beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void;
}
/**
@ -149,7 +160,7 @@ module DotNet {
const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
return result === null || result === undefined
? null
: JSON.stringify(result);
: JSON.stringify(result, argReplacer);
},
/**
@ -172,8 +183,8 @@ module DotNet {
// On completion, dispatch result back to .NET
// Not using "await" because it codegens a lot of boilerplate
promise.then(
result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', JSON.stringify([asyncHandle, true, result])),
error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', JSON.stringify([asyncHandle, false, formatError(error)]))
result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)),
error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)]))
);
}
},
@ -231,4 +242,46 @@ module DotNet {
throw new Error(`The value '${resultIdentifier}' is not a function.`);
}
}
class DotNetObject {
constructor(private _id: number) {
}
public invokeMethod<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)
=> SimpleJson.SimpleJson.SerializeObject(value);
internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy)
=> SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy);
/// <summary>
/// Deserializes the JSON string, creating an object of the specified generic type.
/// </summary>
@ -29,5 +32,8 @@ namespace Microsoft.JSInterop
/// <returns>An object of the specified type.</returns>
public static T Deserialize<T>(string 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.Text;
using Microsoft.JSInterop;
using Microsoft.JSInterop.Internal;
using SimpleJson.Reflection;
// ReSharper disable LoopCanBeConvertedToQuery
@ -1538,8 +1537,6 @@ namespace SimpleJson
output = input.ToString();
else if (input is TimeSpan)
output = ((TimeSpan)input).ToString("c");
else if (input is ICustomJsonSerializer customJsonSerializer)
output = customJsonSerializer.ToJsonPrimitive();
else
{
Enum inputEnum = input as Enum;

View File

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

View File

@ -27,6 +27,32 @@ namespace Mono.WebAssembly.Interop
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson);
}
// Invoked via Mono's JS interop mechanism (invoke_method)
private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
=> DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson);
// Invoked via Mono's JS interop mechanism (invoke_method)
private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
{
// Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID
// We only need one for any given call. This helps to work around the limitation that we can
// only pass a maximum of 4 args in a call from JS to Mono WebAssembly.
string assemblyName;
long dotNetObjectId;
if (char.IsDigit(assemblyNameOrDotNetObjectId[0]))
{
dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId);
assemblyName = null;
}
else
{
dotNetObjectId = default;
assemblyName = assemblyNameOrDotNetObjectId;
}
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
}
#region Custom MonoWebAssemblyJSRuntime methods
/// <summary>

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using BasicTestApp;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
@ -33,45 +31,63 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
["VoidParameterless"] = "[]",
["VoidWithOneParameter"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["VoidWithTwoParameters"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["VoidWithThreeParameters"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["VoidWithFourParameters"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["VoidWithFiveParameters"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["VoidWithSixParameters"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["VoidWithSevenParameters"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["VoidWithEightParameters"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["VoidWithThreeParameters"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]",
["VoidWithFourParameters"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]",
["VoidWithFiveParameters"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]",
["VoidWithSixParameters"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
["VoidWithSevenParameters"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["VoidWithEightParameters"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["VoidParameterlessAsync"] = "[]",
["VoidWithOneParameterAsync"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["VoidWithTwoParametersAsync"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["VoidWithThreeParametersAsync"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["VoidWithFourParametersAsync"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["VoidWithFiveParametersAsync"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["VoidWithSixParametersAsync"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["VoidWithSevenParametersAsync"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["VoidWithEightParametersAsync"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["VoidWithThreeParametersAsync"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]",
["VoidWithFourParametersAsync"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]",
["VoidWithFiveParametersAsync"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]",
["VoidWithSixParametersAsync"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
["VoidWithSevenParametersAsync"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["VoidWithEightParametersAsync"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["result1"] = @"[0.1,0.2]",
["result2"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["result3"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["result4"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["result5"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["result6"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["result4"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]",
["result5"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]",
["result6"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]",
["result7"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["result1Async"] = @"[0.1,0.2]",
["result2Async"] = @"[{""id"":1,""isValid"":false,""data"":{""source"":""Some random text with at least 1 characters"",""start"":1,""length"":1}}]",
["result3Async"] = @"[{""id"":2,""isValid"":true,""data"":{""source"":""Some random text with at least 2 characters"",""start"":2,""length"":2}},2]",
["result4Async"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,6]",
["result5Async"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,8,16]",
["result6Async"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,10,20,40]",
["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,12,24,48,6.25]",
["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,14,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,16,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["result4Async"] = @"[{""id"":3,""isValid"":false,""data"":{""source"":""Some random text with at least 3 characters"",""start"":3,""length"":3}},3,123]",
["result5Async"] = @"[{""id"":4,""isValid"":true,""data"":{""source"":""Some random text with at least 4 characters"",""start"":4,""length"":4}},4,123,16]",
["result6Async"] = @"[{""id"":5,""isValid"":false,""data"":{""source"":""Some random text with at least 5 characters"",""start"":5,""length"":5}},5,123,20,40]",
["result7Async"] = @"[{""id"":6,""isValid"":true,""data"":{""source"":""Some random text with at least 6 characters"",""start"":6,""length"":6}},6,123,24,48,6.25]",
["result8Async"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
["result9Async"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!",
["AsyncThrowSyncException"] = @"""System.InvalidOperationException: Threw a sync exception!",
["AsyncThrowAsyncException"] = @"""System.InvalidOperationException: Threw an async exception!",
["ExceptionFromSyncMethod"] = "Function threw an exception!",
["SyncExceptionFromAsyncMethod"] = "Function threw a sync exception!",
["AsyncExceptionFromAsyncMethod"] = "Function threw an async exception!",
["resultReturnDotNetObjectByRefSync"] = "1000",
["resultReturnDotNetObjectByRefAsync"] = "1001",
["instanceMethodThisTypeName"] = @"""JavaScriptInterop""",
["instanceMethodStringValueUpper"] = @"""MY STRING""",
["instanceMethodIncomingByRef"] = "123",
["instanceMethodOutgoingByRef"] = "1234",
["instanceMethodThisTypeNameAsync"] = @"""JavaScriptInterop""",
["instanceMethodStringValueUpperAsync"] = @"""MY STRING""",
["instanceMethodIncomingByRefAsync"] = "123",
["instanceMethodOutgoingByRefAsync"] = "1234",
["stringValueUpperSync"] = "MY STRING",
["testDtoNonSerializedValueSync"] = "99999",
["testDtoSync"] = "Same",
["stringValueUpperAsync"] = "MY STRING",
["testDtoNonSerializedValueAsync"] = "99999",
["testDtoAsync"] = "Same",
["returnPrimitive"] = "123",
["returnPrimitiveAsync"] = "123",
};
var actualValues = new Dictionary<string, string>();

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.JSInterop.Test
@ -11,13 +12,15 @@ namespace Microsoft.JSInterop.Test
{
private readonly static string thisAssemblyName
= typeof(DotNetDispatcherTest).Assembly.GetName().Name;
private readonly TestJSRuntime jsRuntime
= new TestJSRuntime();
[Fact]
public void CannotInvokeWithEmptyAssemblyName()
{
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke(" ", "SomeMethod", "[]");
DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]");
});
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
@ -29,7 +32,7 @@ namespace Microsoft.JSInterop.Test
{
var ex = Assert.Throws<ArgumentException>(() =>
{
DotNetDispatcher.Invoke("SomeAssembly", " ", "[]");
DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]");
});
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
@ -42,75 +45,172 @@ namespace Microsoft.JSInterop.Test
var assemblyName = "Some.Fake.Assembly";
var ex = Assert.Throws<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);
}
// Note: Currently it's also not possible to invoke instance or generic methods.
// Note: Currently it's also not possible to invoke generic methods.
// That's not something determined by DotNetDispatcher, but rather by the fact that we
// don't pass any 'target' or close over the generics in the reflection code.
// don't close over the generics in the reflection code.
// Not defining this behavior through unit tests because the default outcome is
// fine (an exception stating what info is missing), plus we're likely to add support
// for invoking instance methods in the near future.
// fine (an exception stating what info is missing).
[Theory]
[InlineData("MethodOnInternalType")]
[InlineData("PrivateMethod")]
[InlineData("ProtectedMethod")]
[InlineData("MethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
[InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
[InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
public void CannotInvokeUnsuitableMethods(string methodIdentifier)
{
var ex = Assert.Throws<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);
}
[Fact]
public void CanInvokeStaticVoidMethod()
public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
{
// Arrange/Act
SomePublicType.DidInvokeMyInvocableVoid = false;
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", null);
SomePublicType.DidInvokeMyInvocableStaticVoid = false;
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null);
// Assert
Assert.Null(resultJson);
Assert.True(SomePublicType.DidInvokeMyInvocableVoid);
}
Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
});
[Fact]
public void CanInvokeStaticNonVoidMethod()
public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime =>
{
// Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", null);
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
var result = Json.Deserialize<TestDTO>(resultJson);
// Assert
Assert.Equal("Test", result.StringVal);
Assert.Equal(123, result.IntVal);
}
});
[Fact]
public void CanInvokeStaticWithParams()
public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime =>
{
// Arrange
var argsJson = Json.Serialize(new object[] {
new TestDTO { StringVal = "Another string", IntVal = 456 },
new[] { 100, 200 }
});
// Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", argsJson);
// Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
var result = Json.Deserialize<TestDTO>(resultJson);
// Assert
Assert.Equal("ANOTHER STRING", result.StringVal);
Assert.Equal(756, result.IntVal);
}
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
Assert.Equal(456, result.IntVal);
});
[Fact]
public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime =>
{
// Arrange: Track a .NET object to use as an arg
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
jsRuntime.Invoke<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]
public void CannotInvokeWithIncorrectNumberOfParams()
@ -121,10 +221,73 @@ namespace Microsoft.JSInterop.Test
// Act/Assert
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
@ -134,15 +297,17 @@ namespace Microsoft.JSInterop.Test
public class SomePublicType
{
public static bool DidInvokeMyInvocableVoid;
public static bool DidInvokeMyInvocableStaticVoid;
public bool DidInvokeMyInvocableInstanceVoid;
[JSInvokable("PrivateMethod")] private void MyPrivateMethod() { }
[JSInvokable("ProtectedMethod")] protected void MyProtectedMethod() { }
protected void MethodWithoutAttribute() { }
[JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { }
[JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { }
protected static void StaticMethodWithoutAttribute() { }
protected static void InstanceMethodWithoutAttribute() { }
[JSInvokable("InvocableStaticVoid")] public static void MyInvocableVoid()
{
DidInvokeMyInvocableVoid = true;
DidInvokeMyInvocableStaticVoid = true;
}
[JSInvokable("InvocableStaticNonVoid")]
@ -150,8 +315,65 @@ namespace Microsoft.JSInterop.Test
=> new TestDTO { StringVal = "Test", IntVal = 123 };
[JSInvokable("InvocableStaticWithParams")]
public static TestDTO MyInvocableWithParams(TestDTO dto, int[] incrementAmounts)
=> new TestDTO { StringVal = dto.StringVal.ToUpperInvariant(), IntVal = dto.IntVal + incrementAmounts.Sum() };
public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
=> new object[]
{
new TestDTO // Return via JSON marshalling
{
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
},
new DotNetObjectRef(new TestDTO // Return by ref
{
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
IntVal = dtoByRef.IntVal + incrementAmounts.Sum()
})
};
[JSInvokable]
public static TestDTO InvokableMethodWithoutCustomIdentifier()
=> new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
[JSInvokable]
public void InvokableInstanceVoid()
{
DidInvokeMyInvocableInstanceVoid = true;
}
[JSInvokable]
public object[] InvokableInstanceMethod(string someString, TestDTO someDTO)
{
// Returning an array to make the point that object references
// can be embedded anywhere in the result
return new object[]
{
$"You passed {someString}",
new DotNetObjectRef(new TestDTO
{
IntVal = someDTO.IntVal + 1,
StringVal = someDTO.StringVal.ToUpperInvariant()
})
};
}
[JSInvokable]
public async Task<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
@ -159,5 +381,33 @@ namespace Microsoft.JSInterop.Test
public string StringVal { 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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.JSInterop.Test
@ -98,7 +100,57 @@ namespace Microsoft.JSInterop.Test
});
Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message);
}
[Fact]
public void SerializesDotNetObjectWrappersInKnownFormat()
{
// Arrange
var runtime = new TestJSRuntime();
var obj1 = new object();
var obj2 = new object();
var obj3 = new object();
// Act
// Showing we can pass the DotNetObject either as top-level args or nested
var obj1Ref = new DotNetObjectRef(obj1);
var obj1DifferentRef = new DotNetObjectRef(obj1);
runtime.InvokeAsync<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
{
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
@ -123,5 +175,17 @@ namespace Microsoft.JSInterop.Test
public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException)
=> EndInvokeJS(asyncHandle, succeeded, resultOrException);
}
class WithCustomArgSerializer : ICustomArgSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<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)
=> throw new NotImplementedException();
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> throw new NotImplementedException();
}
}
}

View File

@ -191,16 +191,6 @@ namespace Microsoft.JSInterop.Test
exception.Message);
}
[Fact]
public void SupportsInternalCustomSerializer()
{
// Arrange/Act
var json = Json.Serialize(new WithCustomSerializer());
// Asssert
Assert.Equal("{\"key1\":\"value1\",\"key2\":123}", json);
}
// Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41
// The only difference is that our logic doesn't have to handle space-separated words,
// because we're only use this for camelcasing .NET member names
@ -273,18 +263,6 @@ namespace Microsoft.JSInterop.Test
enum Hobbies { Reading = 1, Swordfighting = 2 }
class WithCustomSerializer : ICustomJsonSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 123 },
};
}
}
#pragma warning disable 0649
class ClashingProperties
{

View File

@ -12,6 +12,20 @@
}
</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>
<h1>Return values and exceptions thrown from .NET</h1>
@foreach (var returnValue in ReturnValues)
@ -44,14 +58,23 @@
public JSException SyncExceptionFromAsyncMethod { 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 async Task InvokeInteropAsync()
{
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime.Current);
var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123);
var instanceMethodsTarget = new JavaScriptInterop();
Console.WriteLine("Starting interop invocations.");
await JSRuntime.Current.InvokeAsync<object>("jsInteropTests.invokeDotNetInteropMethodsAsync");
await JSRuntime.Current.InvokeAsync<object>(
"jsInteropTests.invokeDotNetInteropMethodsAsync",
new DotNetObjectRef(testDTOTOPassByRef),
new DotNetObjectRef(instanceMethodsTarget));
Console.WriteLine("Showing interop invocation results.");
var collectResults = inProcRuntime.Invoke<Dictionary<string,string>>("jsInteropTests.collectInteropResults");
@ -91,6 +114,20 @@
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;
DoneWithInterop = true;
}

View File

@ -4,7 +4,6 @@
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace BasicTestApp.InteropTest
@ -13,33 +12,33 @@ namespace BasicTestApp.InteropTest
{
public static IDictionary<string, object[]> Invocations = new Dictionary<string, object[]>();
[JSInvokable(nameof(ThrowException))]
[JSInvokable]
public static void ThrowException() => throw new InvalidOperationException("Threw an exception!");
[JSInvokable(nameof(AsyncThrowSyncException))]
[JSInvokable]
public static Task AsyncThrowSyncException()
=> throw new InvalidOperationException("Threw a sync exception!");
[JSInvokable(nameof(AsyncThrowAsyncException))]
[JSInvokable]
public static async Task AsyncThrowAsyncException()
{
await Task.Yield();
throw new InvalidOperationException("Threw an async exception!");
}
[JSInvokable(nameof(VoidParameterless))]
[JSInvokable]
public static void VoidParameterless()
{
Invocations[nameof(VoidParameterless)] = new object[0];
}
[JSInvokable(nameof(VoidWithOneParameter))]
[JSInvokable]
public static void VoidWithOneParameter(ComplexParameter parameter1)
{
Invocations[nameof(VoidWithOneParameter)] = new object[] { parameter1 };
}
[JSInvokable(nameof(VoidWithTwoParameters))]
[JSInvokable]
public static void VoidWithTwoParameters(
ComplexParameter parameter1,
byte parameter2)
@ -47,88 +46,88 @@ namespace BasicTestApp.InteropTest
Invocations[nameof(VoidWithTwoParameters)] = new object[] { parameter1, parameter2 };
}
[JSInvokable(nameof(VoidWithThreeParameters))]
[JSInvokable]
public static void VoidWithThreeParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
TestDTO parameter3)
{
Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3 };
Invocations[nameof(VoidWithThreeParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() };
}
[JSInvokable(nameof(VoidWithFourParameters))]
[JSInvokable]
public static void VoidWithFourParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4)
{
Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4 };
Invocations[nameof(VoidWithFourParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 };
}
[JSInvokable(nameof(VoidWithFiveParameters))]
[JSInvokable]
public static void VoidWithFiveParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5)
{
Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
Invocations[nameof(VoidWithFiveParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 };
}
[JSInvokable(nameof(VoidWithSixParameters))]
[JSInvokable]
public static void VoidWithSixParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6)
{
Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
Invocations[nameof(VoidWithSixParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 };
}
[JSInvokable(nameof(VoidWithSevenParameters))]
[JSInvokable]
public static void VoidWithSevenParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<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(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
Invocations[nameof(VoidWithEightParameters)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 };
}
[JSInvokable(nameof(ReturnArray))]
[JSInvokable]
public static decimal[] ReturnArray()
{
return new decimal[] { 0.1M, 0.2M };
}
[JSInvokable(nameof(EchoOneParameter))]
[JSInvokable]
public static object[] EchoOneParameter(ComplexParameter parameter1)
{
return new object[] { parameter1 };
}
[JSInvokable(nameof(EchoTwoParameters))]
[JSInvokable]
public static object[] EchoTwoParameters(
ComplexParameter parameter1,
byte parameter2)
@ -136,88 +135,88 @@ namespace BasicTestApp.InteropTest
return new object[] { parameter1, parameter2 };
}
[JSInvokable(nameof(EchoThreeParameters))]
[JSInvokable]
public static object[] EchoThreeParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
TestDTO parameter3)
{
return new object[] { parameter1, parameter2, parameter3 };
return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() };
}
[JSInvokable(nameof(EchoFourParameters))]
[JSInvokable]
public static object[] EchoFourParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4)
{
return new object[] { parameter1, parameter2, parameter3, parameter4 };
return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 };
}
[JSInvokable(nameof(EchoFiveParameters))]
[JSInvokable]
public static object[] EchoFiveParameters(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 };
}
[JSInvokable(nameof(EchoSixParameters))]
[JSInvokable]
public static object[] EchoSixParameters(ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 };
}
[JSInvokable(nameof(EchoSevenParameters))]
[JSInvokable]
public static object[] EchoSevenParameters(ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<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(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
return new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
return new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 };
}
[JSInvokable(nameof(VoidParameterlessAsync))]
[JSInvokable]
public static Task VoidParameterlessAsync()
{
Invocations[nameof(VoidParameterlessAsync)] = new object[0];
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithOneParameterAsync))]
[JSInvokable]
public static Task VoidWithOneParameterAsync(ComplexParameter parameter1)
{
Invocations[nameof(VoidWithOneParameterAsync)] = new object[] { parameter1 };
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithTwoParametersAsync))]
[JSInvokable]
public static Task VoidWithTwoParametersAsync(
ComplexParameter parameter1,
byte parameter2)
@ -226,94 +225,94 @@ namespace BasicTestApp.InteropTest
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithThreeParametersAsync))]
[JSInvokable]
public static Task VoidWithThreeParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
TestDTO parameter3)
{
Invocations[nameof(VoidWithThreeParametersAsync)] = new object[] { parameter1, parameter2, parameter3 };
Invocations[nameof(VoidWithThreeParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() };
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithFourParametersAsync))]
[JSInvokable]
public static Task VoidWithFourParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4)
{
Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4 };
Invocations[nameof(VoidWithFourParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 };
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithFiveParametersAsync))]
[JSInvokable]
public static Task VoidWithFiveParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5)
{
Invocations[nameof(VoidWithFiveParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 };
Invocations[nameof(VoidWithFiveParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 };
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithSixParametersAsync))]
[JSInvokable]
public static Task VoidWithSixParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6)
{
Invocations[nameof(VoidWithSixParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 };
Invocations[nameof(VoidWithSixParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 };
return Task.CompletedTask;
}
[JSInvokable(nameof(VoidWithSevenParametersAsync))]
[JSInvokable]
public static Task VoidWithSevenParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<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;
}
[JSInvokable(nameof(VoidWithEightParametersAsync))]
[JSInvokable]
public static Task VoidWithEightParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
Invocations[nameof(VoidWithEightParametersAsync)] = new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 };
Invocations[nameof(VoidWithEightParametersAsync)] = new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 };
return Task.CompletedTask;
}
[JSInvokable(nameof(ReturnArrayAsync))]
[JSInvokable]
public static Task<decimal[]> ReturnArrayAsync()
{
return Task.FromResult(new decimal[] { 0.1M, 0.2M });
}
[JSInvokable(nameof(EchoOneParameterAsync))]
[JSInvokable]
public static Task<object[]> EchoOneParameterAsync(ComplexParameter parameter1)
{
return Task.FromResult(new object[] { parameter1 });
}
[JSInvokable(nameof(EchoTwoParametersAsync))]
[JSInvokable]
public static Task<object[]> EchoTwoParametersAsync(
ComplexParameter parameter1,
byte parameter2)
@ -321,72 +320,128 @@ namespace BasicTestApp.InteropTest
return Task.FromResult(new object[] { parameter1, parameter2 });
}
[JSInvokable(nameof(EchoThreeParametersAsync))]
[JSInvokable]
public static Task<object[]> EchoThreeParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3)
TestDTO parameter3)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3 });
return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue() });
}
[JSInvokable(nameof(EchoFourParametersAsync))]
[JSInvokable]
public static Task<object[]> EchoFourParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4 });
return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4 });
}
[JSInvokable(nameof(EchoFiveParametersAsync))]
[JSInvokable]
public static Task<object[]> EchoFiveParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5 });
return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5 });
}
[JSInvokable(nameof(EchoSixParametersAsync))]
[JSInvokable]
public static Task<object[]> EchoSixParametersAsync(ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6 });
return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6 });
}
[JSInvokable(nameof(EchoSevenParametersAsync))]
[JSInvokable]
public static Task<object[]> EchoSevenParametersAsync(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
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(
ComplexParameter parameter1,
byte parameter2,
short parameter3,
TestDTO parameter3,
int parameter4,
long parameter5,
float parameter6,
List<double> parameter7,
Segment parameter8)
{
return Task.FromResult(new object[] { parameter1, parameter2, parameter3, parameter4, parameter5, parameter6, parameter7, parameter8 });
return Task.FromResult(new object[] { parameter1, parameter2, parameter3.GetNonSerializedValue(), parameter4, parameter5, parameter6, parameter7, parameter8 });
}
[JSInvokable]
public static Dictionary<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 assemblyName = 'BasicTestApp';
function invokeDotNetInteropMethodsAsync() {
function invokeDotNetInteropMethodsAsync(dotNetObjectByRef, instanceMethodsTarget) {
console.log('Invoking void sync methods.');
DotNet.invokeMethod(assemblyName, 'VoidParameterless');
DotNet.invokeMethod(assemblyName, 'VoidWithOneParameter', ...createArgumentList(1));
DotNet.invokeMethod(assemblyName, 'VoidWithTwoParameters', ...createArgumentList(2));
DotNet.invokeMethod(assemblyName, 'VoidWithThreeParameters', ...createArgumentList(3));
DotNet.invokeMethod(assemblyName, 'VoidWithFourParameters', ...createArgumentList(4));
DotNet.invokeMethod(assemblyName, 'VoidWithFiveParameters', ...createArgumentList(5));
DotNet.invokeMethod(assemblyName, 'VoidWithSixParameters', ...createArgumentList(6));
DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7));
DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8));
DotNet.invokeMethod(assemblyName, 'VoidWithOneParameter', ...createArgumentList(1, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithTwoParameters', ...createArgumentList(2, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithThreeParameters', ...createArgumentList(3, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithFourParameters', ...createArgumentList(4, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithFiveParameters', ...createArgumentList(5, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithSixParameters', ...createArgumentList(6, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8, dotNetObjectByRef));
console.log('Invoking returning sync methods.');
results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray');
results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1));
results['result3'] = DotNet.invokeMethod(assemblyName, 'EchoTwoParameters', ...createArgumentList(2));
results['result4'] = DotNet.invokeMethod(assemblyName, 'EchoThreeParameters', ...createArgumentList(3));
results['result5'] = DotNet.invokeMethod(assemblyName, 'EchoFourParameters', ...createArgumentList(4));
results['result6'] = DotNet.invokeMethod(assemblyName, 'EchoFiveParameters', ...createArgumentList(5));
results['result7'] = DotNet.invokeMethod(assemblyName, 'EchoSixParameters', ...createArgumentList(6));
results['result8'] = DotNet.invokeMethod(assemblyName, 'EchoSevenParameters', ...createArgumentList(7));
results['result9'] = DotNet.invokeMethod(assemblyName, 'EchoEightParameters', ...createArgumentList(8));
results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1, dotNetObjectByRef));
results['result3'] = DotNet.invokeMethod(assemblyName, 'EchoTwoParameters', ...createArgumentList(2, dotNetObjectByRef));
results['result4'] = DotNet.invokeMethod(assemblyName, 'EchoThreeParameters', ...createArgumentList(3, dotNetObjectByRef));
results['result5'] = DotNet.invokeMethod(assemblyName, 'EchoFourParameters', ...createArgumentList(4, dotNetObjectByRef));
results['result6'] = DotNet.invokeMethod(assemblyName, 'EchoFiveParameters', ...createArgumentList(5, dotNetObjectByRef));
results['result7'] = DotNet.invokeMethod(assemblyName, 'EchoSixParameters', ...createArgumentList(6, dotNetObjectByRef));
results['result8'] = DotNet.invokeMethod(assemblyName, 'EchoSevenParameters', ...createArgumentList(7, dotNetObjectByRef));
results['result9'] = DotNet.invokeMethod(assemblyName, 'EchoEightParameters', ...createArgumentList(8, dotNetObjectByRef));
var returnDotNetObjectByRefResult = DotNet.invokeMethod(assemblyName, 'ReturnDotNetObjectByRef');
results['resultReturnDotNetObjectByRefSync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefResult['Some sync instance']);
var instanceMethodResult = instanceMethodsTarget.invokeMethod('InstanceMethod', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
});
results['instanceMethodThisTypeName'] = instanceMethodResult.thisTypeName;
results['instanceMethodStringValueUpper'] = instanceMethodResult.stringValueUpper;
results['instanceMethodIncomingByRef'] = instanceMethodResult.incomingByRef;
results['instanceMethodOutgoingByRef'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', instanceMethodResult.outgoingByRef);
console.log('Invoking void async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync')
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(() => {
console.log('Invoking returning async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync')
.then(r => results['result1Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(r => results['result2Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(r => results['result3Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(r => results['result4Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(r => results['result5Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(r => results['result6Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(r => results['result7Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(r => results['result8Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8)))
.then(r => results['result9Async'] = r);
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(r => results['result9Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync'))
.then(r => {
results['resultReturnDotNetObjectByRefAsync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', r['Some async instance']);
})
.then(() => instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
}))
.then(r => {
results['instanceMethodThisTypeNameAsync'] = r.thisTypeName;
results['instanceMethodStringValueUpperAsync'] = r.stringValueUpper;
results['instanceMethodIncomingByRefAsync'] = r.incomingByRef;
results['instanceMethodOutgoingByRefAsync'] = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', r.outgoingByRef);
})
})
.then(() => {
console.log('Invoking methods that throw exceptions');
@ -79,7 +105,7 @@ function invokeDotNetInteropMethodsAsync() {
});
}
function createArgumentList(argumentNumber){
function createArgumentList(argumentNumber, dotNetObjectByRef){
const array = new Array(argumentNumber);
if (argumentNumber === 0) {
return [];
@ -101,7 +127,7 @@ function createArgumentList(argumentNumber){
array[i] = argumentNumber;
break;
case 2:
array[i] = argumentNumber * 2;
array[i] = dotNetObjectByRef;
break;
case 3:
array[i] = argumentNumber * 4;
@ -136,9 +162,25 @@ window.jsInteropTests = {
collectInteropResults: collectInteropResults,
functionThrowsException: functionThrowsException,
asyncFunctionThrowsSyncException: asyncFunctionThrowsSyncException,
asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException
asyncFunctionThrowsAsyncException: asyncFunctionThrowsAsyncException,
returnPrimitive: returnPrimitive,
returnPrimitiveAsync: returnPrimitiveAsync,
receiveDotNetObjectByRef: receiveDotNetObjectByRef,
receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync,
};
function returnPrimitive() {
return 123;
}
function returnPrimitiveAsync() {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve(123);
}, 100);
});
}
function functionThrowsException() {
throw new Error('Function threw an exception!');
}
@ -163,3 +205,29 @@ function collectInteropResults() {
return result;
}
function receiveDotNetObjectByRef(incomingData) {
const stringValue = incomingData.stringValue;
const testDto = incomingData.testDto;
// To verify we received a proper reference to testDto, pass it back into .NET
// to have it evaluate something that only .NET can know
const testDtoNonSerializedValue = DotNet.invokeMethod(assemblyName, 'ExtractNonSerializedValue', testDto);
// To show we can return a .NET object by ref anywhere in a complex structure,
// return it among other values
return {
stringValueUpper: stringValue.toUpperCase(),
testDtoNonSerializedValue: testDtoNonSerializedValue,
testDto: testDto
};
}
function receiveDotNetObjectByRefAsync(incomingData) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const promiseResult = receiveDotNetObjectByRef(incomingData);
resolve(promiseResult);
}, 100);
});
}