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() { public serializeAsArg() {
return `__dotNetObject:${this._id}`; return {__dotNetObject: this._id};
} }
} }
const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/; const dotNetObjectRefKey = '__dotNetObject';
attachReviver(function reviveDotNetObject(key: any, value: any) { attachReviver(function reviveDotNetObject(key: any, value: any) {
if (typeof value === 'string') { if (value && typeof value === 'object' && value.hasOwnProperty(dotNetObjectRefKey)) {
const match = value.match(dotNetObjectValueFormat); return new DotNetObject(value.__dotNetObject);
if (match) {
return new DotNetObject(parseInt(match[1]));
}
} }
// Unrecognized - let another reviver handle it // Unrecognized - let another reviver handle it

View File

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

View File

@ -12,12 +12,19 @@ namespace Microsoft.JSInterop
[Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")]
public static void ReleaseDotNetObject(long dotNetObjectId) { } public static void ReleaseDotNetObject(long dotNetObjectId) { }
} }
public partial class DotNetObjectRef : System.IDisposable public static partial class DotNetObjectRef
{ {
public DotNetObjectRef(object value) { } public static Microsoft.JSInterop.DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class { throw null; }
public object Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { 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 Dispose() { }
public void EnsureAttachedToJsRuntime(Microsoft.JSInterop.IJSRuntime runtime) { }
} }
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime
{ {
@ -25,8 +32,7 @@ namespace Microsoft.JSInterop
} }
public partial interface IJSRuntime public partial interface IJSRuntime
{ {
System.Threading.Tasks.Task<T> InvokeAsync<T>(string identifier, params object[] args); System.Threading.Tasks.Task<TValue> InvokeAsync<TValue>(string identifier, params object[] args);
void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef);
} }
public partial class JSException : System.Exception public partial class JSException : System.Exception
{ {
@ -36,7 +42,7 @@ namespace Microsoft.JSInterop
{ {
protected JSInProcessRuntimeBase() { } protected JSInProcessRuntimeBase() { }
protected abstract string InvokeJS(string identifier, string argsJson); 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)] [System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true)]
public partial class JSInvokableAttribute : System.Attribute public partial class JSInvokableAttribute : System.Attribute
@ -45,30 +51,21 @@ namespace Microsoft.JSInterop
public JSInvokableAttribute(string identifier) { } public JSInvokableAttribute(string identifier) { }
public string Identifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } 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 partial class JSRuntime
{ {
public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { }
} }
public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime
{ {
public JSRuntimeBase() { } protected JSRuntimeBase() { }
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson); 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 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 namespace Microsoft.JSInterop.Internal
{ {
public partial interface ICustomArgSerializer [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
{ public sealed partial class JSAsyncCallResult
object ToJsonPrimitive();
}
public partial class JSAsyncCallResult
{ {
internal JSAsyncCallResult() { } internal JSAsyncCallResult() { }
} }

View File

@ -1,14 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop.Internal;
namespace Microsoft.JSInterop namespace Microsoft.JSInterop
{ {
@ -17,8 +19,10 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
public static class DotNetDispatcher public static class DotNetDispatcher
{ {
private static ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly internal const string DotNetObjectRefKey = nameof(DotNetObjectRef<object>.__dotNetObject);
= new ConcurrentDictionary<string, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
= new ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>>();
/// <summary> /// <summary>
/// Receives a call from JS to .NET, locating and invoking the specified method. /// 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, // the targeted method has [JSInvokable]. It is not itself subject to that restriction,
// because there would be nobody to police that. This method *is* the police. // because there would be nobody to police that. This method *is* the police.
// DotNetDispatcher only works with JSRuntimeBase instances.
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
var targetInstance = (object)null; var targetInstance = (object)null;
if (dotNetObjectId != default) if (dotNetObjectId != default)
{ {
targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
} }
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); 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> /// <summary>
@ -69,9 +75,11 @@ namespace Microsoft.JSInterop
// code has to implement its own way of returning async results. // code has to implement its own way of returning async results.
var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current; var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current;
var targetInstance = dotNetObjectId == default var targetInstance = (object)null;
? null if (dotNetObjectId != default)
: jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); {
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
}
// Using ExceptionDispatchInfo here throughout because we want to always preserve // Using ExceptionDispatchInfo here throughout because we want to always preserve
// original stack traces. // original stack traces.
@ -121,6 +129,7 @@ namespace Microsoft.JSInterop
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson)
{ {
AssemblyKey assemblyKey;
if (targetInstance != null) if (targetInstance != null)
{ {
if (assemblyName != 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}'."); 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 var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes);
// 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]);
}
}
try 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> /// <summary>
/// Receives notification that a call from .NET to JS has finished, marking the /// Receives notification that a call from .NET to JS has finished, marking the
/// associated <see cref="Task"/> as completed. /// 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> /// <param name="result">If <paramref name="succeeded"/> is <c>true</c>, specifies the invocation result. If <paramref name="succeeded"/> is <c>false</c>, gives the <see cref="Exception"/> corresponding to the invocation failure.</param>
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))]
public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) 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> /// <summary>
/// Releases the reference to the specified .NET object. This allows the .NET runtime /// 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))] [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))]
public static void ReleaseDotNetObject(long dotNetObjectId) public static void ReleaseDotNetObject(long dotNetObjectId)
{ {
// DotNetDispatcher only works with JSRuntimeBase instances. DotNetObjectRefManager.Current.ReleaseDotNetObject(dotNetObjectId);
var jsRuntime = (JSRuntimeBase)JSRuntime.Current;
jsRuntime.ArgSerializerStrategy.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)) if (string.IsNullOrWhiteSpace(methodIdentifier))
@ -224,23 +282,23 @@ namespace Microsoft.JSInterop
throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier)); 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)) if (assemblyMethods.TryGetValue(methodIdentifier, out var result))
{ {
return result; return result;
} }
else 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, // TODO: Consider looking first for assembly-level attributes (i.e., if there are any,
// only use those) to avoid scanning, especially for framework assemblies. // only use those) to avoid scanning, especially for framework assemblies.
var result = new Dictionary<string, (MethodInfo, Type[])>(); var result = new Dictionary<string, (MethodInfo, Type[])>(StringComparer.Ordinal);
var invokableMethods = GetRequiredLoadedAssembly(assemblyName) var invokableMethods = GetRequiredLoadedAssembly(assemblyKey)
.GetExportedTypes() .GetExportedTypes()
.SelectMany(type => type.GetMethods( .SelectMany(type => type.GetMethods(
BindingFlags.Public | BindingFlags.Public |
@ -261,7 +319,7 @@ namespace Microsoft.JSInterop
{ {
if (result.ContainsKey(identifier)) 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 " + $"[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 " + $"assembly must have different identifiers. You can pass a custom identifier as a parameter to " +
$"the [JSInvokable] attribute."); $"the [JSInvokable] attribute.");
@ -276,7 +334,7 @@ namespace Microsoft.JSInterop
return result; 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 // 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 // "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 // 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. // target assembly (from .NET) before they can invoke its allowed methods from JS.
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal)) return loadedAssemblies.FirstOrDefault(a => new AssemblyKey(a).Equals(assemblyKey))
?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'."); ?? 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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.JSInterop namespace Microsoft.JSInterop
{ {
/// <summary> /// <summary>
/// Wraps a JS interop argument, indicating that the value should not be serialized as JSON /// Provides convenience methods to produce a <see cref="DotNetObjectRef{TValue}" />.
/// 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> /// </summary>
public class DotNetObjectRef : IDisposable public static class DotNetObjectRef
{ {
/// <summary> /// <summary>
/// Gets the object instance represented by this wrapper. /// Creates a new instance of <see cref="DotNetObjectRef{TValue}" />.
/// </summary> /// </summary>
public object Value { get; } /// <param name="value">The reference type to track.</param>
/// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns>
// We track an associated IJSRuntime purely so that this class can be IDisposable public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class
// in the normal way. Developers are more likely to use objectRef.Dispose() than
// some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef).
private IJSRuntime _attachedToRuntime;
/// <summary>
/// Constructs an instance of <see cref="DotNetObjectRef"/>.
/// </summary>
/// <param name="value">The value being wrapped.</param>
public DotNetObjectRef(object value)
{ {
Value = value; return new DotNetObjectRef<TValue>(value);
}
/// <summary>
/// Ensures the <see cref="DotNetObjectRef"/> is associated with the specified <see cref="IJSRuntime"/>.
/// Developers do not normally need to invoke this manually, since it is called automatically by
/// framework code.
/// </summary>
/// <param name="runtime">The <see cref="IJSRuntime"/>.</param>
public void EnsureAttachedToJsRuntime(IJSRuntime runtime)
{
// The reason we populate _attachedToRuntime here rather than in the constructor
// is to ensure developers can't accidentally try to reuse DotNetObjectRef across
// different IJSRuntime instances. This method gets called as part of serializing
// the DotNetObjectRef during an interop call.
var existingRuntime = Interlocked.CompareExchange(ref _attachedToRuntime, runtime, null);
if (existingRuntime != null && existingRuntime != runtime)
{
throw new InvalidOperationException($"The {nameof(DotNetObjectRef)} is already associated with a different {nameof(IJSRuntime)}. Do not attempt to re-use {nameof(DotNetObjectRef)} instances with multiple {nameof(IJSRuntime)} instances.");
}
}
/// <summary>
/// Stops tracking this object reference, allowing it to be garbage collected
/// (if there are no other references to it). Once the instance is disposed, it
/// can no longer be used in interop calls from JavaScript code.
/// </summary>
public void Dispose()
{
_attachedToRuntime?.UntrackObjectRef(this);
} }
} }
} }

View File

@ -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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.JSInterop namespace Microsoft.JSInterop
@ -13,20 +14,10 @@ namespace Microsoft.JSInterop
/// <summary> /// <summary>
/// Invokes the specified JavaScript function asynchronously. /// Invokes the specified JavaScript function asynchronously.
/// </summary> /// </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="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> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
Task<T> InvokeAsync<T>(string identifier, params object[] args); Task<TValue> InvokeAsync<TValue>(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);
} }
} }

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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel;
using System.Text.Json;
namespace Microsoft.JSInterop.Internal namespace Microsoft.JSInterop.Internal
{ {
// This type takes care of a special case in handling the result of an async call from // 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> /// <summary>
/// Intended for framework use only. /// Intended for framework use only.
/// </summary> /// </summary>
public class JSAsyncCallResult [EditorBrowsable(EditorBrowsableState.Never)]
public sealed class JSAsyncCallResult
{ {
internal object ResultOrException { get; } internal JSAsyncCallResult(JsonDocument document, JsonElement jsonElement)
/// <summary>
/// Constructs an instance of <see cref="JSAsyncCallResult"/>.
/// </summary>
/// <param name="resultOrException">The result of the call.</param>
internal JSAsyncCallResult(object resultOrException)
{ {
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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Json.Serialization;
namespace Microsoft.JSInterop namespace Microsoft.JSInterop
{ {
/// <summary> /// <summary>
@ -11,14 +13,19 @@ namespace Microsoft.JSInterop
/// <summary> /// <summary>
/// Invokes the specified JavaScript function synchronously. /// Invokes the specified JavaScript function synchronously.
/// </summary> /// </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="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> /// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns> /// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public T Invoke<T>(string identifier, params object[] args) public TValue Invoke<TValue>(string identifier, params object[] args)
{ {
var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy)); var resultJson = InvokeJS(identifier, JsonSerializer.ToString(args, JsonSerializerOptionsProvider.Options));
return Json.Deserialize<T>(resultJson, ArgSerializerStrategy); if (resultJson is null)
{
return default;
}
return JsonSerializer.Parse<TValue>(resultJson, JsonSerializerOptionsProvider.Options);
} }
/// <summary> /// <summary>

View File

@ -4,8 +4,10 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop.Internal;
namespace Microsoft.JSInterop namespace Microsoft.JSInterop
{ {
@ -18,19 +20,7 @@ namespace Microsoft.JSInterop
private readonly ConcurrentDictionary<long, object> _pendingTasks private readonly ConcurrentDictionary<long, object> _pendingTasks
= new ConcurrentDictionary<long, object>(); = new ConcurrentDictionary<long, object>();
internal InteropArgSerializerStrategy ArgSerializerStrategy { get; } internal DotNetObjectRefManager ObjectRefManager { get; } = new DotNetObjectRefManager();
/// <summary>
/// Constructs an instance of <see cref="JSRuntimeBase"/>.
/// </summary>
public JSRuntimeBase()
{
ArgSerializerStrategy = new InteropArgSerializerStrategy(this);
}
/// <inheritdoc />
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef);
/// <summary> /// <summary>
/// Invokes the specified JavaScript function asynchronously. /// Invokes the specified JavaScript function asynchronously.
@ -51,9 +41,9 @@ namespace Microsoft.JSInterop
try try
{ {
var argsJson = args?.Length > 0 var argsJson = args?.Length > 0 ?
? Json.Serialize(args, ArgSerializerStrategy) JsonSerializer.ToString(args, JsonSerializerOptionsProvider.Options) :
: null; null;
BeginInvokeJS(taskId, identifier, argsJson); BeginInvokeJS(taskId, identifier, argsJson);
return tcs.Task; 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 // 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) // send back any notification (we're just providing a result for an existing async call)
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] { var args = JsonSerializer.ToString(new[] { callId, success, resultOrException }, JsonSerializerOptionsProvider.Options);
callId, BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
success,
resultOrException
}, ArgSerializerStrategy));
} }
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 (!_pendingTasks.TryRemove(asyncHandle, out var tcs))
}
if (succeeded)
{
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray)
{ {
resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType); throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'.");
} }
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException); if (succeeded)
} {
else var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
{ var result = asyncCallResult != null ?
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString())); 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> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
@ -8,6 +8,10 @@
<IsShipping>true</IsShipping> <IsShipping>true</IsShipping>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Reference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="Microsoft.JSInterop.Tests" /> <InternalsVisibleTo Include="Microsoft.JSInterop.Tests" />
</ItemGroup> </ItemGroup>

View File

@ -3,6 +3,8 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
@ -10,10 +12,7 @@ namespace Microsoft.JSInterop.Tests
{ {
public class DotNetDispatcherTest public class DotNetDispatcherTest
{ {
private readonly static string thisAssemblyName private readonly static string thisAssemblyName = typeof(DotNetDispatcherTest).Assembly.GetName().Name;
= typeof(DotNetDispatcherTest).Assembly.GetName().Name;
private readonly TestJSRuntime jsRuntime
= new TestJSRuntime();
[Fact] [Fact]
public void CannotInvokeWithEmptyAssemblyName() public void CannotInvokeWithEmptyAssemblyName()
@ -24,7 +23,7 @@ namespace Microsoft.JSInterop.Tests
}); });
Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
Assert.Equal("assemblyName", ex.ParamName); Assert.Equal("AssemblyName", ex.ParamName);
} }
[Fact] [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); 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 => public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime =>
{ {
// Arrange/Act // Arrange/Act
@ -90,7 +89,7 @@ namespace Microsoft.JSInterop.Tests
{ {
// Arrange/Act // Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null);
var result = Json.Deserialize<TestDTO>(resultJson); var result = JsonSerializer.Parse<TestDTO>(resultJson, JsonSerializerOptionsProvider.Options);
// Assert // Assert
Assert.Equal("Test", result.StringVal); Assert.Equal("Test", result.StringVal);
@ -102,50 +101,81 @@ namespace Microsoft.JSInterop.Tests
{ {
// Arrange/Act // Arrange/Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); 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
Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
Assert.Equal(456, result.IntVal); Assert.Equal(456, result.IntVal);
}); });
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] [Fact]
public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime =>
{ {
// Arrange: Track a .NET object to use as an arg // Arrange: Track a .NET object to use as an arg
var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; 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 // Arrange: Remaining args
var argsJson = Json.Serialize(new object[] { var argsJson = JsonSerializer.ToString(new object[]
{
new TestDTO { StringVal = "Another string", IntVal = 456 }, new TestDTO { StringVal = "Another string", IntVal = 456 },
new[] { 100, 200 }, new[] { 100, 200 },
"__dotNetObject:1" objectRef
}); }, JsonSerializerOptionsProvider.Options);
// Act // Act
var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); 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 // 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("ANOTHER STRING", resultDto1.StringVal);
Assert.Equal(756, resultDto1.IntVal); Assert.Equal(756, resultDto1.IntVal);
// Assert: Second result value marshalled by ref // Assert: Second result value marshalled by ref
var resultDto2Ref = (string)result[1]; var resultDto2Ref = root[1];
Assert.Equal("__dotNetObject:2", resultDto2Ref); Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _));
var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2); 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("MY STRING", resultDto2.StringVal);
Assert.Equal(1299, resultDto2.IntVal); 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 => public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
{ {
// Arrange: Track some instance // Arrange: Track some instance
var targetInstance = new SomePublicType(); var targetInstance = new SomePublicType();
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance)); var objectRef = DotNetObjectRef.Create(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef);
// Act // Act
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null);
@ -155,12 +185,13 @@ namespace Microsoft.JSInterop.Tests
Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
}); });
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] [Fact]
public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime => public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
{ {
// Arrange: Track some instance // Arrange: Track some instance
var targetInstance = new DerivedClass(); var targetInstance = new DerivedClass();
jsRuntime.Invoke<object>("unimportant", new DotNetObjectRef(targetInstance)); var objectRef = DotNetObjectRef.Create(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef);
// Act // Act
var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null);
@ -178,7 +209,7 @@ namespace Microsoft.JSInterop.Tests
// Arrange: Track some instance, then dispose it // Arrange: Track some instance, then dispose it
var targetInstance = new SomePublicType(); var targetInstance = new SomePublicType();
var objectRef = new DotNetObjectRef(targetInstance); var objectRef = DotNetObjectRef.Create(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef); jsRuntime.Invoke<object>("unimportant", objectRef);
objectRef.Dispose(); objectRef.Dispose();
@ -196,7 +227,7 @@ namespace Microsoft.JSInterop.Tests
// Arrange: Track some instance, then dispose it // Arrange: Track some instance, then dispose it
var targetInstance = new SomePublicType(); var targetInstance = new SomePublicType();
var objectRef = new DotNetObjectRef(targetInstance); var objectRef = DotNetObjectRef.Create(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef); jsRuntime.Invoke<object>("unimportant", objectRef);
DotNetDispatcher.ReleaseDotNetObject(1); DotNetDispatcher.ReleaseDotNetObject(1);
@ -206,23 +237,23 @@ namespace Microsoft.JSInterop.Tests
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); 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 => public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime =>
{ {
// Arrange: Track some instance plus another object we'll pass as a param // Arrange: Track some instance plus another object we'll pass as a param
var targetInstance = new SomePublicType(); var targetInstance = new SomePublicType();
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
jsRuntime.Invoke<object>("unimportant", jsRuntime.Invoke<object>("unimportant",
new DotNetObjectRef(targetInstance), DotNetObjectRef.Create(targetInstance),
new DotNetObjectRef(arg2)); DotNetObjectRef.Create(arg2));
var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]"; var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]";
// Act // Act
var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson);
// Assert // Assert
Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson); Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3);
Assert.Equal(1235, resultDto.IntVal); Assert.Equal(1235, resultDto.IntVal);
Assert.Equal("MY STRING", resultDto.StringVal); Assert.Equal("MY STRING", resultDto.StringVal);
}); });
@ -231,7 +262,7 @@ namespace Microsoft.JSInterop.Tests
public void CannotInvokeWithIncorrectNumberOfParams() public void CannotInvokeWithIncorrectNumberOfParams()
{ {
// Arrange // 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 // Act/Assert
var ex = Assert.Throws<ArgumentException>(() => 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); 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 => public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime =>
{ {
// Arrange: Track some instance plus another object we'll pass as a param // Arrange: Track some instance plus another object we'll pass as a param
var targetInstance = new SomePublicType(); var targetInstance = new SomePublicType();
var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; 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 // Arrange: all args
var argsJson = Json.Serialize(new object[] var argsJson = JsonSerializer.ToString(new object[]
{ {
new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
"__dotNetObject:2" arg2Ref,
}); }, JsonSerializerOptionsProvider.Options);
// Act // Act
var callId = "123"; var callId = "123";
var resultTask = jsRuntime.NextInvocationTask; var resultTask = jsRuntime.NextInvocationTask;
DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson);
await resultTask; await resultTask;
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson); var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement;
var resultValue = (SimpleJson.JsonArray)result[2]; var resultValue = result[2];
// Assert: Correct info to complete the async call // 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(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("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier);
Assert.Equal(3, result.Count); Assert.Equal(3, result.GetArrayLength());
Assert.Equal(callId, result[0]); Assert.Equal(callId, result[0].GetString());
Assert.True((bool)result[1]); // Success flag Assert.True(result[1].GetBoolean()); // Success flag
// Assert: First result value marshalled via JSON // 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("STRING VIA JSON", resultDto1.StringVal);
Assert.Equal(2000, resultDto1.IntVal); Assert.Equal(2000, resultDto1.IntVal);
// Assert: Second result value marshalled by ref // Assert: Second result value marshalled by ref
var resultDto2Ref = (string)resultValue[1]; var resultDto2Ref = JsonSerializer.Parse<DotNetObjectRef<TestDTO>>(resultValue[1].GetRawText(), JsonSerializerOptionsProvider.Options);
Assert.Equal("__dotNetObject:3", resultDto2Ref); var resultDto2 = resultDto2Ref.Value;
var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3);
Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal("MY STRING", resultDto2.StringVal);
Assert.Equal(2468, resultDto2.IntVal); Assert.Equal(2468, resultDto2.IntVal);
}); });
[Fact] [Fact]
public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime => 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. await resultTask; // This won't throw, it sets properties on the jsRuntime.
// Assert // Assert
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson); var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement;
Assert.Equal(callId, result[0]); Assert.Equal(callId, result[0].GetString());
Assert.False((bool)result[1]); // Fails Assert.False(result[1].GetBoolean()); // Fails
// Make sure the method that threw the exception shows up in the call stack // Make sure the method that threw the exception shows up in the call stack
// https://github.com/aspnet/AspNetCore/issues/8612 // https://github.com/aspnet/AspNetCore/issues/8612
var exception = (string)result[2]; var exception = result[2].GetString();
Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); 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. await resultTask; // This won't throw, it sets properties on the jsRuntime.
// Assert // Assert
var result = Json.Deserialize<SimpleJson.JsonArray>(jsRuntime.LastInvocationArgsJson); var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement;
Assert.Equal(callId, result[0]); Assert.Equal(callId, result[0].GetString());
Assert.False((bool)result[1]); // Fails Assert.False(result[1].GetBoolean()); // Fails
// Make sure the method that threw the exception shows up in the call stack // Make sure the method that threw the exception shows up in the call stack
// https://github.com/aspnet/AspNetCore/issues/8612 // https://github.com/aspnet/AspNetCore/issues/8612
var exception = (string)result[2]; var exception = result[2].GetString();
Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception);
}); });
Task WithJSRuntime(Action<TestJSRuntime> testCode) Task WithJSRuntime(Action<TestJSRuntime> testCode)
{ {
return WithJSRuntime(jsRuntime => return WithJSRuntime(jsRuntime =>
@ -379,7 +409,7 @@ namespace Microsoft.JSInterop.Tests
=> new TestDTO { StringVal = "Test", IntVal = 123 }; => new TestDTO { StringVal = "Test", IntVal = 123 };
[JSInvokable("InvocableStaticWithParams")] [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 object[]
{ {
new TestDTO // Return via JSON marshalling new TestDTO // Return via JSON marshalling
@ -387,13 +417,17 @@ namespace Microsoft.JSInterop.Tests
StringVal = dtoViaJson.StringVal.ToUpperInvariant(), StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
IntVal = dtoViaJson.IntVal + incrementAmounts.Sum() IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
}, },
new DotNetObjectRef(new TestDTO // Return by ref DotNetObjectRef.Create(new TestDTO // Return by ref
{ {
StringVal = dtoByRef.StringVal.ToUpperInvariant(), StringVal = dtoByRef.Value.StringVal.ToUpperInvariant(),
IntVal = dtoByRef.IntVal + incrementAmounts.Sum() 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] [JSInvokable]
public static TestDTO InvokableMethodWithoutCustomIdentifier() public static TestDTO InvokableMethodWithoutCustomIdentifier()
=> new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 }; => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
@ -405,14 +439,15 @@ namespace Microsoft.JSInterop.Tests
} }
[JSInvokable] [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 // Returning an array to make the point that object references
// can be embedded anywhere in the result // can be embedded anywhere in the result
return new object[] return new object[]
{ {
$"You passed {someString}", $"You passed {someString}",
new DotNetObjectRef(new TestDTO DotNetObjectRef.Create(new TestDTO
{ {
IntVal = someDTO.IntVal + 1, IntVal = someDTO.IntVal + 1,
StringVal = someDTO.StringVal.ToUpperInvariant() StringVal = someDTO.StringVal.ToUpperInvariant()
@ -421,9 +456,10 @@ namespace Microsoft.JSInterop.Tests
} }
[JSInvokable] [JSInvokable]
public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef) public async Task<object[]> InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectRef<TestDTO> dtoByRefWrapper)
{ {
await Task.Delay(50); await Task.Delay(50);
var dtoByRef = dtoByRefWrapper.Value;
return new object[] return new object[]
{ {
new TestDTO // Return via JSON new TestDTO // Return via JSON
@ -431,7 +467,7 @@ namespace Microsoft.JSInterop.Tests
StringVal = dtoViaJson.StringVal.ToUpperInvariant(), StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
IntVal = dtoViaJson.IntVal * 2, IntVal = dtoViaJson.IntVal * 2,
}, },
new DotNetObjectRef(new TestDTO // Return by ref DotNetObjectRef.Create(new TestDTO // Return by ref
{ {
StringVal = dtoByRef.StringVal.ToUpperInvariant(), StringVal = dtoByRef.StringVal.ToUpperInvariant(),
IntVal = dtoByRef.IntVal * 2, 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
@ -11,58 +10,44 @@ namespace Microsoft.JSInterop.Tests
public class DotNetObjectRefTest public class DotNetObjectRefTest
{ {
[Fact] [Fact]
public void CanAccessValue() public Task CanAccessValue() => WithJSRuntime(_ =>
{ {
var obj = new object(); var obj = new object();
Assert.Same(obj, new DotNetObjectRef(obj).Value); Assert.Same(obj, DotNetObjectRef.Create(obj).Value);
} });
[Fact] [Fact]
public void CanAssociateWithSameRuntimeMultipleTimes() public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime =>
{
var objRef = new DotNetObjectRef(new object());
var jsRuntime = new TestJsRuntime();
objRef.EnsureAttachedToJsRuntime(jsRuntime);
objRef.EnsureAttachedToJsRuntime(jsRuntime);
}
[Fact]
public void CannotAssociateWithDifferentRuntimes()
{
var objRef = new DotNetObjectRef(new object());
var jsRuntime1 = new TestJsRuntime();
var jsRuntime2 = new TestJsRuntime();
objRef.EnsureAttachedToJsRuntime(jsRuntime1);
var ex = Assert.Throws<InvalidOperationException>(
() => objRef.EnsureAttachedToJsRuntime(jsRuntime2));
Assert.Contains("Do not attempt to re-use", ex.Message);
}
[Fact]
public void NotifiesAssociatedJsRuntimeOfDisposal()
{ {
// Arrange // Arrange
var objRef = new DotNetObjectRef(new object()); var objRef = DotNetObjectRef.Create(new object());
var jsRuntime = new TestJsRuntime(); var trackingId = objRef.__dotNetObject;
objRef.EnsureAttachedToJsRuntime(jsRuntime);
// Act // Act
objRef.Dispose(); objRef.Dispose();
// Assert // 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) var runtime = new TestJSRuntime();
=> throw new NotImplementedException(); JSRuntime.SetCurrentJSRuntime(runtime);
testCode(runtime);
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
=> UntrackedRefs.Add(dotNetObjectRef);
} }
} }
} }

View File

@ -10,15 +10,15 @@ namespace Microsoft.JSInterop.Tests
{ {
public class JSInProcessRuntimeBaseTest public class JSInProcessRuntimeBaseTest
{ {
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1807#issuecomment-470756811")] [Fact]
public void DispatchesSyncCallsAndDeserializesResults() public void DispatchesSyncCallsAndDeserializesResults()
{ {
// Arrange // Arrange
var runtime = new TestJSInProcessRuntime var runtime = new TestJSInProcessRuntime
{ {
NextResultJson = Json.Serialize( NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}"
new TestDTO { IntValue = 123, StringValue = "Hello" })
}; };
JSRuntime.SetCurrentJSRuntime(runtime);
// Act // Act
var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true); var syncResult = runtime.Invoke<TestDTO>("test identifier 1", "arg1", 123, true);
@ -36,18 +36,19 @@ namespace Microsoft.JSInterop.Tests
{ {
// Arrange // Arrange
var runtime = new TestJSInProcessRuntime { NextResultJson = null }; var runtime = new TestJSInProcessRuntime { NextResultJson = null };
JSRuntime.SetCurrentJSRuntime(runtime);
var obj1 = new object(); var obj1 = new object();
var obj2 = new object(); var obj2 = new object();
var obj3 = new object(); var obj3 = new object();
// Act // Act
// Showing we can pass the DotNetObject either as top-level args or nested // Showing we can pass the DotNetObject either as top-level args or nested
var syncResult = runtime.Invoke<object>("test identifier", var syncResult = runtime.Invoke<DotNetObjectRef<object>>("test identifier",
new DotNetObjectRef(obj1), DotNetObjectRef.Create(obj1),
new Dictionary<string, object> new Dictionary<string, object>
{ {
{ "obj2", new DotNetObjectRef(obj2) }, { "obj2", DotNetObjectRef.Create(obj2) },
{ "obj3", new DotNetObjectRef(obj3) } { "obj3", DotNetObjectRef.Create(obj3) },
}); });
// Assert: Handles null result string // Assert: Handles null result string
@ -56,12 +57,12 @@ namespace Microsoft.JSInterop.Tests
// Assert: Serialized as expected // Assert: Serialized as expected
var call = runtime.InvokeCalls.Single(); var call = runtime.InvokeCalls.Single();
Assert.Equal("test identifier", call.Identifier); 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: Objects were tracked
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1));
Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2));
Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3));
} }
[Fact] [Fact]
@ -70,20 +71,22 @@ namespace Microsoft.JSInterop.Tests
// Arrange // Arrange
var runtime = new TestJSInProcessRuntime var runtime = new TestJSInProcessRuntime
{ {
NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]" NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]"
}; };
JSRuntime.SetCurrentJSRuntime(runtime);
var obj1 = new object(); var obj1 = new object();
var obj2 = new object(); var obj2 = new object();
// Act // Act
var syncResult = runtime.Invoke<object[]>("test identifier", var syncResult = runtime.Invoke<DotNetObjectRef<object>[]>(
new DotNetObjectRef(obj1), "test identifier",
DotNetObjectRef.Create(obj1),
"some other arg", "some other arg",
new DotNetObjectRef(obj2)); DotNetObjectRef.Create(obj2));
var call = runtime.InvokeCalls.Single(); var call = runtime.InvokeCalls.Single();
// Assert // Assert
Assert.Equal(new[] { obj2, obj1 }, syncResult); Assert.Equal(new[] { obj2, obj1 }, syncResult.Select(r => r.Value));
} }
class TestDTO class TestDTO

View File

@ -1,10 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json;
using Microsoft.JSInterop.Internal;
using Xunit; using Xunit;
namespace Microsoft.JSInterop.Tests namespace Microsoft.JSInterop.Tests
@ -47,12 +48,13 @@ namespace Microsoft.JSInterop.Tests
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>()); var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
Assert.False(unrelatedTask.IsCompleted); Assert.False(unrelatedTask.IsCompleted);
Assert.False(task.IsCompleted); Assert.False(task.IsCompleted);
using var jsonDocument = JsonDocument.Parse("\"my result\"");
// Act/Assert: Task can be completed // Act/Assert: Task can be completed
runtime.OnEndInvoke( runtime.OnEndInvoke(
runtime.BeginInvokeCalls[1].AsyncHandle, runtime.BeginInvokeCalls[1].AsyncHandle,
/* succeeded: */ true, /* succeeded: */ true,
"my result"); new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement));
Assert.False(unrelatedTask.IsCompleted); Assert.False(unrelatedTask.IsCompleted);
Assert.True(task.IsCompleted); Assert.True(task.IsCompleted);
Assert.Equal("my result", task.Result); Assert.Equal("my result", task.Result);
@ -69,12 +71,13 @@ namespace Microsoft.JSInterop.Tests
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>()); var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
Assert.False(unrelatedTask.IsCompleted); Assert.False(unrelatedTask.IsCompleted);
Assert.False(task.IsCompleted); Assert.False(task.IsCompleted);
using var jsonDocument = JsonDocument.Parse("\"This is a test exception\"");
// Act/Assert: Task can be failed // Act/Assert: Task can be failed
runtime.OnEndInvoke( runtime.OnEndInvoke(
runtime.BeginInvokeCalls[1].AsyncHandle, runtime.BeginInvokeCalls[1].AsyncHandle,
/* succeeded: */ false, /* succeeded: */ false,
"This is a test exception"); new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement));
Assert.False(unrelatedTask.IsCompleted); Assert.False(unrelatedTask.IsCompleted);
Assert.True(task.IsCompleted); Assert.True(task.IsCompleted);
@ -106,20 +109,21 @@ namespace Microsoft.JSInterop.Tests
{ {
// Arrange // Arrange
var runtime = new TestJSRuntime(); var runtime = new TestJSRuntime();
JSRuntime.SetCurrentJSRuntime(runtime);
var obj1 = new object(); var obj1 = new object();
var obj2 = new object(); var obj2 = new object();
var obj3 = new object(); var obj3 = new object();
// Act // Act
// Showing we can pass the DotNetObject either as top-level args or nested // Showing we can pass the DotNetObject either as top-level args or nested
var obj1Ref = new DotNetObjectRef(obj1); var obj1Ref = DotNetObjectRef.Create(obj1);
var obj1DifferentRef = new DotNetObjectRef(obj1); var obj1DifferentRef = DotNetObjectRef.Create(obj1);
runtime.InvokeAsync<object>("test identifier", runtime.InvokeAsync<object>("test identifier",
obj1Ref, obj1Ref,
new Dictionary<string, object> new Dictionary<string, object>
{ {
{ "obj2", new DotNetObjectRef(obj2) }, { "obj2", DotNetObjectRef.Create(obj2) },
{ "obj3", new DotNetObjectRef(obj3) }, { "obj3", DotNetObjectRef.Create(obj3) },
{ "obj1SameRef", obj1Ref }, { "obj1SameRef", obj1Ref },
{ "obj1DifferentRef", obj1DifferentRef }, { "obj1DifferentRef", obj1DifferentRef },
}); });
@ -127,28 +131,13 @@ namespace Microsoft.JSInterop.Tests
// Assert: Serialized as expected // Assert: Serialized as expected
var call = runtime.BeginInvokeCalls.Single(); var call = runtime.BeginInvokeCalls.Single();
Assert.Equal("test identifier", call.Identifier); 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: Objects were tracked
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1));
Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2));
Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3));
Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4)); Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4));
}
[Fact]
public void SupportsCustomSerializationForArguments()
{
// Arrange
var runtime = new TestJSRuntime();
// Arrange/Act
runtime.InvokeAsync<object>("test identifier",
new WithCustomArgSerializer());
// Asssert
var call = runtime.BeginInvokeCalls.Single();
Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson);
} }
class TestJSRuntime : JSRuntimeBase class TestJSRuntime : JSRuntimeBase
@ -172,20 +161,8 @@ namespace Microsoft.JSInterop.Tests
}); });
} }
public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException) public void OnEndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult callResult)
=> EndInvokeJS(asyncHandle, succeeded, resultOrException); => EndInvokeJS(asyncHandle, succeeded, callResult);
}
class WithCustomArgSerializer : ICustomArgSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 123 },
};
}
} }
} }
} }

View File

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

View File

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