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:
Pranav K 2019-05-29 09:47:26 -07:00 committed by GitHub
parent 8b068f2320
commit 78daf0166c
26 changed files with 550 additions and 3181 deletions

View File

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

View File

@ -5,6 +5,6 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<Compile Include="Microsoft.JSInterop.netstandard2.0.cs" />
<Reference Include="System.Text.Json" />
</ItemGroup>
</Project>

View File

@ -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() { }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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