// 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.JSInterop
{
///
/// Provides methods that receive incoming calls from JS to .NET.
///
public static class DotNetDispatcher
{
private static ConcurrentDictionary> _cachedMethodsByAssembly
= new ConcurrentDictionary>();
///
/// Receives a call from JS to .NET, locating and invoking the specified method.
///
/// The assembly containing the method to be invoked.
/// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string.
/// A JSON representation of the parameters.
/// A JSON representation of the return value, or null.
public static string Invoke(string assemblyName, string methodIdentifier, string argsJson)
{
// 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);
}
///
/// Receives a call from JS to .NET, locating and invoking the specified method asynchronously.
///
/// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.
/// The assembly containing the method to be invoked.
/// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string.
/// A JSON representation of the parameters.
/// A JSON representation of the return value, or null.
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, string argsJson)
{
// 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);
// If there was no callId, the caller does not want to be notified about the result
if (callId != null)
{
// Invoke and coerce the result to a Task so the caller can use the same async API
// for both synchronous and asynchronous methods
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);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result);
}
catch (Exception ex)
{
ex = UnwrapException(ex);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ex);
}
});
}
}
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, string argsJson)
{
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier);
// There's no direct way to say we want to deserialize as an array with heterogenous
// entry types (e.g., [string, int, bool]), so we need to deserialize in two phases.
// First we deserialize as object[], for which SimpleJson will supply JsonObject
// instances for nonprimitive values.
var suppliedArgs = (object[])null;
var suppliedArgsLength = 0;
if (argsJson != null)
{
suppliedArgs = Json.Deserialize(argsJson).ToArray