Use System.Text.Json in Microsoft.JSInterop (dotnet/extensions#1704)
* Use System.Text.Json in Microsoft.JSInterop
\n\nCommit migrated from f6c9258abe
This commit is contained in:
parent
8b068f2320
commit
78daf0166c
|
|
@ -264,17 +264,14 @@ module DotNet {
|
|||
}
|
||||
|
||||
public serializeAsArg() {
|
||||
return `__dotNetObject:${this._id}`;
|
||||
return {__dotNetObject: this._id};
|
||||
}
|
||||
}
|
||||
|
||||
const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/;
|
||||
const dotNetObjectRefKey = '__dotNetObject';
|
||||
attachReviver(function reviveDotNetObject(key: any, value: any) {
|
||||
if (typeof value === 'string') {
|
||||
const match = value.match(dotNetObjectValueFormat);
|
||||
if (match) {
|
||||
return new DotNetObject(parseInt(match[1]));
|
||||
}
|
||||
if (value && typeof value === 'object' && value.hasOwnProperty(dotNetObjectRefKey)) {
|
||||
return new DotNetObject(value.__dotNetObject);
|
||||
}
|
||||
|
||||
// Unrecognized - let another reviver handle it
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="Microsoft.JSInterop.netstandard2.0.cs" />
|
||||
|
||||
<Reference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,19 @@ namespace Microsoft.JSInterop
|
|||
[Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")]
|
||||
public static void ReleaseDotNetObject(long dotNetObjectId) { }
|
||||
}
|
||||
public partial class DotNetObjectRef : System.IDisposable
|
||||
public static partial class DotNetObjectRef
|
||||
{
|
||||
public DotNetObjectRef(object value) { }
|
||||
public object Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public static Microsoft.JSInterop.DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class { throw null; }
|
||||
}
|
||||
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
|
||||
{
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public DotNetObjectRef() { }
|
||||
[System.Text.Json.Serialization.JsonIgnoreAttribute]
|
||||
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public long __dotNetObject { get { throw null; } set { } }
|
||||
public void Dispose() { }
|
||||
public void EnsureAttachedToJsRuntime(Microsoft.JSInterop.IJSRuntime runtime) { }
|
||||
}
|
||||
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime
|
||||
{
|
||||
|
|
@ -25,8 +32,7 @@ namespace Microsoft.JSInterop
|
|||
}
|
||||
public partial interface IJSRuntime
|
||||
{
|
||||
System.Threading.Tasks.Task<T> InvokeAsync<T>(string identifier, params object[] args);
|
||||
void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef);
|
||||
System.Threading.Tasks.Task<TValue> InvokeAsync<TValue>(string identifier, params object[] args);
|
||||
}
|
||||
public partial class JSException : System.Exception
|
||||
{
|
||||
|
|
@ -36,7 +42,7 @@ namespace Microsoft.JSInterop
|
|||
{
|
||||
protected JSInProcessRuntimeBase() { }
|
||||
protected abstract string InvokeJS(string identifier, string argsJson);
|
||||
public T Invoke<T>(string identifier, params object[] args) { throw null; }
|
||||
public TValue Invoke<TValue>(string identifier, params object[] args) { throw null; }
|
||||
}
|
||||
[System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true)]
|
||||
public partial class JSInvokableAttribute : System.Attribute
|
||||
|
|
@ -45,30 +51,21 @@ namespace Microsoft.JSInterop
|
|||
public JSInvokableAttribute(string identifier) { }
|
||||
public string Identifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public static partial class Json
|
||||
{
|
||||
public static T Deserialize<T>(string json) { throw null; }
|
||||
public static string Serialize(object value) { throw null; }
|
||||
}
|
||||
public static partial class JSRuntime
|
||||
{
|
||||
public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { }
|
||||
}
|
||||
public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime
|
||||
{
|
||||
public JSRuntimeBase() { }
|
||||
protected JSRuntimeBase() { }
|
||||
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
|
||||
public System.Threading.Tasks.Task<T> InvokeAsync<T>(string identifier, params object[] args) { throw null; }
|
||||
public void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef) { }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.JSInterop.Internal
|
||||
{
|
||||
public partial interface ICustomArgSerializer
|
||||
{
|
||||
object ToJsonPrimitive();
|
||||
}
|
||||
public partial class JSAsyncCallResult
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed partial class JSAsyncCallResult
|
||||
{
|
||||
internal JSAsyncCallResult() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
// 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;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop.Internal;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
|
|
@ -17,8 +19,10 @@ namespace Microsoft.JSInterop
|
|||
/// </summary>
|
||||
public static class DotNetDispatcher
|
||||
{
|
||||
private static ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
||||
= new ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
|
||||
internal const string DotNetObjectRefKey = nameof(DotNetObjectRef<object>.__dotNetObject);
|
||||
|
||||
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
||||
= new ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
|
||||
|
||||
/// <summary>
|
||||
/// Receives a call from JS to .NET, locating and invoking the specified method.
|
||||
|
|
@ -35,17 +39,19 @@ namespace Microsoft.JSInterop
|
|||
// 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.
|
||||
|
||||
// DotNetDispatcher only works with JSRuntimeBase instances.
|
||||
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
|
||||
|
||||
var targetInstance = (object)null;
|
||||
if (dotNetObjectId != default)
|
||||
{
|
||||
targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId);
|
||||
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
|
||||
}
|
||||
|
||||
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
|
||||
return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy);
|
||||
if (syncResult == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.ToString(syncResult, JsonSerializerOptionsProvider.Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -69,9 +75,11 @@ namespace Microsoft.JSInterop
|
|||
// 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 targetInstance = (object)null;
|
||||
if (dotNetObjectId != default)
|
||||
{
|
||||
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
|
||||
}
|
||||
|
||||
// Using ExceptionDispatchInfo here throughout because we want to always preserve
|
||||
// original stack traces.
|
||||
|
|
@ -121,6 +129,7 @@ namespace Microsoft.JSInterop
|
|||
|
||||
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson)
|
||||
{
|
||||
AssemblyKey assemblyKey;
|
||||
if (targetInstance != null)
|
||||
{
|
||||
if (assemblyName != null)
|
||||
|
|
@ -128,44 +137,16 @@ namespace Microsoft.JSInterop
|
|||
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
|
||||
}
|
||||
|
||||
assemblyName = targetInstance.GetType().Assembly.GetName().Name;
|
||||
assemblyKey = new AssemblyKey(targetInstance.GetType().Assembly);
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblyKey = new AssemblyKey(assemblyName);
|
||||
}
|
||||
|
||||
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier);
|
||||
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, 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<SimpleJson.JsonArray>(argsJson).ToArray<object>();
|
||||
suppliedArgsLength = suppliedArgs.Length;
|
||||
}
|
||||
if (suppliedArgsLength != parameterTypes.Length)
|
||||
{
|
||||
throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}.");
|
||||
}
|
||||
|
||||
// Second, convert each supplied value to the type expected by the method
|
||||
var runtime = (JSRuntimeBase)JSRuntime.Current;
|
||||
var serializerStrategy = runtime.ArgSerializerStrategy;
|
||||
for (var i = 0; i < suppliedArgsLength; 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]);
|
||||
}
|
||||
}
|
||||
var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -183,6 +164,85 @@ namespace Microsoft.JSInterop
|
|||
}
|
||||
}
|
||||
|
||||
private static object[] ParseArguments(string methodIdentifier, string argsJson, Type[] parameterTypes)
|
||||
{
|
||||
if (parameterTypes.Length == 0)
|
||||
{
|
||||
return Array.Empty<object>();
|
||||
}
|
||||
|
||||
// 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.
|
||||
var jsonDocument = JsonDocument.Parse(argsJson);
|
||||
var shouldDisposeJsonDocument = true;
|
||||
try
|
||||
{
|
||||
if (jsonDocument.RootElement.Type != JsonValueType.Array)
|
||||
{
|
||||
throw new ArgumentException($"Expected a JSON array but got {jsonDocument.RootElement.Type}.");
|
||||
}
|
||||
|
||||
var suppliedArgsLength = jsonDocument.RootElement.GetArrayLength();
|
||||
|
||||
if (suppliedArgsLength != parameterTypes.Length)
|
||||
{
|
||||
throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}.");
|
||||
}
|
||||
|
||||
// Second, convert each supplied value to the type expected by the method
|
||||
var suppliedArgs = new object[parameterTypes.Length];
|
||||
var index = 0;
|
||||
foreach (var item in jsonDocument.RootElement.EnumerateArray())
|
||||
{
|
||||
var parameterType = parameterTypes[index];
|
||||
|
||||
if (parameterType == typeof(JSAsyncCallResult))
|
||||
{
|
||||
// We will pass the JsonDocument instance to JAsyncCallResult and make JSRuntimeBase
|
||||
// responsible for disposing it.
|
||||
shouldDisposeJsonDocument = false;
|
||||
// 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[index] = new JSAsyncCallResult(jsonDocument, item);
|
||||
}
|
||||
else if (IsIncorrectDotNetObjectRefUse(item, parameterType))
|
||||
{
|
||||
throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value.");
|
||||
}
|
||||
else
|
||||
{
|
||||
suppliedArgs[index] = JsonSerializer.Parse(item.GetRawText(), parameterType, JsonSerializerOptionsProvider.Options);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
if (shouldDisposeJsonDocument)
|
||||
{
|
||||
jsonDocument.Dispose();
|
||||
}
|
||||
|
||||
return suppliedArgs;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Always dispose the JsonDocument in case of an error.
|
||||
jsonDocument.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
|
||||
static bool IsIncorrectDotNetObjectRefUse(JsonElement item, Type parameterType)
|
||||
{
|
||||
// Check for incorrect use of DotNetObjectRef<T> at the top level. We know it's
|
||||
// an incorrect use if there's a object that looks like { '__dotNetObject': <some number> },
|
||||
// but we aren't assigning to DotNetObjectRef{T}.
|
||||
return item.Type == JsonValueType.Object &&
|
||||
item.TryGetProperty(DotNetObjectRefKey, out _) &&
|
||||
!typeof(IDotNetObjectRef).IsAssignableFrom(parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives notification that a call from .NET to JS has finished, marking the
|
||||
/// associated <see cref="Task"/> as completed.
|
||||
|
|
@ -192,7 +252,7 @@ namespace Microsoft.JSInterop
|
|||
/// <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, JSAsyncCallResult result)
|
||||
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException);
|
||||
=> ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result);
|
||||
|
||||
/// <summary>
|
||||
/// Releases the reference to the specified .NET object. This allows the .NET runtime
|
||||
|
|
@ -207,16 +267,14 @@ namespace Microsoft.JSInterop
|
|||
[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);
|
||||
DotNetObjectRefManager.Current.ReleaseDotNetObject(dotNetObjectId);
|
||||
}
|
||||
|
||||
private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier)
|
||||
private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assemblyName))
|
||||
if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyName));
|
||||
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyKey.AssemblyName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(methodIdentifier))
|
||||
|
|
@ -224,23 +282,23 @@ namespace Microsoft.JSInterop
|
|||
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier));
|
||||
}
|
||||
|
||||
var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyName, ScanAssemblyForCallableMethods);
|
||||
var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyKey, ScanAssemblyForCallableMethods);
|
||||
if (assemblyMethods.TryGetValue(methodIdentifier, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"The assembly '{assemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
|
||||
throw new ArgumentException($"The assembly '{assemblyKey.AssemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, (MethodInfo, Type[])> ScanAssemblyForCallableMethods(string assemblyName)
|
||||
private static Dictionary<string, (MethodInfo, Type[])> ScanAssemblyForCallableMethods(AssemblyKey assemblyKey)
|
||||
{
|
||||
// TODO: Consider looking first for assembly-level attributes (i.e., if there are any,
|
||||
// only use those) to avoid scanning, especially for framework assemblies.
|
||||
var result = new Dictionary<string, (MethodInfo, Type[])>();
|
||||
var invokableMethods = GetRequiredLoadedAssembly(assemblyName)
|
||||
var result = new Dictionary<string, (MethodInfo, Type[])>(StringComparer.Ordinal);
|
||||
var invokableMethods = GetRequiredLoadedAssembly(assemblyKey)
|
||||
.GetExportedTypes()
|
||||
.SelectMany(type => type.GetMethods(
|
||||
BindingFlags.Public |
|
||||
|
|
@ -261,7 +319,7 @@ namespace Microsoft.JSInterop
|
|||
{
|
||||
if (result.ContainsKey(identifier))
|
||||
{
|
||||
throw new InvalidOperationException($"The assembly '{assemblyName}' contains more than one " +
|
||||
throw new InvalidOperationException($"The assembly '{assemblyKey.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.");
|
||||
|
|
@ -276,7 +334,7 @@ namespace Microsoft.JSInterop
|
|||
return result;
|
||||
}
|
||||
|
||||
private static Assembly GetRequiredLoadedAssembly(string assemblyName)
|
||||
private static Assembly GetRequiredLoadedAssembly(AssemblyKey assemblyKey)
|
||||
{
|
||||
// We don't want to load assemblies on demand here, because we don't necessarily trust
|
||||
// "assemblyName" to be something the developer intended to load. So only pick from the
|
||||
|
|
@ -284,8 +342,40 @@ namespace Microsoft.JSInterop
|
|||
// In some edge cases this might force developers to explicitly call something on the
|
||||
// target assembly (from .NET) before they can invoke its allowed methods from JS.
|
||||
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal))
|
||||
?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'.");
|
||||
return loadedAssemblies.FirstOrDefault(a => new AssemblyKey(a).Equals(assemblyKey))
|
||||
?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyKey.AssemblyName}'.");
|
||||
}
|
||||
|
||||
private readonly struct AssemblyKey : IEquatable<AssemblyKey>
|
||||
{
|
||||
public AssemblyKey(Assembly assembly)
|
||||
{
|
||||
Assembly = assembly;
|
||||
AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
|
||||
public AssemblyKey(string assemblyName)
|
||||
{
|
||||
Assembly = null;
|
||||
AssemblyName = assemblyName;
|
||||
}
|
||||
|
||||
public Assembly Assembly { get; }
|
||||
|
||||
public string AssemblyName { get; }
|
||||
|
||||
public bool Equals(AssemblyKey other)
|
||||
{
|
||||
if (Assembly != null && other.Assembly != null)
|
||||
{
|
||||
return Assembly == other.Assembly;
|
||||
}
|
||||
|
||||
return AssemblyName.Equals(other.AssemblyName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(AssemblyName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +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.
|
||||
|
||||
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.
|
||||
/// Provides convenience methods to produce a <see cref="DotNetObjectRef{TValue}" />.
|
||||
/// </summary>
|
||||
public class DotNetObjectRef : IDisposable
|
||||
public static class DotNetObjectRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the object instance represented by this wrapper.
|
||||
/// Creates a new instance of <see cref="DotNetObjectRef{TValue}" />.
|
||||
/// </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)
|
||||
/// <param name="value">The reference type to track.</param>
|
||||
/// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns>
|
||||
public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class
|
||||
{
|
||||
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);
|
||||
return new DotNetObjectRef<TValue>(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
// 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.Threading;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal class DotNetObjectRefManager
|
||||
{
|
||||
private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
|
||||
private readonly ConcurrentDictionary<long, IDotNetObjectRef> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectRef>();
|
||||
|
||||
public static DotNetObjectRefManager Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!(JSRuntime.Current is JSRuntimeBase jsRuntimeBase))
|
||||
{
|
||||
throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectRef.");
|
||||
}
|
||||
|
||||
return jsRuntimeBase.ObjectRefManager;
|
||||
}
|
||||
}
|
||||
|
||||
public long TrackObject(IDotNetObjectRef dotNetObjectRef)
|
||||
{
|
||||
var dotNetObjectId = Interlocked.Increment(ref _nextId);
|
||||
_trackedRefsById[dotNetObjectId] = dotNetObjectRef;
|
||||
|
||||
return dotNetObjectId;
|
||||
}
|
||||
|
||||
public object FindDotNetObject(long dotNetObjectId)
|
||||
{
|
||||
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
|
||||
? dotNetObjectRef.Value
|
||||
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectRef instance was already disposed.", nameof(dotNetObjectId));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops tracking the specified .NET object reference.
|
||||
/// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code
|
||||
/// </summary>
|
||||
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectRef{TValue}"/>.</param>
|
||||
public void ReleaseDotNetObject(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// 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.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
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>
|
||||
/// <typeparam name="TValue">The type of the value to wrap.</typeparam>
|
||||
public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class
|
||||
{
|
||||
private long? _trackingId;
|
||||
|
||||
/// <summary>
|
||||
/// This API is for meant for JSON deserialization and should not be used by user code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public DotNetObjectRef()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to pass by reference.</param>
|
||||
internal DotNetObjectRef(TValue value)
|
||||
{
|
||||
Value = value;
|
||||
_trackingId = DotNetObjectRefManager.Current.TrackObject(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object instance represented by this wrapper.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public TValue Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API is for meant for JSON serialization and should not be used by user code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public long __dotNetObject
|
||||
{
|
||||
get => _trackingId.Value;
|
||||
set
|
||||
{
|
||||
if (_trackingId != null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(DotNetObjectRef<TValue>)} cannot be reinitialized.");
|
||||
}
|
||||
|
||||
_trackingId = value;
|
||||
Value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(value);
|
||||
}
|
||||
}
|
||||
|
||||
object IDotNetObjectRef.Value => Value;
|
||||
|
||||
/// <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()
|
||||
{
|
||||
DotNetObjectRefManager.Current.ReleaseDotNetObject(_trackingId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// 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 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 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 ICustomArgSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal. Intended for framework use only.
|
||||
/// </summary>
|
||||
object ToJsonPrimitive();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal interface IDotNetObjectRef : IDisposable
|
||||
{
|
||||
public object Value { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
|
|
@ -13,20 +14,10 @@ namespace Microsoft.JSInterop
|
|||
/// <summary>
|
||||
/// Invokes the specified JavaScript function asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
|
||||
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||
/// <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 automatically by <see cref="DotNetObjectRef.Dispose"/>.</remarks>
|
||||
void UntrackObjectRef(DotNetObjectRef dotNetObjectRef);
|
||||
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
|
||||
Task<TValue> InvokeAsync<TValue>(string identifier, params object[] args);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
// 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.ComponentModel;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.JSInterop.Internal
|
||||
{
|
||||
// This type takes care of a special case in handling the result of an async call from
|
||||
|
|
@ -20,17 +23,16 @@ namespace Microsoft.JSInterop.Internal
|
|||
/// <summary>
|
||||
/// Intended for framework use only.
|
||||
/// </summary>
|
||||
public class JSAsyncCallResult
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed 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)
|
||||
internal JSAsyncCallResult(JsonDocument document, JsonElement jsonElement)
|
||||
{
|
||||
ResultOrException = resultOrException;
|
||||
JsonDocument = document;
|
||||
JsonElement = jsonElement;
|
||||
}
|
||||
|
||||
internal JsonElement JsonElement { get; }
|
||||
internal JsonDocument JsonDocument { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -11,14 +13,19 @@ namespace Microsoft.JSInterop
|
|||
/// <summary>
|
||||
/// Invokes the specified JavaScript function synchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
|
||||
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||
/// <param name="args">JSON-serializable arguments.</param>
|
||||
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||
public T Invoke<T>(string identifier, params object[] args)
|
||||
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
|
||||
public TValue Invoke<TValue>(string identifier, params object[] args)
|
||||
{
|
||||
var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy));
|
||||
return Json.Deserialize<T>(resultJson, ArgSerializerStrategy);
|
||||
var resultJson = InvokeJS(identifier, JsonSerializer.ToString(args, JsonSerializerOptionsProvider.Options));
|
||||
if (resultJson is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Parse<TValue>(resultJson, JsonSerializerOptionsProvider.Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop.Internal;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
|
|
@ -18,19 +20,7 @@ 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);
|
||||
internal DotNetObjectRefManager ObjectRefManager { get; } = new DotNetObjectRefManager();
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified JavaScript function asynchronously.
|
||||
|
|
@ -51,9 +41,9 @@ namespace Microsoft.JSInterop
|
|||
|
||||
try
|
||||
{
|
||||
var argsJson = args?.Length > 0
|
||||
? Json.Serialize(args, ArgSerializerStrategy)
|
||||
: null;
|
||||
var argsJson = args?.Length > 0 ?
|
||||
JsonSerializer.ToString(args, JsonSerializerOptionsProvider.Options) :
|
||||
null;
|
||||
BeginInvokeJS(taskId, identifier, argsJson);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
|
@ -88,33 +78,32 @@ namespace Microsoft.JSInterop
|
|||
|
||||
// We pass 0 as the async handle because we don't want the JS-side code to
|
||||
// send back any notification (we're just providing a result for an existing async call)
|
||||
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] {
|
||||
callId,
|
||||
success,
|
||||
resultOrException
|
||||
}, ArgSerializerStrategy));
|
||||
var args = JsonSerializer.ToString(new[] { callId, success, resultOrException }, JsonSerializerOptionsProvider.Options);
|
||||
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
||||
}
|
||||
|
||||
internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException)
|
||||
internal void EndInvokeJS(long asyncHandle, bool succeeded, JSAsyncCallResult asyncCallResult)
|
||||
{
|
||||
if (!_pendingTasks.TryRemove(asyncHandle, out var tcs))
|
||||
using (asyncCallResult?.JsonDocument)
|
||||
{
|
||||
throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'.");
|
||||
}
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
|
||||
if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray)
|
||||
if (!_pendingTasks.TryRemove(asyncHandle, out var tcs))
|
||||
{
|
||||
resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType);
|
||||
throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'.");
|
||||
}
|
||||
|
||||
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException);
|
||||
}
|
||||
else
|
||||
{
|
||||
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString()));
|
||||
if (succeeded)
|
||||
{
|
||||
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
|
||||
var result = asyncCallResult != null ?
|
||||
JsonSerializer.Parse(asyncCallResult.JsonElement.GetRawText(), resultType, JsonSerializerOptionsProvider.Options) :
|
||||
null;
|
||||
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var exceptionText = asyncCallResult?.JsonElement.ToString() ?? string.Empty;
|
||||
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal static class CamelCase
|
||||
{
|
||||
public static string MemberNameToCamelCase(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The value '{value ?? "null"}' is not a valid member name.",
|
||||
nameof(value));
|
||||
}
|
||||
|
||||
// If we don't need to modify the value, bail out without creating a char array
|
||||
if (!char.IsUpper(value[0]))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// We have to modify at least one character
|
||||
var chars = value.ToCharArray();
|
||||
|
||||
var length = chars.Length;
|
||||
if (length < 2 || !char.IsUpper(chars[1]))
|
||||
{
|
||||
// Only the first character needs to be modified
|
||||
// Note that this branch is functionally necessary, because the 'else' branch below
|
||||
// never looks at char[1]. It's always looking at the n+2 character.
|
||||
chars[0] = char.ToLowerInvariant(chars[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If chars[0] and chars[1] are both upper, then we'll lowercase the first char plus
|
||||
// any consecutive uppercase ones, stopping if we find any char that is followed by a
|
||||
// non-uppercase one
|
||||
var i = 0;
|
||||
while (i < length)
|
||||
{
|
||||
chars[i] = char.ToLowerInvariant(chars[i]);
|
||||
|
||||
i++;
|
||||
|
||||
// If the next-plus-one char isn't also uppercase, then we're now on the last uppercase, so stop
|
||||
if (i < length - 1 && !char.IsUpper(chars[i + 1]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides mechanisms for converting between .NET objects and JSON strings for use
|
||||
/// when making calls to JavaScript functions via <see cref="IJSRuntime"/>.
|
||||
///
|
||||
/// Warning: This is not intended as a general-purpose JSON library. It is only intended
|
||||
/// for use when making calls via <see cref="IJSRuntime"/>. Eventually its implementation
|
||||
/// will be replaced by something more general-purpose.
|
||||
/// </summary>
|
||||
public static class Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes the value as a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to serialize.</param>
|
||||
/// <returns>The JSON string.</returns>
|
||||
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>
|
||||
/// <typeparam name="T">The type of object to create.</typeparam>
|
||||
/// <param name="json">The JSON string.</param>
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
SimpleJson is from https://github.com/facebook-csharp-sdk/simple-json
|
||||
|
||||
LICENSE (from https://github.com/facebook-csharp-sdk/simple-json/blob/08b6871e8f63e866810d25e7a03c48502c9a234b/LICENSE.txt):
|
||||
=====
|
||||
Copyright (c) 2011, The Outercurve Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,15 @@
|
|||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal static class JsonSerializerOptionsProvider
|
||||
{
|
||||
public static readonly JsonSerializerOptions Options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
|
@ -8,6 +8,10 @@
|
|||
<IsShipping>true</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.JSInterop.Tests" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -10,10 +12,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
public class DotNetDispatcherTest
|
||||
{
|
||||
private readonly static string thisAssemblyName
|
||||
= typeof(DotNetDispatcherTest).Assembly.GetName().Name;
|
||||
private readonly TestJSRuntime jsRuntime
|
||||
= new TestJSRuntime();
|
||||
private readonly static string thisAssemblyName = typeof(DotNetDispatcherTest).Assembly.GetName().Name;
|
||||
|
||||
[Fact]
|
||||
public void CannotInvokeWithEmptyAssemblyName()
|
||||
|
|
@ -24,7 +23,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
});
|
||||
|
||||
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
|
||||
Assert.Equal("assemblyName", ex.ParamName);
|
||||
Assert.Equal("AssemblyName", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")]
|
||||
[Fact]
|
||||
public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||
{
|
||||
// Arrange/Act
|
||||
|
|
@ -90,7 +89,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
// Arrange/Act
|
||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
|
||||
var result = Json.Deserialize<TestDTO>(resultJson);
|
||||
var result = JsonSerializer.Parse<TestDTO>(resultJson, JsonSerializerOptionsProvider.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test", result.StringVal);
|
||||
|
|
@ -102,50 +101,81 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
// Arrange/Act
|
||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null);
|
||||
var result = Json.Deserialize<TestDTO>(resultJson);
|
||||
var result = JsonSerializer.Parse<TestDTO>(resultJson, JsonSerializerOptionsProvider.Options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
|
||||
Assert.Equal(456, result.IntVal);
|
||||
});
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")]
|
||||
[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));
|
||||
var objectRef = DotNetObjectRef.Create(arg3);
|
||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||
|
||||
// Arrange: Remaining args
|
||||
var argsJson = Json.Serialize(new object[] {
|
||||
var argsJson = JsonSerializer.ToString(new object[]
|
||||
{
|
||||
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
||||
new[] { 100, 200 },
|
||||
"__dotNetObject:1"
|
||||
});
|
||||
objectRef
|
||||
}, JsonSerializerOptionsProvider.Options);
|
||||
|
||||
// Act
|
||||
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson);
|
||||
var result = Json.Deserialize<object[]>(resultJson);
|
||||
var result = JsonDocument.Parse(resultJson);
|
||||
var root = result.RootElement;
|
||||
|
||||
// Assert: First result value marshalled via JSON
|
||||
var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO));
|
||||
var resultDto1 = JsonSerializer.Parse<TestDTO>(root[0].GetRawText(), JsonSerializerOptionsProvider.Options);
|
||||
|
||||
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);
|
||||
var resultDto2Ref = root[1];
|
||||
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _));
|
||||
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
|
||||
|
||||
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey, out var property));
|
||||
var resultDto2 = Assert.IsType<TestDTO>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64()));
|
||||
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||
Assert.Equal(1299, resultDto2.IntVal);
|
||||
});
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")]
|
||||
[Fact]
|
||||
public Task InvokingWithIncorrectUseOfDotNetObjectRefThrows() => WithJSRuntime(jsRuntime =>
|
||||
{
|
||||
// Arrange
|
||||
var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage);
|
||||
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
|
||||
var objectRef = DotNetObjectRef.Create(arg3);
|
||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||
|
||||
// Arrange: Remaining args
|
||||
var argsJson = JsonSerializer.ToString(new object[]
|
||||
{
|
||||
new TestDTO { StringVal = "Another string", IntVal = 456 },
|
||||
new[] { 100, 200 },
|
||||
objectRef
|
||||
}, JsonSerializerOptionsProvider.Options);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
DotNetDispatcher.Invoke(thisAssemblyName, method, default, argsJson));
|
||||
Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||
{
|
||||
// Arrange: Track some instance
|
||||
var targetInstance = new SomePublicType();
|
||||
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance));
|
||||
var objectRef = DotNetObjectRef.Create(targetInstance);
|
||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||
|
||||
// Act
|
||||
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null);
|
||||
|
|
@ -155,12 +185,13 @@ namespace Microsoft.JSInterop.Tests
|
|||
Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
|
||||
});
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")]
|
||||
[Fact]
|
||||
public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
|
||||
{
|
||||
// Arrange: Track some instance
|
||||
var targetInstance = new DerivedClass();
|
||||
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance));
|
||||
var objectRef = DotNetObjectRef.Create(targetInstance);
|
||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||
|
||||
// Act
|
||||
var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null);
|
||||
|
|
@ -178,7 +209,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
|
||||
// Arrange: Track some instance, then dispose it
|
||||
var targetInstance = new SomePublicType();
|
||||
var objectRef = new DotNetObjectRef(targetInstance);
|
||||
var objectRef = DotNetObjectRef.Create(targetInstance);
|
||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||
objectRef.Dispose();
|
||||
|
||||
|
|
@ -196,7 +227,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
|
||||
// Arrange: Track some instance, then dispose it
|
||||
var targetInstance = new SomePublicType();
|
||||
var objectRef = new DotNetObjectRef(targetInstance);
|
||||
var objectRef = DotNetObjectRef.Create(targetInstance);
|
||||
jsRuntime.Invoke<object>("unimportant", objectRef);
|
||||
DotNetDispatcher.ReleaseDotNetObject(1);
|
||||
|
||||
|
|
@ -206,23 +237,23 @@ namespace Microsoft.JSInterop.Tests
|
|||
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||
});
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")]
|
||||
[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\"]";
|
||||
DotNetObjectRef.Create(targetInstance),
|
||||
DotNetObjectRef.Create(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("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
|
||||
var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3);
|
||||
Assert.Equal(1235, resultDto.IntVal);
|
||||
Assert.Equal("MY STRING", resultDto.StringVal);
|
||||
});
|
||||
|
|
@ -231,7 +262,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
public void CannotInvokeWithIncorrectNumberOfParams()
|
||||
{
|
||||
// Arrange
|
||||
var argsJson = Json.Serialize(new object[] { 1, 2, 3, 4 });
|
||||
var argsJson = JsonSerializer.ToString(new object[] { 1, 2, 3, 4 }, JsonSerializerOptionsProvider.Options);
|
||||
|
||||
// Act/Assert
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
|
|
@ -242,50 +273,50 @@ namespace Microsoft.JSInterop.Tests
|
|||
Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")]
|
||||
[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));
|
||||
var arg1Ref = DotNetObjectRef.Create(targetInstance);
|
||||
var arg2Ref = DotNetObjectRef.Create(arg2);
|
||||
jsRuntime.Invoke<object>("unimportant", arg1Ref, arg2Ref);
|
||||
|
||||
// Arrange: all args
|
||||
var argsJson = Json.Serialize(new object[]
|
||||
var argsJson = JsonSerializer.ToString(new object[]
|
||||
{
|
||||
new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
|
||||
"__dotNetObject:2"
|
||||
});
|
||||
arg2Ref,
|
||||
}, JsonSerializerOptionsProvider.Options);
|
||||
|
||||
// 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];
|
||||
var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement;
|
||||
var resultValue = 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.Equal(3, result.GetArrayLength());
|
||||
Assert.Equal(callId, result[0].GetString());
|
||||
Assert.True(result[1].GetBoolean()); // Success flag
|
||||
|
||||
// Assert: First result value marshalled via JSON
|
||||
var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO));
|
||||
var resultDto1 = JsonSerializer.Parse<TestDTO>(resultValue[0].GetRawText(), JsonSerializerOptionsProvider.Options);
|
||||
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);
|
||||
var resultDto2Ref = JsonSerializer.Parse<DotNetObjectRef<TestDTO>>(resultValue[1].GetRawText(), JsonSerializerOptionsProvider.Options);
|
||||
var resultDto2 = resultDto2Ref.Value;
|
||||
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||
Assert.Equal(2468, resultDto2.IntVal);
|
||||
});
|
||||
|
||||
|
||||
[Fact]
|
||||
public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime =>
|
||||
{
|
||||
|
|
@ -299,13 +330,13 @@ namespace Microsoft.JSInterop.Tests
|
|||
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
||||
|
||||
// Assert
|
||||
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson);
|
||||
Assert.Equal(callId, result[0]);
|
||||
Assert.False((bool)result[1]); // Fails
|
||||
var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement;
|
||||
Assert.Equal(callId, result[0].GetString());
|
||||
Assert.False(result[1].GetBoolean()); // Fails
|
||||
|
||||
// Make sure the method that threw the exception shows up in the call stack
|
||||
// https://github.com/aspnet/AspNetCore/issues/8612
|
||||
var exception = (string)result[2];
|
||||
var exception = result[2].GetString();
|
||||
Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception);
|
||||
});
|
||||
|
||||
|
|
@ -322,17 +353,16 @@ namespace Microsoft.JSInterop.Tests
|
|||
await resultTask; // This won't throw, it sets properties on the jsRuntime.
|
||||
|
||||
// Assert
|
||||
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson);
|
||||
Assert.Equal(callId, result[0]);
|
||||
Assert.False((bool)result[1]); // Fails
|
||||
var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement;
|
||||
Assert.Equal(callId, result[0].GetString());
|
||||
Assert.False(result[1].GetBoolean()); // Fails
|
||||
|
||||
// Make sure the method that threw the exception shows up in the call stack
|
||||
// https://github.com/aspnet/AspNetCore/issues/8612
|
||||
var exception = (string)result[2];
|
||||
var exception = result[2].GetString();
|
||||
Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception);
|
||||
});
|
||||
|
||||
|
||||
Task WithJSRuntime(Action<TestJSRuntime> testCode)
|
||||
{
|
||||
return WithJSRuntime(jsRuntime =>
|
||||
|
|
@ -379,7 +409,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
=> new TestDTO { StringVal = "Test", IntVal = 123 };
|
||||
|
||||
[JSInvokable("InvocableStaticWithParams")]
|
||||
public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
|
||||
public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, DotNetObjectRef<TestDTO> dtoByRef)
|
||||
=> new object[]
|
||||
{
|
||||
new TestDTO // Return via JSON marshalling
|
||||
|
|
@ -387,13 +417,17 @@ namespace Microsoft.JSInterop.Tests
|
|||
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
|
||||
IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
|
||||
},
|
||||
new DotNetObjectRef(new TestDTO // Return by ref
|
||||
DotNetObjectRef.Create(new TestDTO // Return by ref
|
||||
{
|
||||
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
|
||||
IntVal = dtoByRef.IntVal + incrementAmounts.Sum()
|
||||
StringVal = dtoByRef.Value.StringVal.ToUpperInvariant(),
|
||||
IntVal = dtoByRef.Value.IntVal + incrementAmounts.Sum()
|
||||
})
|
||||
};
|
||||
|
||||
[JSInvokable(nameof(IncorrectDotNetObjectRefUsage))]
|
||||
public static object[] IncorrectDotNetObjectRefUsage(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
|
||||
=> throw new InvalidOperationException("Shouldn't be called");
|
||||
|
||||
[JSInvokable]
|
||||
public static TestDTO InvokableMethodWithoutCustomIdentifier()
|
||||
=> new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
|
||||
|
|
@ -405,14 +439,15 @@ namespace Microsoft.JSInterop.Tests
|
|||
}
|
||||
|
||||
[JSInvokable]
|
||||
public object[] InvokableInstanceMethod(string someString, TestDTO someDTO)
|
||||
public object[] InvokableInstanceMethod(string someString, DotNetObjectRef<TestDTO> someDTORef)
|
||||
{
|
||||
var someDTO = someDTORef.Value;
|
||||
// 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
|
||||
DotNetObjectRef.Create(new TestDTO
|
||||
{
|
||||
IntVal = someDTO.IntVal + 1,
|
||||
StringVal = someDTO.StringVal.ToUpperInvariant()
|
||||
|
|
@ -421,9 +456,10 @@ namespace Microsoft.JSInterop.Tests
|
|||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef)
|
||||
public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectRef<TestDTO> dtoByRefWrapper)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
var dtoByRef = dtoByRefWrapper.Value;
|
||||
return new object[]
|
||||
{
|
||||
new TestDTO // Return via JSON
|
||||
|
|
@ -431,7 +467,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
|
||||
IntVal = dtoViaJson.IntVal * 2,
|
||||
},
|
||||
new DotNetObjectRef(new TestDTO // Return by ref
|
||||
DotNetObjectRef.Create(new TestDTO // Return by ref
|
||||
{
|
||||
StringVal = dtoByRef.StringVal.ToUpperInvariant(),
|
||||
IntVal = dtoByRef.IntVal * 2,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// 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;
|
||||
|
||||
|
|
@ -11,58 +10,44 @@ namespace Microsoft.JSInterop.Tests
|
|||
public class DotNetObjectRefTest
|
||||
{
|
||||
[Fact]
|
||||
public void CanAccessValue()
|
||||
public Task CanAccessValue() => WithJSRuntime(_ =>
|
||||
{
|
||||
var obj = new object();
|
||||
Assert.Same(obj, new DotNetObjectRef(obj).Value);
|
||||
}
|
||||
Assert.Same(obj, DotNetObjectRef.Create(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()
|
||||
public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime =>
|
||||
{
|
||||
// Arrange
|
||||
var objRef = new DotNetObjectRef(new object());
|
||||
var jsRuntime = new TestJsRuntime();
|
||||
objRef.EnsureAttachedToJsRuntime(jsRuntime);
|
||||
var objRef = DotNetObjectRef.Create(new object());
|
||||
var trackingId = objRef.__dotNetObject;
|
||||
|
||||
// Act
|
||||
objRef.Dispose();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs);
|
||||
var ex = Assert.Throws<ArgumentException>(() => jsRuntime.ObjectRefManager.FindDotNetObject(trackingId));
|
||||
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||
});
|
||||
|
||||
class TestJSRuntime : JSRuntimeBase
|
||||
{
|
||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
class TestJsRuntime : IJSRuntime
|
||||
async Task WithJSRuntime(Action<JSRuntimeBase> testCode)
|
||||
{
|
||||
public List<DotNetObjectRef> UntrackedRefs = new List<DotNetObjectRef>();
|
||||
// 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();
|
||||
|
||||
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
|
||||
=> UntrackedRefs.Add(dotNetObjectRef);
|
||||
var runtime = new TestJSRuntime();
|
||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
||||
testCode(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
public class JSInProcessRuntimeBaseTest
|
||||
{
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1807#issuecomment-470756811")]
|
||||
[Fact]
|
||||
public void DispatchesSyncCallsAndDeserializesResults()
|
||||
{
|
||||
// Arrange
|
||||
var runtime = new TestJSInProcessRuntime
|
||||
{
|
||||
NextResultJson = Json.Serialize(
|
||||
new TestDTO { IntValue = 123, StringValue = "Hello" })
|
||||
NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}"
|
||||
};
|
||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
||||
|
||||
// Act
|
||||
var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true);
|
||||
|
|
@ -36,18 +36,19 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
// Arrange
|
||||
var runtime = new TestJSInProcessRuntime { NextResultJson = null };
|
||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
||||
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),
|
||||
var syncResult = runtime.Invoke<DotNetObjectRef<object>>("test identifier",
|
||||
DotNetObjectRef.Create(obj1),
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "obj2", new DotNetObjectRef(obj2) },
|
||||
{ "obj3", new DotNetObjectRef(obj3) }
|
||||
{ "obj2", DotNetObjectRef.Create(obj2) },
|
||||
{ "obj3", DotNetObjectRef.Create(obj3) },
|
||||
});
|
||||
|
||||
// Assert: Handles null result string
|
||||
|
|
@ -56,12 +57,12 @@ namespace Microsoft.JSInterop.Tests
|
|||
// 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.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));
|
||||
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1));
|
||||
Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2));
|
||||
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -70,20 +71,22 @@ namespace Microsoft.JSInterop.Tests
|
|||
// Arrange
|
||||
var runtime = new TestJSInProcessRuntime
|
||||
{
|
||||
NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]"
|
||||
NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]"
|
||||
};
|
||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
||||
var obj1 = new object();
|
||||
var obj2 = new object();
|
||||
|
||||
// Act
|
||||
var syncResult = runtime.Invoke<object[]>("test identifier",
|
||||
new DotNetObjectRef(obj1),
|
||||
var syncResult = runtime.Invoke<DotNetObjectRef<object>[]>(
|
||||
"test identifier",
|
||||
DotNetObjectRef.Create(obj1),
|
||||
"some other arg",
|
||||
new DotNetObjectRef(obj2));
|
||||
DotNetObjectRef.Create(obj2));
|
||||
var call = runtime.InvokeCalls.Single();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { obj2, obj1 }, syncResult);
|
||||
Assert.Equal(new[] { obj2, obj1 }, syncResult.Select(r => r.Value));
|
||||
}
|
||||
|
||||
class TestDTO
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// 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 System.Text.Json;
|
||||
using Microsoft.JSInterop.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.JSInterop.Tests
|
||||
|
|
@ -47,12 +48,13 @@ namespace Microsoft.JSInterop.Tests
|
|||
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||
Assert.False(unrelatedTask.IsCompleted);
|
||||
Assert.False(task.IsCompleted);
|
||||
using var jsonDocument = JsonDocument.Parse("\"my result\"");
|
||||
|
||||
// Act/Assert: Task can be completed
|
||||
runtime.OnEndInvoke(
|
||||
runtime.BeginInvokeCalls[1].AsyncHandle,
|
||||
/* succeeded: */ true,
|
||||
"my result");
|
||||
new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement));
|
||||
Assert.False(unrelatedTask.IsCompleted);
|
||||
Assert.True(task.IsCompleted);
|
||||
Assert.Equal("my result", task.Result);
|
||||
|
|
@ -69,12 +71,13 @@ namespace Microsoft.JSInterop.Tests
|
|||
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||
Assert.False(unrelatedTask.IsCompleted);
|
||||
Assert.False(task.IsCompleted);
|
||||
using var jsonDocument = JsonDocument.Parse("\"This is a test exception\"");
|
||||
|
||||
// Act/Assert: Task can be failed
|
||||
runtime.OnEndInvoke(
|
||||
runtime.BeginInvokeCalls[1].AsyncHandle,
|
||||
/* succeeded: */ false,
|
||||
"This is a test exception");
|
||||
new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement));
|
||||
Assert.False(unrelatedTask.IsCompleted);
|
||||
Assert.True(task.IsCompleted);
|
||||
|
||||
|
|
@ -106,20 +109,21 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
// Arrange
|
||||
var runtime = new TestJSRuntime();
|
||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
||||
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);
|
||||
var obj1Ref = DotNetObjectRef.Create(obj1);
|
||||
var obj1DifferentRef = DotNetObjectRef.Create(obj1);
|
||||
runtime.InvokeAsync<object>("test identifier",
|
||||
obj1Ref,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "obj2", new DotNetObjectRef(obj2) },
|
||||
{ "obj3", new DotNetObjectRef(obj3) },
|
||||
{ "obj2", DotNetObjectRef.Create(obj2) },
|
||||
{ "obj3", DotNetObjectRef.Create(obj3) },
|
||||
{ "obj1SameRef", obj1Ref },
|
||||
{ "obj1DifferentRef", obj1DifferentRef },
|
||||
});
|
||||
|
|
@ -127,28 +131,13 @@ namespace Microsoft.JSInterop.Tests
|
|||
// 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.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", 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);
|
||||
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1));
|
||||
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2));
|
||||
Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3));
|
||||
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4));
|
||||
}
|
||||
|
||||
class TestJSRuntime : JSRuntimeBase
|
||||
|
|
@ -172,20 +161,8 @@ namespace Microsoft.JSInterop.Tests
|
|||
});
|
||||
}
|
||||
|
||||
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 },
|
||||
};
|
||||
}
|
||||
public void OnEndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult callResult)
|
||||
=> EndInvokeJS(asyncHandle, succeeded, callResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,349 +0,0 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.JSInterop.Tests
|
||||
{
|
||||
public class JsonUtilTest
|
||||
{
|
||||
// It's not useful to have a complete set of behavior specifications for
|
||||
// what the JSON serializer/deserializer does in all cases here. We merely
|
||||
// expose a simple wrapper over a third-party library that maintains its
|
||||
// own specs and tests.
|
||||
//
|
||||
// We should only add tests here to cover behaviors that Blazor itself
|
||||
// depends on.
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "null")]
|
||||
[InlineData("My string", "\"My string\"")]
|
||||
[InlineData(123, "123")]
|
||||
[InlineData(123.456f, "123.456")]
|
||||
[InlineData(123.456d, "123.456")]
|
||||
[InlineData(true, "true")]
|
||||
public void CanSerializePrimitivesToJson(object value, string expectedJson)
|
||||
{
|
||||
Assert.Equal(expectedJson, Json.Serialize(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null", null)]
|
||||
[InlineData("\"My string\"", "My string")]
|
||||
[InlineData("123", 123L)] // Would also accept 123 as a System.Int32, but Int64 is fine as a default
|
||||
[InlineData("123.456", 123.456d)]
|
||||
[InlineData("true", true)]
|
||||
public void CanDeserializePrimitivesFromJson(string json, object expectedValue)
|
||||
{
|
||||
Assert.Equal(expectedValue, Json.Deserialize<object>(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSerializeClassToJson()
|
||||
{
|
||||
// Arrange
|
||||
var person = new Person
|
||||
{
|
||||
Id = 1844,
|
||||
Name = "Athos",
|
||||
Pets = new[] { "Aramis", "Porthos", "D'Artagnan" },
|
||||
Hobby = Hobbies.Swordfighting,
|
||||
SecondaryHobby = Hobbies.Reading,
|
||||
Nicknames = new List<string> { "Comte de la Fère", "Armand" },
|
||||
BirthInstant = new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)),
|
||||
Age = new TimeSpan(7665, 1, 30, 0),
|
||||
Allergies = new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } },
|
||||
};
|
||||
|
||||
// Act/Assert
|
||||
Assert.Equal(
|
||||
"{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}",
|
||||
Json.Serialize(person));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDeserializeClassFromJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}";
|
||||
|
||||
// Act
|
||||
var person = Json.Deserialize<Person>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1844, person.Id);
|
||||
Assert.Equal("Athos", person.Name);
|
||||
Assert.Equal(new[] { "Aramis", "Porthos", "D'Artagnan" }, person.Pets);
|
||||
Assert.Equal(Hobbies.Swordfighting, person.Hobby);
|
||||
Assert.Equal(Hobbies.Reading, person.SecondaryHobby);
|
||||
Assert.Null(person.NullHobby);
|
||||
Assert.Equal(new[] { "Comte de la Fère", "Armand" }, person.Nicknames);
|
||||
Assert.Equal(new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), person.BirthInstant);
|
||||
Assert.Equal(new TimeSpan(7665, 1, 30, 0), person.Age);
|
||||
Assert.Equal(new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } }, person.Allergies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDeserializeWithCaseInsensitiveKeys()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"ID\":1844,\"NamE\":\"Athos\"}";
|
||||
|
||||
// Act
|
||||
var person = Json.Deserialize<Person>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1844, person.Id);
|
||||
Assert.Equal("Athos", person.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializationPrefersPropertiesOverFields()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"member1\":\"Hello\"}";
|
||||
|
||||
// Act
|
||||
var person = Json.Deserialize<PrefersPropertiesOverFields>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello", person.Member1);
|
||||
Assert.Null(person.member1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSerializeStructToJson()
|
||||
{
|
||||
// Arrange
|
||||
var commandResult = new SimpleStruct
|
||||
{
|
||||
StringProperty = "Test",
|
||||
BoolProperty = true,
|
||||
NullableIntProperty = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = Json.Serialize(commandResult);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDeserializeStructFromJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}";
|
||||
|
||||
//Act
|
||||
var simpleError = Json.Deserialize<SimpleStruct>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test", simpleError.StringProperty);
|
||||
Assert.True(simpleError.BoolProperty);
|
||||
Assert.Equal(1, simpleError.NullableIntProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreateInstanceOfClassWithPrivateConstructor()
|
||||
{
|
||||
// Arrange
|
||||
var expectedName = "NameValue";
|
||||
var json = $"{{\"Name\":\"{expectedName}\"}}";
|
||||
|
||||
// Act
|
||||
var instance = Json.Deserialize<PrivateConstructor>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedName, instance.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSetValueOfPublicPropertiesWithNonPublicSetters()
|
||||
{
|
||||
// Arrange
|
||||
var expectedPrivateValue = "PrivateValue";
|
||||
var expectedProtectedValue = "ProtectedValue";
|
||||
var expectedInternalValue = "InternalValue";
|
||||
|
||||
var json = "{" +
|
||||
$"\"PrivateSetter\":\"{expectedPrivateValue}\"," +
|
||||
$"\"ProtectedSetter\":\"{expectedProtectedValue}\"," +
|
||||
$"\"InternalSetter\":\"{expectedInternalValue}\"," +
|
||||
"}";
|
||||
|
||||
// Act
|
||||
var instance = Json.Deserialize<NonPublicSetterOnPublicProperty>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedPrivateValue, instance.PrivateSetter);
|
||||
Assert.Equal(expectedProtectedValue, instance.ProtectedSetter);
|
||||
Assert.Equal(expectedInternalValue, instance.InternalSetter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsTypesWithAmbiguouslyNamedProperties()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Json.Deserialize<ClashingProperties>("{}");
|
||||
});
|
||||
|
||||
Assert.Equal($"The type '{typeof(ClashingProperties).FullName}' contains multiple public properties " +
|
||||
$"with names case-insensitively matching '{nameof(ClashingProperties.PROP1).ToLowerInvariant()}'. " +
|
||||
$"Such types cannot be used for JSON deserialization.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsTypesWithAmbiguouslyNamedFields()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Json.Deserialize<ClashingFields>("{}");
|
||||
});
|
||||
|
||||
Assert.Equal($"The type '{typeof(ClashingFields).FullName}' contains multiple public fields " +
|
||||
$"with names case-insensitively matching '{nameof(ClashingFields.Field1).ToLowerInvariant()}'. " +
|
||||
$"Such types cannot be used for JSON deserialization.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonEmptyConstructorThrowsUsefulException()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"Property\":1}";
|
||||
var type = typeof(NonEmptyConstructorPoco);
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Json.Deserialize<NonEmptyConstructorPoco>(json);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
$"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// Not all of the following cases are really valid .NET member names, but we have no reason
|
||||
// to implement more logic to detect invalid member names besides the basics (null or empty).
|
||||
[Theory]
|
||||
[InlineData("URLValue", "urlValue")]
|
||||
[InlineData("URL", "url")]
|
||||
[InlineData("ID", "id")]
|
||||
[InlineData("I", "i")]
|
||||
[InlineData("Person", "person")]
|
||||
[InlineData("xPhone", "xPhone")]
|
||||
[InlineData("XPhone", "xPhone")]
|
||||
[InlineData("X_Phone", "x_Phone")]
|
||||
[InlineData("X__Phone", "x__Phone")]
|
||||
[InlineData("IsCIA", "isCIA")]
|
||||
[InlineData("VmQ", "vmQ")]
|
||||
[InlineData("Xml2Json", "xml2Json")]
|
||||
[InlineData("SnAkEcAsE", "snAkEcAsE")]
|
||||
[InlineData("SnA__kEcAsE", "snA__kEcAsE")]
|
||||
[InlineData("already_snake_case_", "already_snake_case_")]
|
||||
[InlineData("IsJSONProperty", "isJSONProperty")]
|
||||
[InlineData("SHOUTING_CASE", "shoutinG_CASE")]
|
||||
[InlineData("9999-12-31T23:59:59.9999999Z", "9999-12-31T23:59:59.9999999Z")]
|
||||
[InlineData("Hi!! This is text. Time to test.", "hi!! This is text. Time to test.")]
|
||||
[InlineData("BUILDING", "building")]
|
||||
[InlineData("BUILDINGProperty", "buildingProperty")]
|
||||
public void MemberNameToCamelCase_Valid(string input, string expectedOutput)
|
||||
{
|
||||
Assert.Equal(expectedOutput, CamelCase.MemberNameToCamelCase(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void MemberNameToCamelCase_Invalid(string input)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
CamelCase.MemberNameToCamelCase(input));
|
||||
Assert.Equal("value", ex.ParamName);
|
||||
Assert.StartsWith($"The value '{input ?? "null"}' is not a valid member name.", ex.Message);
|
||||
}
|
||||
|
||||
class NonEmptyConstructorPoco
|
||||
{
|
||||
public NonEmptyConstructorPoco(int parameter) { }
|
||||
|
||||
public int Property { get; set; }
|
||||
}
|
||||
|
||||
struct SimpleStruct
|
||||
{
|
||||
public string StringProperty { get; set; }
|
||||
public bool BoolProperty { get; set; }
|
||||
public int? NullableIntProperty { get; set; }
|
||||
}
|
||||
|
||||
class Person
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string[] Pets { get; set; }
|
||||
public Hobbies Hobby { get; set; }
|
||||
public Hobbies? SecondaryHobby { get; set; }
|
||||
public Hobbies? NullHobby { get; set; }
|
||||
public IList<string> Nicknames { get; set; }
|
||||
public DateTimeOffset BirthInstant { get; set; }
|
||||
public TimeSpan Age { get; set; }
|
||||
public IDictionary<string, object> Allergies { get; set; }
|
||||
}
|
||||
|
||||
enum Hobbies { Reading = 1, Swordfighting = 2 }
|
||||
|
||||
#pragma warning disable 0649
|
||||
class ClashingProperties
|
||||
{
|
||||
public string Prop1 { get; set; }
|
||||
public int PROP1 { get; set; }
|
||||
}
|
||||
|
||||
class ClashingFields
|
||||
{
|
||||
public string Field1;
|
||||
public int field1;
|
||||
}
|
||||
|
||||
class PrefersPropertiesOverFields
|
||||
{
|
||||
public string member1;
|
||||
public string Member1 { get; set; }
|
||||
}
|
||||
#pragma warning restore 0649
|
||||
|
||||
class PrivateConstructor
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
private PrivateConstructor()
|
||||
{
|
||||
}
|
||||
|
||||
public PrivateConstructor(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class NonPublicSetterOnPublicProperty
|
||||
{
|
||||
public string PrivateSetter { get; private set; }
|
||||
public string ProtectedSetter { get; protected set; }
|
||||
public string InternalSetter { get; internal set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue