diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index 4b5d409d0f..5386b92bb5 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -264,17 +264,14 @@ module DotNet { } public serializeAsArg() { - return `__dotNetObject:${this._id}`; + return {__dotNetObject: this._id}; } } - const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/; + const dotNetObjectRefKey = '__dotNetObject'; attachReviver(function reviveDotNetObject(key: any, value: any) { - if (typeof value === 'string') { - const match = value.match(dotNetObjectValueFormat); - if (match) { - return new DotNetObject(parseInt(match[1])); - } + if (value && typeof value === 'object' && value.hasOwnProperty(dotNetObjectRefKey)) { + return new DotNetObject(value.__dotNetObject); } // Unrecognized - let another reviver handle it diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj index 87fd913427..5f83c53091 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj @@ -5,6 +5,6 @@ - + diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs index 95b9b7956c..1db3385bd7 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -12,12 +12,19 @@ namespace Microsoft.JSInterop [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] public static void ReleaseDotNetObject(long dotNetObjectId) { } } - public partial class DotNetObjectRef : System.IDisposable + public static partial class DotNetObjectRef { - public DotNetObjectRef(object value) { } - public object Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public static Microsoft.JSInterop.DotNetObjectRef Create(TValue value) where TValue : class { throw null; } + } + public sealed partial class DotNetObjectRef : System.IDisposable where TValue : class + { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public DotNetObjectRef() { } + [System.Text.Json.Serialization.JsonIgnoreAttribute] + public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public long __dotNetObject { get { throw null; } set { } } public void Dispose() { } - public void EnsureAttachedToJsRuntime(Microsoft.JSInterop.IJSRuntime runtime) { } } public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime { @@ -25,8 +32,7 @@ namespace Microsoft.JSInterop } public partial interface IJSRuntime { - System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args); - void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef); + System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args); } public partial class JSException : System.Exception { @@ -36,7 +42,7 @@ namespace Microsoft.JSInterop { protected JSInProcessRuntimeBase() { } protected abstract string InvokeJS(string identifier, string argsJson); - public T Invoke(string identifier, params object[] args) { throw null; } + public TValue Invoke(string identifier, params object[] args) { throw null; } } [System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true)] public partial class JSInvokableAttribute : System.Attribute @@ -45,30 +51,21 @@ namespace Microsoft.JSInterop public JSInvokableAttribute(string identifier) { } public string Identifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } - public static partial class Json - { - public static T Deserialize(string json) { throw null; } - public static string Serialize(object value) { throw null; } - } public static partial class JSRuntime { public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } } public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime { - public JSRuntimeBase() { } + protected JSRuntimeBase() { } protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson); public System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args) { throw null; } - public void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef) { } } } namespace Microsoft.JSInterop.Internal { - public partial interface ICustomArgSerializer - { - object ToJsonPrimitive(); - } - public partial class JSAsyncCallResult + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public sealed partial class JSAsyncCallResult { internal JSAsyncCallResult() { } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs index d6c83d512d..5ecd3506e0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs @@ -1,14 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.JSInterop.Internal; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; +using Microsoft.JSInterop.Internal; namespace Microsoft.JSInterop { @@ -17,8 +19,10 @@ namespace Microsoft.JSInterop /// public static class DotNetDispatcher { - private static ConcurrentDictionary> _cachedMethodsByAssembly - = new ConcurrentDictionary>(); + internal const string DotNetObjectRefKey = nameof(DotNetObjectRef.__dotNetObject); + + private static readonly ConcurrentDictionary> _cachedMethodsByAssembly + = new ConcurrentDictionary>(); /// /// Receives a call from JS to .NET, locating and invoking the specified method. @@ -35,17 +39,19 @@ namespace Microsoft.JSInterop // the targeted method has [JSInvokable]. It is not itself subject to that restriction, // because there would be nobody to police that. This method *is* the police. - // DotNetDispatcher only works with JSRuntimeBase instances. - var jsRuntime = (JSRuntimeBase)JSRuntime.Current; - var targetInstance = (object)null; if (dotNetObjectId != default) { - targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); + targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); } var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); - return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy); + if (syncResult == null) + { + return null; + } + + return JsonSerializer.ToString(syncResult, JsonSerializerOptionsProvider.Options); } /// @@ -69,9 +75,11 @@ namespace Microsoft.JSInterop // code has to implement its own way of returning async results. var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current; - var targetInstance = dotNetObjectId == default - ? null - : jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); + var targetInstance = (object)null; + if (dotNetObjectId != default) + { + targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); + } // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. @@ -121,6 +129,7 @@ namespace Microsoft.JSInterop private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) { + AssemblyKey assemblyKey; if (targetInstance != null) { if (assemblyName != null) @@ -128,44 +137,16 @@ namespace Microsoft.JSInterop throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'."); } - assemblyName = targetInstance.GetType().Assembly.GetName().Name; + assemblyKey = new AssemblyKey(targetInstance.GetType().Assembly); + } + else + { + assemblyKey = new AssemblyKey(assemblyName); } - var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier); + var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier); - // There's no direct way to say we want to deserialize as an array with heterogenous - // entry types (e.g., [string, int, bool]), so we need to deserialize in two phases. - // First we deserialize as object[], for which SimpleJson will supply JsonObject - // instances for nonprimitive values. - var suppliedArgs = (object[])null; - var suppliedArgsLength = 0; - if (argsJson != null) - { - suppliedArgs = Json.Deserialize(argsJson).ToArray(); - suppliedArgsLength = suppliedArgs.Length; - } - if (suppliedArgsLength != parameterTypes.Length) - { - throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}."); - } - - // Second, convert each supplied value to the type expected by the method - var runtime = (JSRuntimeBase)JSRuntime.Current; - var serializerStrategy = runtime.ArgSerializerStrategy; - for (var i = 0; i < suppliedArgsLength; i++) - { - if (parameterTypes[i] == typeof(JSAsyncCallResult)) - { - // For JS async call results, we have to defer the deserialization until - // later when we know what type it's meant to be deserialized as - suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]); - } - else - { - suppliedArgs[i] = serializerStrategy.DeserializeObject( - suppliedArgs[i], parameterTypes[i]); - } - } + var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes); try { @@ -183,6 +164,85 @@ namespace Microsoft.JSInterop } } + private static object[] ParseArguments(string methodIdentifier, string argsJson, Type[] parameterTypes) + { + if (parameterTypes.Length == 0) + { + return Array.Empty(); + } + + // 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 at the top level. We know it's + // an incorrect use if there's a object that looks like { '__dotNetObject': }, + // but we aren't assigning to DotNetObjectRef{T}. + return item.Type == JsonValueType.Object && + item.TryGetProperty(DotNetObjectRefKey, out _) && + !typeof(IDotNetObjectRef).IsAssignableFrom(parameterType); + } + } + /// /// Receives notification that a call from .NET to JS has finished, marking the /// associated as completed. @@ -192,7 +252,7 @@ namespace Microsoft.JSInterop /// If is true, specifies the invocation result. If is false, gives the corresponding to the invocation failure. [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) - => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException); + => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result); /// /// Releases the reference to the specified .NET object. This allows the .NET runtime @@ -207,16 +267,14 @@ namespace Microsoft.JSInterop [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))] public static void ReleaseDotNetObject(long dotNetObjectId) { - // DotNetDispatcher only works with JSRuntimeBase instances. - var jsRuntime = (JSRuntimeBase)JSRuntime.Current; - jsRuntime.ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectId); + DotNetObjectRefManager.Current.ReleaseDotNetObject(dotNetObjectId); } - private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier) + private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier) { - if (string.IsNullOrWhiteSpace(assemblyName)) + if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName)) { - throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyName)); + throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyKey.AssemblyName)); } if (string.IsNullOrWhiteSpace(methodIdentifier)) @@ -224,23 +282,23 @@ namespace Microsoft.JSInterop throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier)); } - var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyName, ScanAssemblyForCallableMethods); + var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyKey, ScanAssemblyForCallableMethods); if (assemblyMethods.TryGetValue(methodIdentifier, out var result)) { return result; } else { - throw new ArgumentException($"The assembly '{assemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")]."); + throw new ArgumentException($"The assembly '{assemblyKey.AssemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")]."); } } - private static IReadOnlyDictionary ScanAssemblyForCallableMethods(string assemblyName) + private static Dictionary ScanAssemblyForCallableMethods(AssemblyKey assemblyKey) { // TODO: Consider looking first for assembly-level attributes (i.e., if there are any, // only use those) to avoid scanning, especially for framework assemblies. - var result = new Dictionary(); - var invokableMethods = GetRequiredLoadedAssembly(assemblyName) + var result = new Dictionary(StringComparer.Ordinal); + var invokableMethods = GetRequiredLoadedAssembly(assemblyKey) .GetExportedTypes() .SelectMany(type => type.GetMethods( BindingFlags.Public | @@ -261,7 +319,7 @@ namespace Microsoft.JSInterop { if (result.ContainsKey(identifier)) { - throw new InvalidOperationException($"The assembly '{assemblyName}' contains more than one " + + throw new InvalidOperationException($"The assembly '{assemblyKey.AssemblyName}' contains more than one " + $"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " + $"assembly must have different identifiers. You can pass a custom identifier as a parameter to " + $"the [JSInvokable] attribute."); @@ -276,7 +334,7 @@ namespace Microsoft.JSInterop return result; } - private static Assembly GetRequiredLoadedAssembly(string assemblyName) + private static Assembly GetRequiredLoadedAssembly(AssemblyKey assemblyKey) { // We don't want to load assemblies on demand here, because we don't necessarily trust // "assemblyName" to be something the developer intended to load. So only pick from the @@ -284,8 +342,40 @@ namespace Microsoft.JSInterop // In some edge cases this might force developers to explicitly call something on the // target assembly (from .NET) before they can invoke its allowed methods from JS. var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal)) - ?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'."); + return loadedAssemblies.FirstOrDefault(a => new AssemblyKey(a).Equals(assemblyKey)) + ?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyKey.AssemblyName}'."); } + + private readonly struct AssemblyKey : IEquatable + { + 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); + } + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs index aa62bee341..1aabc5ad59 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs @@ -1,66 +1,21 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Threading; - namespace Microsoft.JSInterop { /// - /// Wraps a JS interop argument, indicating that the value should not be serialized as JSON - /// but instead should be passed as a reference. - /// - /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. + /// Provides convenience methods to produce a . /// - public class DotNetObjectRef : IDisposable + public static class DotNetObjectRef { /// - /// Gets the object instance represented by this wrapper. + /// Creates a new instance of . /// - public object Value { get; } - - // We track an associated IJSRuntime purely so that this class can be IDisposable - // in the normal way. Developers are more likely to use objectRef.Dispose() than - // some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef). - private IJSRuntime _attachedToRuntime; - - /// - /// Constructs an instance of . - /// - /// The value being wrapped. - public DotNetObjectRef(object value) + /// The reference type to track. + /// An instance of . + public static DotNetObjectRef Create(TValue value) where TValue : class { - Value = value; - } - - /// - /// Ensures the is associated with the specified . - /// Developers do not normally need to invoke this manually, since it is called automatically by - /// framework code. - /// - /// The . - 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."); - } - } - - /// - /// 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. - /// - public void Dispose() - { - _attachedToRuntime?.UntrackObjectRef(this); + return new DotNetObjectRef(value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs new file mode 100644 index 0000000000..f263716f53 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs @@ -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 _trackedRefsById = new ConcurrentDictionary(); + + 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)); + + } + + /// + /// 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 + /// + /// The ID of the . + public void ReleaseDotNetObject(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs new file mode 100644 index 0000000000..8b7035e957 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs @@ -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 +{ + /// + /// 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. + /// + /// The type of the value to wrap. + public sealed class DotNetObjectRef : IDotNetObjectRef, IDisposable where TValue : class + { + private long? _trackingId; + + /// + /// This API is for meant for JSON deserialization and should not be used by user code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DotNetObjectRef() + { + } + + /// + /// Initializes a new instance of . + /// + /// The value to pass by reference. + internal DotNetObjectRef(TValue value) + { + Value = value; + _trackingId = DotNetObjectRefManager.Current.TrackObject(this); + } + + /// + /// Gets the object instance represented by this wrapper. + /// + [JsonIgnore] + public TValue Value { get; private set; } + + /// + /// This API is for meant for JSON serialization and should not be used by user code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public long __dotNetObject + { + get => _trackingId.Value; + set + { + if (_trackingId != null) + { + throw new InvalidOperationException($"{nameof(DotNetObjectRef)} cannot be reinitialized."); + } + + _trackingId = value; + Value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(value); + } + } + + object IDotNetObjectRef.Value => Value; + + /// + /// 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. + /// + public void Dispose() + { + DotNetObjectRefManager.Current.ReleaseDotNetObject(_trackingId.Value); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/ICustomArgSerializer.cs b/src/JSInterop/Microsoft.JSInterop/src/ICustomArgSerializer.cs deleted file mode 100644 index f4012af8e9..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/ICustomArgSerializer.cs +++ /dev/null @@ -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). - - /// - /// Internal. Intended for framework use only. - /// - public interface ICustomArgSerializer - { - /// - /// Internal. Intended for framework use only. - /// - object ToJsonPrimitive(); - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs new file mode 100644 index 0000000000..5f21808a9f --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs @@ -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; } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index b56d1f0089..97713bb3c1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading.Tasks; namespace Microsoft.JSInterop @@ -13,20 +14,10 @@ namespace Microsoft.JSInterop /// /// Invokes the specified JavaScript function asynchronously. /// - /// The JSON-serializable return type. + /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - Task InvokeAsync(string identifier, params object[] args); - - /// - /// Stops tracking the .NET object represented by the . - /// 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. - /// - /// The reference to stop tracking. - /// This method is called automatically by . - void UntrackObjectRef(DotNetObjectRef dotNetObjectRef); + /// An instance of obtained by JSON-deserializing the return value. + Task InvokeAsync(string identifier, params object[] args); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/InteropArgSerializerStrategy.cs b/src/JSInterop/Microsoft.JSInterop/src/InteropArgSerializerStrategy.cs deleted file mode 100644 index 663c1cf85a..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/InteropArgSerializerStrategy.cs +++ /dev/null @@ -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 _trackedRefsById = new Dictionary(); - private Dictionary _trackedIdsByRef = new Dictionary(); - - 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)); - } - } - - /// - /// Stops tracking the specified .NET object reference. - /// This overload is typically invoked from JS code via JS interop. - /// - /// The ID of the . - public void ReleaseDotNetObject(long dotNetObjectId) - { - lock (_storageLock) - { - if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)) - { - _trackedRefsById.Remove(dotNetObjectId); - _trackedIdsByRef.Remove(dotNetObjectRef); - } - } - } - - /// - /// Stops tracking the specified .NET object reference. - /// This overload is typically invoked from .NET code by . - /// - /// The . - 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); - } - } - } - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs b/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs index d46517eddc..1ea8c47995 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs @@ -1,6 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.ComponentModel; +using System.Text.Json; + namespace Microsoft.JSInterop.Internal { // This type takes care of a special case in handling the result of an async call from @@ -20,17 +23,16 @@ namespace Microsoft.JSInterop.Internal /// /// Intended for framework use only. /// - public class JSAsyncCallResult + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class JSAsyncCallResult { - internal object ResultOrException { get; } - - /// - /// Constructs an instance of . - /// - /// The result of the call. - internal JSAsyncCallResult(object resultOrException) + internal JSAsyncCallResult(JsonDocument document, JsonElement jsonElement) { - ResultOrException = resultOrException; + JsonDocument = document; + JsonElement = jsonElement; } + + internal JsonElement JsonElement { get; } + internal JsonDocument JsonDocument { get; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs index 49a47d0595..7e6dcdd462 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Text.Json.Serialization; + namespace Microsoft.JSInterop { /// @@ -11,14 +13,19 @@ namespace Microsoft.JSInterop /// /// Invokes the specified JavaScript function synchronously. /// - /// The JSON-serializable return type. + /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - public T Invoke(string identifier, params object[] args) + /// An instance of obtained by JSON-deserializing the return value. + public TValue Invoke(string identifier, params object[] args) { - var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy)); - return Json.Deserialize(resultJson, ArgSerializerStrategy); + var resultJson = InvokeJS(identifier, JsonSerializer.ToString(args, JsonSerializerOptionsProvider.Options)); + if (resultJson is null) + { + return default; + } + + return JsonSerializer.Parse(resultJson, JsonSerializerOptionsProvider.Options); } /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs index d18bc7f4fe..70ab856a89 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Concurrent; using System.Runtime.ExceptionServices; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Microsoft.JSInterop.Internal; namespace Microsoft.JSInterop { @@ -18,19 +20,7 @@ namespace Microsoft.JSInterop private readonly ConcurrentDictionary _pendingTasks = new ConcurrentDictionary(); - internal InteropArgSerializerStrategy ArgSerializerStrategy { get; } - - /// - /// Constructs an instance of . - /// - public JSRuntimeBase() - { - ArgSerializerStrategy = new InteropArgSerializerStrategy(this); - } - - /// - public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) - => ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef); + internal DotNetObjectRefManager ObjectRefManager { get; } = new DotNetObjectRefManager(); /// /// Invokes the specified JavaScript function asynchronously. @@ -51,9 +41,9 @@ namespace Microsoft.JSInterop try { - var argsJson = args?.Length > 0 - ? Json.Serialize(args, ArgSerializerStrategy) - : null; + var argsJson = args?.Length > 0 ? + JsonSerializer.ToString(args, JsonSerializerOptionsProvider.Options) : + null; BeginInvokeJS(taskId, identifier, argsJson); return tcs.Task; } @@ -88,33 +78,32 @@ namespace Microsoft.JSInterop // We pass 0 as the async handle because we don't want the JS-side code to // send back any notification (we're just providing a result for an existing async call) - BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] { - callId, - success, - resultOrException - }, ArgSerializerStrategy)); + var args = JsonSerializer.ToString(new[] { callId, success, resultOrException }, JsonSerializerOptionsProvider.Options); + BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); } - internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException) + internal void EndInvokeJS(long asyncHandle, bool succeeded, JSAsyncCallResult asyncCallResult) { - if (!_pendingTasks.TryRemove(asyncHandle, out var tcs)) + using (asyncCallResult?.JsonDocument) { - throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'."); - } - - if (succeeded) - { - var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); - if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray) + if (!_pendingTasks.TryRemove(asyncHandle, out var tcs)) { - resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType); + throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'."); } - TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException); - } - else - { - TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString())); + if (succeeded) + { + var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); + var result = asyncCallResult != null ? + JsonSerializer.Parse(asyncCallResult.JsonElement.GetRawText(), resultType, JsonSerializerOptionsProvider.Options) : + null; + TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); + } + else + { + var exceptionText = asyncCallResult?.JsonElement.ToString() ?? string.Empty; + TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText)); + } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/CamelCase.cs b/src/JSInterop/Microsoft.JSInterop/src/Json/CamelCase.cs deleted file mode 100644 index 8caae1387b..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Json/CamelCase.cs +++ /dev/null @@ -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); - } - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/Json.cs b/src/JSInterop/Microsoft.JSInterop/src/Json/Json.cs deleted file mode 100644 index 7275dfe427..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Json/Json.cs +++ /dev/null @@ -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 -{ - /// - /// Provides mechanisms for converting between .NET objects and JSON strings for use - /// when making calls to JavaScript functions via . - /// - /// Warning: This is not intended as a general-purpose JSON library. It is only intended - /// for use when making calls via . Eventually its implementation - /// will be replaced by something more general-purpose. - /// - public static class Json - { - /// - /// Serializes the value as a JSON string. - /// - /// The value to serialize. - /// The JSON string. - public static string Serialize(object value) - => SimpleJson.SimpleJson.SerializeObject(value); - - internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy) - => SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy); - - /// - /// Deserializes the JSON string, creating an object of the specified generic type. - /// - /// The type of object to create. - /// The JSON string. - /// An object of the specified type. - public static T Deserialize(string json) - => SimpleJson.SimpleJson.DeserializeObject(json); - - internal static T Deserialize(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy) - => SimpleJson.SimpleJson.DeserializeObject(json, serializerStrategy); - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/README.txt b/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/README.txt deleted file mode 100644 index 5e58eb7106..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/README.txt +++ /dev/null @@ -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. \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/SimpleJson.cs b/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/SimpleJson.cs deleted file mode 100644 index d12c6fae30..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/SimpleJson.cs +++ /dev/null @@ -1,2201 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) 2011, The Outercurve Foundation. -// -// Licensed under the MIT License (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.opensource.org/licenses/mit-license.php -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Nathan Totten (ntotten.com), Jim Zimmerman (jimzimmerman.com) and Prabir Shrestha (prabir.me) -// https://github.com/facebook-csharp-sdk/simple-json -//----------------------------------------------------------------------- - -// VERSION: - -// NOTE: uncomment the following line to make SimpleJson class internal. -#define SIMPLE_JSON_INTERNAL - -// NOTE: uncomment the following line to make JsonArray and JsonObject class internal. -#define SIMPLE_JSON_OBJARRAYINTERNAL - -// NOTE: uncomment the following line to enable dynamic support. -//#define SIMPLE_JSON_DYNAMIC - -// NOTE: uncomment the following line to enable DataContract support. -//#define SIMPLE_JSON_DATACONTRACT - -// NOTE: uncomment the following line to enable IReadOnlyCollection and IReadOnlyList support. -//#define SIMPLE_JSON_READONLY_COLLECTIONS - -// NOTE: uncomment the following line to disable linq expressions/compiled lambda (better performance) instead of method.invoke(). -// define if you are using .net framework <= 3.0 or < WP7.5 -#define SIMPLE_JSON_NO_LINQ_EXPRESSION - -// NOTE: uncomment the following line if you are compiling under Window Metro style application/library. -// usually already defined in properties -//#define NETFX_CORE; - -// If you are targetting WinStore, WP8 and NET4.5+ PCL make sure to #define SIMPLE_JSON_TYPEINFO; - -// original json parsing code from http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html - -#if NETFX_CORE -#define SIMPLE_JSON_TYPEINFO -#endif - -using System; -using System.CodeDom.Compiler; -using System.Collections; -using System.Collections.Generic; -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION -using System.Linq.Expressions; -#endif -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -#if SIMPLE_JSON_DYNAMIC -using System.Dynamic; -#endif -using System.Globalization; -using System.Reflection; -using System.Runtime.Serialization; -using System.Text; -using Microsoft.JSInterop; -using SimpleJson.Reflection; - -// ReSharper disable LoopCanBeConvertedToQuery -// ReSharper disable RedundantExplicitArrayCreation -// ReSharper disable SuggestUseVarKeywordEvident -namespace SimpleJson -{ - /// - /// Represents the json array. - /// - [GeneratedCode("simple-json", "1.0.0")] - [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] -#if SIMPLE_JSON_OBJARRAYINTERNAL - internal -#else - public -#endif - class JsonArray : List - { - /// - /// Initializes a new instance of the class. - /// - public JsonArray() { } - - /// - /// Initializes a new instance of the class. - /// - /// The capacity of the json array. - public JsonArray(int capacity) : base(capacity) { } - - /// - /// The json representation of the array. - /// - /// The json representation of the array. - public override string ToString() - { - return SimpleJson.SerializeObject(this) ?? string.Empty; - } - } - - /// - /// Represents the json object. - /// - [GeneratedCode("simple-json", "1.0.0")] - [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] -#if SIMPLE_JSON_OBJARRAYINTERNAL - internal -#else - public -#endif - class JsonObject : -#if SIMPLE_JSON_DYNAMIC - DynamicObject, -#endif - IDictionary - { - /// - /// The internal member dictionary. - /// - private readonly Dictionary _members; - - /// - /// Initializes a new instance of . - /// - public JsonObject() - { - _members = new Dictionary(); - } - - /// - /// Initializes a new instance of . - /// - /// The implementation to use when comparing keys, or null to use the default for the type of the key. - public JsonObject(IEqualityComparer comparer) - { - _members = new Dictionary(comparer); - } - - /// - /// Gets the at the specified index. - /// - /// - public object this[int index] - { - get { return GetAtIndex(_members, index); } - } - - internal static object GetAtIndex(IDictionary obj, int index) - { - if (obj == null) - throw new ArgumentNullException("obj"); - if (index >= obj.Count) - throw new ArgumentOutOfRangeException("index"); - int i = 0; - foreach (KeyValuePair o in obj) - if (i++ == index) return o.Value; - return null; - } - - /// - /// Adds the specified key. - /// - /// The key. - /// The value. - public void Add(string key, object value) - { - _members.Add(key, value); - } - - /// - /// Determines whether the specified key contains key. - /// - /// The key. - /// - /// true if the specified key contains key; otherwise, false. - /// - public bool ContainsKey(string key) - { - return _members.ContainsKey(key); - } - - /// - /// Gets the keys. - /// - /// The keys. - public ICollection Keys - { - get { return _members.Keys; } - } - - /// - /// Removes the specified key. - /// - /// The key. - /// - public bool Remove(string key) - { - return _members.Remove(key); - } - - /// - /// Tries the get value. - /// - /// The key. - /// The value. - /// - public bool TryGetValue(string key, out object value) - { - return _members.TryGetValue(key, out value); - } - - /// - /// Gets the values. - /// - /// The values. - public ICollection Values - { - get { return _members.Values; } - } - - /// - /// Gets or sets the with the specified key. - /// - /// - public object this[string key] - { - get { return _members[key]; } - set { _members[key] = value; } - } - - /// - /// Adds the specified item. - /// - /// The item. - public void Add(KeyValuePair item) - { - _members.Add(item.Key, item.Value); - } - - /// - /// Clears this instance. - /// - public void Clear() - { - _members.Clear(); - } - - /// - /// Determines whether [contains] [the specified item]. - /// - /// The item. - /// - /// true if [contains] [the specified item]; otherwise, false. - /// - public bool Contains(KeyValuePair item) - { - return _members.ContainsKey(item.Key) && _members[item.Key] == item.Value; - } - - /// - /// Copies to. - /// - /// The array. - /// Index of the array. - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - if (array == null) throw new ArgumentNullException("array"); - int num = Count; - foreach (KeyValuePair kvp in this) - { - array[arrayIndex++] = kvp; - if (--num <= 0) - return; - } - } - - /// - /// Gets the count. - /// - /// The count. - public int Count - { - get { return _members.Count; } - } - - /// - /// Gets a value indicating whether this instance is read only. - /// - /// - /// true if this instance is read only; otherwise, false. - /// - public bool IsReadOnly - { - get { return false; } - } - - /// - /// Removes the specified item. - /// - /// The item. - /// - public bool Remove(KeyValuePair item) - { - return _members.Remove(item.Key); - } - - /// - /// Gets the enumerator. - /// - /// - public IEnumerator> GetEnumerator() - { - return _members.GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return _members.GetEnumerator(); - } - - /// - /// Returns a json that represents the current . - /// - /// - /// A json that represents the current . - /// - public override string ToString() - { - return SimpleJson.SerializeObject(this); - } - -#if SIMPLE_JSON_DYNAMIC - /// - /// Provides implementation for type conversion operations. Classes derived from the class can override this method to specify dynamic behavior for operations that convert an object from one type to another. - /// - /// Provides information about the conversion operation. The binder.Type property provides the type to which the object must be converted. For example, for the statement (String)sampleObject in C# (CType(sampleObject, Type) in Visual Basic), where sampleObject is an instance of the class derived from the class, binder.Type returns the type. The binder.Explicit property provides information about the kind of conversion that occurs. It returns true for explicit conversion and false for implicit conversion. - /// The result of the type conversion operation. - /// - /// Alwasy returns true. - /// - public override bool TryConvert(ConvertBinder binder, out object result) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - Type targetType = binder.Type; - - if ((targetType == typeof(IEnumerable)) || - (targetType == typeof(IEnumerable>)) || - (targetType == typeof(IDictionary)) || - (targetType == typeof(IDictionary))) - { - result = this; - return true; - } - - return base.TryConvert(binder, out result); - } - - /// - /// Provides the implementation for operations that delete an object member. This method is not intended for use in C# or Visual Basic. - /// - /// Provides information about the deletion. - /// - /// Alwasy returns true. - /// - public override bool TryDeleteMember(DeleteMemberBinder binder) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - return _members.Remove(binder.Name); - } - - /// - /// Provides the implementation for operations that get a value by index. Classes derived from the class can override this method to specify dynamic behavior for indexing operations. - /// - /// Provides information about the operation. - /// The indexes that are used in the operation. For example, for the sampleObject[3] operation in C# (sampleObject(3) in Visual Basic), where sampleObject is derived from the DynamicObject class, is equal to 3. - /// The result of the index operation. - /// - /// Alwasy returns true. - /// - public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) - { - if (indexes == null) throw new ArgumentNullException("indexes"); - if (indexes.Length == 1) - { - result = ((IDictionary)this)[(string)indexes[0]]; - return true; - } - result = null; - return true; - } - - /// - /// Provides the implementation for operations that get member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as getting a value for a property. - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// The result of the get operation. For example, if the method is called for a property, you can assign the property value to . - /// - /// Alwasy returns true. - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - object value; - if (_members.TryGetValue(binder.Name, out value)) - { - result = value; - return true; - } - result = null; - return true; - } - - /// - /// Provides the implementation for operations that set a value by index. Classes derived from the class can override this method to specify dynamic behavior for operations that access objects by a specified index. - /// - /// Provides information about the operation. - /// The indexes that are used in the operation. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 3. - /// The value to set to the object that has the specified index. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 10. - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown. - /// - public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) - { - if (indexes == null) throw new ArgumentNullException("indexes"); - if (indexes.Length == 1) - { - ((IDictionary)this)[(string)indexes[0]] = value; - return true; - } - return base.TrySetIndex(binder, indexes, value); - } - - /// - /// Provides the implementation for operations that set member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as setting a value for a property. - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, the is "Test". - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) - /// - public override bool TrySetMember(SetMemberBinder binder, object value) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - _members[binder.Name] = value; - return true; - } - - /// - /// Returns the enumeration of all dynamic member names. - /// - /// - /// A sequence that contains dynamic member names. - /// - public override IEnumerable GetDynamicMemberNames() - { - foreach (var key in Keys) - yield return key; - } -#endif - } -} - -namespace SimpleJson -{ - /// - /// This class encodes and decodes JSON strings. - /// Spec. details, see http://www.json.org/ - /// - /// JSON uses Arrays and Objects. These correspond here to the datatypes JsonArray(IList<object>) and JsonObject(IDictionary<string,object>). - /// All numbers are parsed to doubles. - /// - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - static class SimpleJson - { - private const int TOKEN_NONE = 0; - private const int TOKEN_CURLY_OPEN = 1; - private const int TOKEN_CURLY_CLOSE = 2; - private const int TOKEN_SQUARED_OPEN = 3; - private const int TOKEN_SQUARED_CLOSE = 4; - private const int TOKEN_COLON = 5; - private const int TOKEN_COMMA = 6; - private const int TOKEN_STRING = 7; - private const int TOKEN_NUMBER = 8; - private const int TOKEN_TRUE = 9; - private const int TOKEN_FALSE = 10; - private const int TOKEN_NULL = 11; - private const int BUILDER_CAPACITY = 2000; - - private static readonly char[] EscapeTable; - private static readonly char[] EscapeCharacters = new char[] { '"', '\\', '\b', '\f', '\n', '\r', '\t' }; - private static readonly string EscapeCharactersString = new string(EscapeCharacters); - - static SimpleJson() - { - EscapeTable = new char[93]; - EscapeTable['"'] = '"'; - EscapeTable['\\'] = '\\'; - EscapeTable['\b'] = 'b'; - EscapeTable['\f'] = 'f'; - EscapeTable['\n'] = 'n'; - EscapeTable['\r'] = 'r'; - EscapeTable['\t'] = 't'; - } - - /// - /// Parses the string json into a value - /// - /// A JSON string. - /// An IList<object>, a IDictionary<string,object>, a double, a string, null, true, or false - public static object DeserializeObject(string json) - { - object obj; - if (TryDeserializeObject(json, out obj)) - return obj; - throw new SerializationException("Invalid JSON string"); - } - - /// - /// Try parsing the json string into a value. - /// - /// - /// A JSON string. - /// - /// - /// The object. - /// - /// - /// Returns true if successful otherwise false. - /// - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - public static bool TryDeserializeObject(string json, out object obj) - { - bool success = true; - if (json != null) - { - char[] charArray = json.ToCharArray(); - int index = 0; - obj = ParseValue(charArray, ref index, ref success); - } - else - obj = null; - - return success; - } - - public static object DeserializeObject(string json, Type type, IJsonSerializerStrategy jsonSerializerStrategy) - { - object jsonObject = DeserializeObject(json); - return type == null || jsonObject != null && ReflectionUtils.IsAssignableFrom(jsonObject.GetType(), type) - ? jsonObject - : (jsonSerializerStrategy ?? CurrentJsonSerializerStrategy).DeserializeObject(jsonObject, type); - } - - public static object DeserializeObject(string json, Type type) - { - return DeserializeObject(json, type, null); - } - - public static T DeserializeObject(string json, IJsonSerializerStrategy jsonSerializerStrategy) - { - return (T)DeserializeObject(json, typeof(T), jsonSerializerStrategy); - } - - public static T DeserializeObject(string json) - { - return (T)DeserializeObject(json, typeof(T), null); - } - - /// - /// Converts a IDictionary<string,object> / IList<object> object into a JSON string - /// - /// A IDictionary<string,object> / IList<object> - /// Serializer strategy to use - /// A JSON encoded string, or null if object 'json' is not serializable - public static string SerializeObject(object json, IJsonSerializerStrategy jsonSerializerStrategy) - { - StringBuilder builder = new StringBuilder(BUILDER_CAPACITY); - bool success = SerializeValue(jsonSerializerStrategy, json, builder); - return (success ? builder.ToString() : null); - } - - public static string SerializeObject(object json) - { - return SerializeObject(json, CurrentJsonSerializerStrategy); - } - - public static string EscapeToJavascriptString(string jsonString) - { - if (string.IsNullOrEmpty(jsonString)) - return jsonString; - - StringBuilder sb = new StringBuilder(); - char c; - - for (int i = 0; i < jsonString.Length; ) - { - c = jsonString[i++]; - - if (c == '\\') - { - int remainingLength = jsonString.Length - i; - if (remainingLength >= 2) - { - char lookahead = jsonString[i]; - if (lookahead == '\\') - { - sb.Append('\\'); - ++i; - } - else if (lookahead == '"') - { - sb.Append("\""); - ++i; - } - else if (lookahead == 't') - { - sb.Append('\t'); - ++i; - } - else if (lookahead == 'b') - { - sb.Append('\b'); - ++i; - } - else if (lookahead == 'n') - { - sb.Append('\n'); - ++i; - } - else if (lookahead == 'r') - { - sb.Append('\r'); - ++i; - } - } - } - else - { - sb.Append(c); - } - } - return sb.ToString(); - } - - static IDictionary ParseObject(char[] json, ref int index, ref bool success) - { - IDictionary table = new JsonObject(); - int token; - - // { - NextToken(json, ref index); - - bool done = false; - while (!done) - { - token = LookAhead(json, index); - if (token == TOKEN_NONE) - { - success = false; - return null; - } - else if (token == TOKEN_COMMA) - NextToken(json, ref index); - else if (token == TOKEN_CURLY_CLOSE) - { - NextToken(json, ref index); - return table; - } - else - { - // name - string name = ParseString(json, ref index, ref success); - if (!success) - { - success = false; - return null; - } - // : - token = NextToken(json, ref index); - if (token != TOKEN_COLON) - { - success = false; - return null; - } - // value - object value = ParseValue(json, ref index, ref success); - if (!success) - { - success = false; - return null; - } - table[name] = value; - } - } - return table; - } - - static JsonArray ParseArray(char[] json, ref int index, ref bool success) - { - JsonArray array = new JsonArray(); - - // [ - NextToken(json, ref index); - - bool done = false; - while (!done) - { - int token = LookAhead(json, index); - if (token == TOKEN_NONE) - { - success = false; - return null; - } - else if (token == TOKEN_COMMA) - NextToken(json, ref index); - else if (token == TOKEN_SQUARED_CLOSE) - { - NextToken(json, ref index); - break; - } - else - { - object value = ParseValue(json, ref index, ref success); - if (!success) - return null; - array.Add(value); - } - } - return array; - } - - static object ParseValue(char[] json, ref int index, ref bool success) - { - switch (LookAhead(json, index)) - { - case TOKEN_STRING: - return ParseString(json, ref index, ref success); - case TOKEN_NUMBER: - return ParseNumber(json, ref index, ref success); - case TOKEN_CURLY_OPEN: - return ParseObject(json, ref index, ref success); - case TOKEN_SQUARED_OPEN: - return ParseArray(json, ref index, ref success); - case TOKEN_TRUE: - NextToken(json, ref index); - return true; - case TOKEN_FALSE: - NextToken(json, ref index); - return false; - case TOKEN_NULL: - NextToken(json, ref index); - return null; - case TOKEN_NONE: - break; - } - success = false; - return null; - } - - static string ParseString(char[] json, ref int index, ref bool success) - { - StringBuilder s = new StringBuilder(BUILDER_CAPACITY); - char c; - - EatWhitespace(json, ref index); - - // " - c = json[index++]; - bool complete = false; - while (!complete) - { - if (index == json.Length) - break; - - c = json[index++]; - if (c == '"') - { - complete = true; - break; - } - else if (c == '\\') - { - if (index == json.Length) - break; - c = json[index++]; - if (c == '"') - s.Append('"'); - else if (c == '\\') - s.Append('\\'); - else if (c == '/') - s.Append('/'); - else if (c == 'b') - s.Append('\b'); - else if (c == 'f') - s.Append('\f'); - else if (c == 'n') - s.Append('\n'); - else if (c == 'r') - s.Append('\r'); - else if (c == 't') - s.Append('\t'); - else if (c == 'u') - { - int remainingLength = json.Length - index; - if (remainingLength >= 4) - { - // parse the 32 bit hex into an integer codepoint - uint codePoint; - if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint))) - return ""; - - // convert the integer codepoint to a unicode char and add to string - if (0xD800 <= codePoint && codePoint <= 0xDBFF) // if high surrogate - { - index += 4; // skip 4 chars - remainingLength = json.Length - index; - if (remainingLength >= 6) - { - uint lowCodePoint; - if (new string(json, index, 2) == "\\u" && UInt32.TryParse(new string(json, index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out lowCodePoint)) - { - if (0xDC00 <= lowCodePoint && lowCodePoint <= 0xDFFF) // if low surrogate - { - s.Append((char)codePoint); - s.Append((char)lowCodePoint); - index += 6; // skip 6 chars - continue; - } - } - } - success = false; // invalid surrogate pair - return ""; - } - s.Append(ConvertFromUtf32((int)codePoint)); - // skip 4 chars - index += 4; - } - else - break; - } - } - else - s.Append(c); - } - if (!complete) - { - success = false; - return null; - } - return s.ToString(); - } - - private static string ConvertFromUtf32(int utf32) - { - // http://www.java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/System/System/Char.cs.htm - if (utf32 < 0 || utf32 > 0x10FFFF) - throw new ArgumentOutOfRangeException("utf32", "The argument must be from 0 to 0x10FFFF."); - if (0xD800 <= utf32 && utf32 <= 0xDFFF) - throw new ArgumentOutOfRangeException("utf32", "The argument must not be in surrogate pair range."); - if (utf32 < 0x10000) - return new string((char)utf32, 1); - utf32 -= 0x10000; - return new string(new char[] { (char)((utf32 >> 10) + 0xD800), (char)(utf32 % 0x0400 + 0xDC00) }); - } - - static object ParseNumber(char[] json, ref int index, ref bool success) - { - EatWhitespace(json, ref index); - int lastIndex = GetLastIndexOfNumber(json, index); - int charLength = (lastIndex - index) + 1; - object returnNumber; - string str = new string(json, index, charLength); - if (str.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || str.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) - { - double number; - success = double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); - returnNumber = number; - } - else - { - long number; - success = long.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); - returnNumber = number; - } - index = lastIndex + 1; - return returnNumber; - } - - static int GetLastIndexOfNumber(char[] json, int index) - { - int lastIndex; - for (lastIndex = index; lastIndex < json.Length; lastIndex++) - if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) break; - return lastIndex - 1; - } - - static void EatWhitespace(char[] json, ref int index) - { - for (; index < json.Length; index++) { - switch (json[index]) { - case ' ': - case '\t': - case '\n': - case '\r': - case '\b': - case '\f': - break; - default: - return; - } - } - } - - static int LookAhead(char[] json, int index) - { - int saveIndex = index; - return NextToken(json, ref saveIndex); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - static int NextToken(char[] json, ref int index) - { - EatWhitespace(json, ref index); - if (index == json.Length) - return TOKEN_NONE; - char c = json[index]; - index++; - switch (c) - { - case '{': - return TOKEN_CURLY_OPEN; - case '}': - return TOKEN_CURLY_CLOSE; - case '[': - return TOKEN_SQUARED_OPEN; - case ']': - return TOKEN_SQUARED_CLOSE; - case ',': - return TOKEN_COMMA; - case '"': - return TOKEN_STRING; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': - return TOKEN_NUMBER; - case ':': - return TOKEN_COLON; - } - index--; - int remainingLength = json.Length - index; - // false - if (remainingLength >= 5) - { - if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') - { - index += 5; - return TOKEN_FALSE; - } - } - // true - if (remainingLength >= 4) - { - if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') - { - index += 4; - return TOKEN_TRUE; - } - } - // null - if (remainingLength >= 4) - { - if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') - { - index += 4; - return TOKEN_NULL; - } - } - return TOKEN_NONE; - } - - static bool SerializeValue(IJsonSerializerStrategy jsonSerializerStrategy, object value, StringBuilder builder) - { - bool success = true; - string stringValue = value as string; - if (stringValue != null) - success = SerializeString(stringValue, builder); - else - { - IDictionary dict = value as IDictionary; - if (dict != null) - { - success = SerializeObject(jsonSerializerStrategy, dict.Keys, dict.Values, builder); - } - else - { - IDictionary stringDictionary = value as IDictionary; - if (stringDictionary != null) - { - success = SerializeObject(jsonSerializerStrategy, stringDictionary.Keys, stringDictionary.Values, builder); - } - else - { - IEnumerable enumerableValue = value as IEnumerable; - if (enumerableValue != null) - success = SerializeArray(jsonSerializerStrategy, enumerableValue, builder); - else if (IsNumeric(value)) - success = SerializeNumber(value, builder); - else if (value is bool) - builder.Append((bool)value ? "true" : "false"); - else if (value == null) - builder.Append("null"); - else - { - object serializedObject; - success = jsonSerializerStrategy.TrySerializeNonPrimitiveObject(value, out serializedObject); - if (success) - SerializeValue(jsonSerializerStrategy, serializedObject, builder); - } - } - } - } - return success; - } - - static bool SerializeObject(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable keys, IEnumerable values, StringBuilder builder) - { - builder.Append("{"); - IEnumerator ke = keys.GetEnumerator(); - IEnumerator ve = values.GetEnumerator(); - bool first = true; - while (ke.MoveNext() && ve.MoveNext()) - { - object key = ke.Current; - object value = ve.Current; - if (!first) - builder.Append(","); - string stringKey = key as string; - if (stringKey != null) - SerializeString(stringKey, builder); - else - if (!SerializeValue(jsonSerializerStrategy, value, builder)) return false; - builder.Append(":"); - if (!SerializeValue(jsonSerializerStrategy, value, builder)) - return false; - first = false; - } - builder.Append("}"); - return true; - } - - static bool SerializeArray(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable anArray, StringBuilder builder) - { - builder.Append("["); - bool first = true; - foreach (object value in anArray) - { - if (!first) - builder.Append(","); - if (!SerializeValue(jsonSerializerStrategy, value, builder)) - return false; - first = false; - } - builder.Append("]"); - return true; - } - - static bool SerializeString(string aString, StringBuilder builder) - { - // Happy path if there's nothing to be escaped. IndexOfAny is highly optimized (and unmanaged) - if (aString.IndexOfAny(EscapeCharacters) == -1) - { - builder.Append('"'); - builder.Append(aString); - builder.Append('"'); - - return true; - } - - builder.Append('"'); - int safeCharacterCount = 0; - char[] charArray = aString.ToCharArray(); - - for (int i = 0; i < charArray.Length; i++) - { - char c = charArray[i]; - - // Non ascii characters are fine, buffer them up and send them to the builder - // in larger chunks if possible. The escape table is a 1:1 translation table - // with \0 [default(char)] denoting a safe character. - if (c >= EscapeTable.Length || EscapeTable[c] == default(char)) - { - safeCharacterCount++; - } - else - { - if (safeCharacterCount > 0) - { - builder.Append(charArray, i - safeCharacterCount, safeCharacterCount); - safeCharacterCount = 0; - } - - builder.Append('\\'); - builder.Append(EscapeTable[c]); - } - } - - if (safeCharacterCount > 0) - { - builder.Append(charArray, charArray.Length - safeCharacterCount, safeCharacterCount); - } - - builder.Append('"'); - return true; - } - - static bool SerializeNumber(object number, StringBuilder builder) - { - if (number is long) - builder.Append(((long)number).ToString(CultureInfo.InvariantCulture)); - else if (number is ulong) - builder.Append(((ulong)number).ToString(CultureInfo.InvariantCulture)); - else if (number is int) - builder.Append(((int)number).ToString(CultureInfo.InvariantCulture)); - else if (number is uint) - builder.Append(((uint)number).ToString(CultureInfo.InvariantCulture)); - else if (number is decimal) - builder.Append(((decimal)number).ToString(CultureInfo.InvariantCulture)); - else if (number is float) - builder.Append(((float)number).ToString(CultureInfo.InvariantCulture)); - else - builder.Append(Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture)); - return true; - } - - /// - /// Determines if a given object is numeric in any way - /// (can be integer, double, null, etc). - /// - static bool IsNumeric(object value) - { - if (value is sbyte) return true; - if (value is byte) return true; - if (value is short) return true; - if (value is ushort) return true; - if (value is int) return true; - if (value is uint) return true; - if (value is long) return true; - if (value is ulong) return true; - if (value is float) return true; - if (value is double) return true; - if (value is decimal) return true; - return false; - } - - private static IJsonSerializerStrategy _currentJsonSerializerStrategy; - public static IJsonSerializerStrategy CurrentJsonSerializerStrategy - { - get - { - return _currentJsonSerializerStrategy ?? - (_currentJsonSerializerStrategy = -#if SIMPLE_JSON_DATACONTRACT - DataContractJsonSerializerStrategy -#else - PocoJsonSerializerStrategy -#endif -); - } - set - { - _currentJsonSerializerStrategy = value; - } - } - - private static PocoJsonSerializerStrategy _pocoJsonSerializerStrategy; - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static PocoJsonSerializerStrategy PocoJsonSerializerStrategy - { - get - { - return _pocoJsonSerializerStrategy ?? (_pocoJsonSerializerStrategy = new PocoJsonSerializerStrategy()); - } - } - -#if SIMPLE_JSON_DATACONTRACT - - private static DataContractJsonSerializerStrategy _dataContractJsonSerializerStrategy; - [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Advanced)] - public static DataContractJsonSerializerStrategy DataContractJsonSerializerStrategy - { - get - { - return _dataContractJsonSerializerStrategy ?? (_dataContractJsonSerializerStrategy = new DataContractJsonSerializerStrategy()); - } - } - -#endif - } - - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - interface IJsonSerializerStrategy - { - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - bool TrySerializeNonPrimitiveObject(object input, out object output); - object DeserializeObject(object value, Type type); - } - - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - class PocoJsonSerializerStrategy : IJsonSerializerStrategy - { - internal IDictionary ConstructorCache; - internal IDictionary> GetCache; - internal IDictionary>> SetCache; - - internal static readonly Type[] EmptyTypes = new Type[0]; - internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; - - private static readonly string[] Iso8601Format = new string[] - { - @"yyyy-MM-dd\THH:mm:ss.FFFFFFF\Z", - @"yyyy-MM-dd\THH:mm:ss\Z", - @"yyyy-MM-dd\THH:mm:ssK" - }; - - public PocoJsonSerializerStrategy() - { - ConstructorCache = new ReflectionUtils.ThreadSafeDictionary(ConstructorDelegateFactory); - GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); - SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); - } - - protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName) - { - return CamelCase.MemberNameToCamelCase(clrPropertyName); - } - - internal virtual ReflectionUtils.ConstructorDelegate ConstructorDelegateFactory(Type key) - { - // We need List(int) constructor so that DeserializeObject method will work for generating IList-declared values - var needsCapacityArgument = key.IsArray || key.IsConstructedGenericType && key.GetGenericTypeDefinition() == typeof(List<>); - return ReflectionUtils.GetConstructor(key, needsCapacityArgument ? ArrayConstructorParameterTypes : EmptyTypes); - } - - internal virtual IDictionary GetterValueFactory(Type type) - { - IDictionary result = new Dictionary(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanRead) - { - MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (getMethod.IsStatic || !getMethod.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = ReflectionUtils.GetGetMethod(propertyInfo); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (fieldInfo.IsStatic || !fieldInfo.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = ReflectionUtils.GetGetMethod(fieldInfo); - } - return result; - } - - internal virtual IDictionary> SetterValueFactory(Type type) - { - // BLAZOR-SPECIFIC MODIFICATION FROM STOCK SIMPLEJSON: - // - // For incoming keys we match case-insensitively. But if two .NET properties differ only by case, - // it's ambiguous which should be used: the one that matches the incoming JSON exactly, or the - // one that uses 'correct' PascalCase corresponding to the incoming camelCase? What if neither - // meets these descriptions? - // - // To resolve this: - // - If multiple public properties differ only by case, we throw - // - If multiple public fields differ only by case, we throw - // - If there's a public property and a public field that differ only by case, we prefer the property - // This unambiguously selects one member, and that's what we'll use. - - IDictionary> result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanWrite) - { - MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (setMethod.IsStatic) - continue; - if (result.ContainsKey(propertyInfo.Name)) - { - throw new InvalidOperationException($"The type '{type.FullName}' contains multiple public properties with names case-insensitively matching '{propertyInfo.Name.ToLowerInvariant()}'. Such types cannot be used for JSON deserialization."); - } - result[propertyInfo.Name] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); - } - } - - IDictionary> fieldResult = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic) - continue; - if (fieldResult.ContainsKey(fieldInfo.Name)) - { - throw new InvalidOperationException($"The type '{type.FullName}' contains multiple public fields with names case-insensitively matching '{fieldInfo.Name.ToLowerInvariant()}'. Such types cannot be used for JSON deserialization."); - } - fieldResult[fieldInfo.Name] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); - if (!result.ContainsKey(fieldInfo.Name)) - { - result[fieldInfo.Name] = fieldResult[fieldInfo.Name]; - } - } - - return result; - } - - public virtual bool TrySerializeNonPrimitiveObject(object input, out object output) - { - return TrySerializeKnownTypes(input, out output) || TrySerializeUnknownTypes(input, out output); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - public virtual object DeserializeObject(object value, Type type) - { - if (type == null) throw new ArgumentNullException("type"); - string str = value as string; - - if (type == typeof (Guid) && string.IsNullOrEmpty(str)) - return default(Guid); - - if (type.IsEnum) - { - type = type.GetEnumUnderlyingType(); - } - - if (value == null) - return null; - - object obj = null; - - if (str != null) - { - if (str.Length != 0) // We know it can't be null now. - { - if (type == typeof(TimeSpan) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(TimeSpan))) - return TimeSpan.ParseExact(str, "c", CultureInfo.InvariantCulture); - if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) - return DateTime.TryParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var result) - ? result : DateTime.Parse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); - if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) - return DateTimeOffset.TryParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var result) - ? result : DateTimeOffset.Parse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); - if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) - return new Guid(str); - if (type == typeof(Uri)) - { - bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); - - Uri result; - if (isValid && Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out result)) - return result; - - return null; - } - - if (type == typeof(string)) - return str; - - return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); - } - else - { - if (type == typeof(Guid)) - obj = default(Guid); - else if (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) - obj = null; - else - obj = str; - } - // Empty string case - if (!ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) - return str; - } - else if (value is bool) - return value; - - bool valueIsLong = value is long; - bool valueIsDouble = value is double; - if ((valueIsLong && type == typeof(long)) || (valueIsDouble && type == typeof(double))) - return value; - if ((valueIsDouble && type != typeof(double)) || (valueIsLong && type != typeof(long))) - { - obj = type == typeof(int) || type == typeof(long) || type == typeof(double) || type == typeof(float) || type == typeof(bool) || type == typeof(decimal) || type == typeof(byte) || type == typeof(short) - ? Convert.ChangeType(value, type, CultureInfo.InvariantCulture) - : value; - } - else - { - IDictionary objects = value as IDictionary; - if (objects != null) - { - IDictionary jsonObject = objects; - - if (ReflectionUtils.IsTypeDictionary(type)) - { - // if dictionary then - Type[] types = ReflectionUtils.GetGenericTypeArguments(type); - Type keyType = types[0]; - Type valueType = types[1]; - - Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); - - IDictionary dict = (IDictionary)ConstructorCache[genericType](); - - foreach (KeyValuePair kvp in jsonObject) - dict.Add(kvp.Key, DeserializeObject(kvp.Value, valueType)); - - obj = dict; - } - else - { - if (type == typeof(object)) - obj = value; - else - { - var constructorDelegate = ConstructorCache[type] - ?? throw new InvalidOperationException($"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor."); - obj = constructorDelegate(); - - var setterCache = SetCache[type]; - foreach (var jsonKeyValuePair in jsonObject) - { - if (setterCache.TryGetValue(jsonKeyValuePair.Key, out var setter)) - { - var jsonValue = DeserializeObject(jsonKeyValuePair.Value, setter.Key); - setter.Value(obj, jsonValue); - } - } - } - } - } - else - { - IList valueAsList = value as IList; - if (valueAsList != null) - { - IList jsonObject = valueAsList; - IList list = null; - - if (type.IsArray) - { - list = (IList)ConstructorCache[type](jsonObject.Count); - int i = 0; - foreach (object o in jsonObject) - list[i++] = DeserializeObject(o, type.GetElementType()); - } - else if (ReflectionUtils.IsTypeGenericCollectionInterface(type) || ReflectionUtils.IsAssignableFrom(typeof(IList), type)) - { - Type innerType = ReflectionUtils.GetGenericListElementType(type); - list = (IList)(ConstructorCache[type] ?? ConstructorCache[typeof(List<>).MakeGenericType(innerType)])(jsonObject.Count); - foreach (object o in jsonObject) - list.Add(DeserializeObject(o, innerType)); - } - obj = list; - } - } - return obj; - } - if (ReflectionUtils.IsNullableType(type)) - { - // For nullable enums serialized as numbers - if (Nullable.GetUnderlyingType(type).IsEnum) - { - return Enum.ToObject(Nullable.GetUnderlyingType(type), value); - } - - return ReflectionUtils.ToNullableType(obj, type); - } - - return obj; - } - - protected virtual object SerializeEnum(Enum p) - { - return Convert.ToDouble(p, CultureInfo.InvariantCulture); - } - - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - protected virtual bool TrySerializeKnownTypes(object input, out object output) - { - bool returnValue = true; - if (input is DateTime) - output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); - else if (input is DateTimeOffset) - output = ((DateTimeOffset)input).ToString("o"); - else if (input is Guid) - output = ((Guid)input).ToString("D"); - else if (input is Uri) - output = input.ToString(); - else if (input is TimeSpan) - output = ((TimeSpan)input).ToString("c"); - else - { - Enum inputEnum = input as Enum; - if (inputEnum != null) - output = SerializeEnum(inputEnum); - else - { - returnValue = false; - output = null; - } - } - return returnValue; - } - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - protected virtual bool TrySerializeUnknownTypes(object input, out object output) - { - if (input == null) throw new ArgumentNullException("input"); - output = null; - Type type = input.GetType(); - if (type.FullName == null) - return false; - IDictionary obj = new JsonObject(); - IDictionary getters = GetCache[type]; - foreach (KeyValuePair getter in getters) - { - if (getter.Value != null) - obj.Add(MapClrMemberNameToJsonFieldName(getter.Key), getter.Value(input)); - } - output = obj; - return true; - } - } - -#if SIMPLE_JSON_DATACONTRACT - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - class DataContractJsonSerializerStrategy : PocoJsonSerializerStrategy - { - public DataContractJsonSerializerStrategy() - { - GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); - SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); - } - - internal override IDictionary GetterValueFactory(Type type) - { - bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; - if (!hasDataContract) - return base.GetterValueFactory(type); - string jsonKey; - IDictionary result = new Dictionary(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanRead) - { - MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (!getMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) - result[jsonKey] = ReflectionUtils.GetGetMethod(propertyInfo); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (!fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) - result[jsonKey] = ReflectionUtils.GetGetMethod(fieldInfo); - } - return result; - } - - internal override IDictionary> SetterValueFactory(Type type) - { - bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; - if (!hasDataContract) - return base.SetterValueFactory(type); - string jsonKey; - IDictionary> result = new Dictionary>(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanWrite) - { - MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (!setMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) - result[jsonKey] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (!fieldInfo.IsInitOnly && !fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) - result[jsonKey] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); - } - // todo implement sorting for DATACONTRACT. - return result; - } - - private static bool CanAdd(MemberInfo info, out string jsonKey) - { - jsonKey = null; - if (ReflectionUtils.GetAttribute(info, typeof(IgnoreDataMemberAttribute)) != null) - return false; - DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)ReflectionUtils.GetAttribute(info, typeof(DataMemberAttribute)); - if (dataMemberAttribute == null) - return false; - jsonKey = string.IsNullOrEmpty(dataMemberAttribute.Name) ? info.Name : dataMemberAttribute.Name; - return true; - } - } - -#endif - - namespace Reflection - { - // This class is meant to be copied into other libraries. So we want to exclude it from Code Analysis rules - // that might be in place in the target project. - [GeneratedCode("reflection-utils", "1.0.0")] -#if SIMPLE_JSON_REFLECTION_UTILS_PUBLIC - public -#else - internal -#endif - class ReflectionUtils - { - private static readonly object[] EmptyObjects = new object[] { }; - - public delegate object GetDelegate(object source); - public delegate void SetDelegate(object source, object value); - public delegate object ConstructorDelegate(params object[] args); - - public delegate TValue ThreadSafeDictionaryValueFactory(TKey key); - -#if SIMPLE_JSON_TYPEINFO - public static TypeInfo GetTypeInfo(Type type) - { - return type.GetTypeInfo(); - } -#else - public static Type GetTypeInfo(Type type) - { - return type; - } -#endif - - public static Attribute GetAttribute(MemberInfo info, Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (info == null || type == null || !info.IsDefined(type)) - return null; - return info.GetCustomAttribute(type); -#else - if (info == null || type == null || !Attribute.IsDefined(info, type)) - return null; - return Attribute.GetCustomAttribute(info, type); -#endif - } - - public static Type GetGenericListElementType(Type type) - { - IEnumerable interfaces; -#if SIMPLE_JSON_TYPEINFO - interfaces = type.GetTypeInfo().ImplementedInterfaces; -#else - interfaces = type.GetInterfaces(); -#endif - foreach (Type implementedInterface in interfaces) - { - if (IsTypeGeneric(implementedInterface) && - implementedInterface.GetGenericTypeDefinition() == typeof (IList<>)) - { - return GetGenericTypeArguments(implementedInterface)[0]; - } - } - return GetGenericTypeArguments(type)[0]; - } - - public static Attribute GetAttribute(Type objectType, Type attributeType) - { - -#if SIMPLE_JSON_TYPEINFO - if (objectType == null || attributeType == null || !objectType.GetTypeInfo().IsDefined(attributeType)) - return null; - return objectType.GetTypeInfo().GetCustomAttribute(attributeType); -#else - if (objectType == null || attributeType == null || !Attribute.IsDefined(objectType, attributeType)) - return null; - return Attribute.GetCustomAttribute(objectType, attributeType); -#endif - } - - public static Type[] GetGenericTypeArguments(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().GenericTypeArguments; -#else - return type.GetGenericArguments(); -#endif - } - - public static bool IsTypeGeneric(Type type) - { - return GetTypeInfo(type).IsGenericType; - } - - public static bool IsTypeGenericCollectionInterface(Type type) - { - if (!IsTypeGeneric(type)) - return false; - - Type genericDefinition = type.GetGenericTypeDefinition(); - - return (genericDefinition == typeof(IList<>) - || genericDefinition == typeof(ICollection<>) - || genericDefinition == typeof(IEnumerable<>) -#if SIMPLE_JSON_READONLY_COLLECTIONS - || genericDefinition == typeof(IReadOnlyCollection<>) - || genericDefinition == typeof(IReadOnlyList<>) -#endif - ); - } - - public static bool IsAssignableFrom(Type type1, Type type2) - { - return GetTypeInfo(type1).IsAssignableFrom(GetTypeInfo(type2)); - } - - public static bool IsTypeDictionary(Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (typeof(IDictionary<,>).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) - return true; -#else - if (typeof(System.Collections.IDictionary).IsAssignableFrom(type)) - return true; -#endif - if (!GetTypeInfo(type).IsGenericType) - return false; - - Type genericDefinition = type.GetGenericTypeDefinition(); - return genericDefinition == typeof(IDictionary<,>); - } - - public static bool IsNullableType(Type type) - { - return GetTypeInfo(type).IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - public static object ToNullableType(object obj, Type nullableType) - { - return obj == null ? null : Convert.ChangeType(obj, Nullable.GetUnderlyingType(nullableType), CultureInfo.InvariantCulture); - } - - public static bool IsValueType(Type type) - { - return GetTypeInfo(type).IsValueType; - } - - public static IEnumerable GetConstructors(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredConstructors; -#else - const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - return type.GetConstructors(flags); -#endif - } - - public static ConstructorInfo GetConstructorInfo(Type type, params Type[] argsType) - { - IEnumerable constructorInfos = GetConstructors(type); - int i; - bool matches; - foreach (ConstructorInfo constructorInfo in constructorInfos) - { - ParameterInfo[] parameters = constructorInfo.GetParameters(); - if (argsType.Length != parameters.Length) - continue; - - i = 0; - matches = true; - foreach (ParameterInfo parameterInfo in constructorInfo.GetParameters()) - { - if (parameterInfo.ParameterType != argsType[i]) - { - matches = false; - break; - } - } - - if (matches) - return constructorInfo; - } - - return null; - } - - public static IEnumerable GetProperties(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetRuntimeProperties(); -#else - return type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static IEnumerable GetFields(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetRuntimeFields(); -#else - return type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static MethodInfo GetGetterMethodInfo(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_TYPEINFO - return propertyInfo.GetMethod; -#else - return propertyInfo.GetGetMethod(true); -#endif - } - - public static MethodInfo GetSetterMethodInfo(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_TYPEINFO - return propertyInfo.SetMethod; -#else - return propertyInfo.GetSetMethod(true); -#endif - } - - public static ConstructorDelegate GetConstructor(ConstructorInfo constructorInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetConstructorByReflection(constructorInfo); -#else - return GetConstructorByExpression(constructorInfo); -#endif - } - - public static ConstructorDelegate GetConstructor(Type type, params Type[] argsType) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetConstructorByReflection(type, argsType); -#else - return GetConstructorByExpression(type, argsType); -#endif - } - - public static ConstructorDelegate GetConstructorByReflection(ConstructorInfo constructorInfo) - { - return delegate(object[] args) { return constructorInfo.Invoke(args); }; - } - - public static ConstructorDelegate GetConstructorByReflection(Type type, params Type[] argsType) - { - ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - - if (constructorInfo == null && argsType.Length == 0 && type.IsValueType) - { - // If it's a struct, then parameterless constructors are implicit - // We can always call Activator.CreateInstance in lieu of a zero-arg constructor - return args => Activator.CreateInstance(type); - } - - return constructorInfo == null ? null : GetConstructorByReflection(constructorInfo); - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static ConstructorDelegate GetConstructorByExpression(ConstructorInfo constructorInfo) - { - ParameterInfo[] paramsInfo = constructorInfo.GetParameters(); - ParameterExpression param = Expression.Parameter(typeof(object[]), "args"); - Expression[] argsExp = new Expression[paramsInfo.Length]; - for (int i = 0; i < paramsInfo.Length; i++) - { - Expression index = Expression.Constant(i); - Type paramType = paramsInfo[i].ParameterType; - Expression paramAccessorExp = Expression.ArrayIndex(param, index); - Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); - argsExp[i] = paramCastExp; - } - NewExpression newExp = Expression.New(constructorInfo, argsExp); - Expression> lambda = Expression.Lambda>(newExp, param); - Func compiledLambda = lambda.Compile(); - return delegate(object[] args) { return compiledLambda(args); }; - } - - public static ConstructorDelegate GetConstructorByExpression(Type type, params Type[] argsType) - { - ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByExpression(constructorInfo); - } - -#endif - - public static GetDelegate GetGetMethod(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetGetMethodByReflection(propertyInfo); -#else - return GetGetMethodByExpression(propertyInfo); -#endif - } - - public static GetDelegate GetGetMethod(FieldInfo fieldInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetGetMethodByReflection(fieldInfo); -#else - return GetGetMethodByExpression(fieldInfo); -#endif - } - - public static GetDelegate GetGetMethodByReflection(PropertyInfo propertyInfo) - { - MethodInfo methodInfo = GetGetterMethodInfo(propertyInfo); - return delegate(object source) { return methodInfo.Invoke(source, EmptyObjects); }; - } - - public static GetDelegate GetGetMethodByReflection(FieldInfo fieldInfo) - { - return delegate(object source) { return fieldInfo.GetValue(source); }; - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static GetDelegate GetGetMethodByExpression(PropertyInfo propertyInfo) - { - MethodInfo getMethodInfo = GetGetterMethodInfo(propertyInfo); - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); - Func compiled = Expression.Lambda>(Expression.TypeAs(Expression.Call(instanceCast, getMethodInfo), typeof(object)), instance).Compile(); - return delegate(object source) { return compiled(source); }; - } - - public static GetDelegate GetGetMethodByExpression(FieldInfo fieldInfo) - { - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - MemberExpression member = Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo); - GetDelegate compiled = Expression.Lambda(Expression.Convert(member, typeof(object)), instance).Compile(); - return delegate(object source) { return compiled(source); }; - } - -#endif - - public static SetDelegate GetSetMethod(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetSetMethodByReflection(propertyInfo); -#else - return GetSetMethodByExpression(propertyInfo); -#endif - } - - public static SetDelegate GetSetMethod(FieldInfo fieldInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetSetMethodByReflection(fieldInfo); -#else - return GetSetMethodByExpression(fieldInfo); -#endif - } - - public static SetDelegate GetSetMethodByReflection(PropertyInfo propertyInfo) - { - MethodInfo methodInfo = GetSetterMethodInfo(propertyInfo); - return delegate(object source, object value) { methodInfo.Invoke(source, new object[] { value }); }; - } - - public static SetDelegate GetSetMethodByReflection(FieldInfo fieldInfo) - { - return delegate(object source, object value) { fieldInfo.SetValue(source, value); }; - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static SetDelegate GetSetMethodByExpression(PropertyInfo propertyInfo) - { - MethodInfo setMethodInfo = GetSetterMethodInfo(propertyInfo); - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - ParameterExpression value = Expression.Parameter(typeof(object), "value"); - UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); - UnaryExpression valueCast = (!IsValueType(propertyInfo.PropertyType)) ? Expression.TypeAs(value, propertyInfo.PropertyType) : Expression.Convert(value, propertyInfo.PropertyType); - Action compiled = Expression.Lambda>(Expression.Call(instanceCast, setMethodInfo, valueCast), new ParameterExpression[] { instance, value }).Compile(); - return delegate(object source, object val) { compiled(source, val); }; - } - - public static SetDelegate GetSetMethodByExpression(FieldInfo fieldInfo) - { - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - ParameterExpression value = Expression.Parameter(typeof(object), "value"); - Action compiled = Expression.Lambda>( - Assign(Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo), Expression.Convert(value, fieldInfo.FieldType)), instance, value).Compile(); - return delegate(object source, object val) { compiled(source, val); }; - } - - public static BinaryExpression Assign(Expression left, Expression right) - { -#if SIMPLE_JSON_TYPEINFO - return Expression.Assign(left, right); -#else - MethodInfo assign = typeof(Assigner<>).MakeGenericType(left.Type).GetMethod("Assign"); - BinaryExpression assignExpr = Expression.Add(left, right, assign); - return assignExpr; -#endif - } - - private static class Assigner - { - public static T Assign(ref T left, T right) - { - return (left = right); - } - } - -#endif - - public sealed class ThreadSafeDictionary : IDictionary - { - private readonly object _lock = new object(); - private readonly ThreadSafeDictionaryValueFactory _valueFactory; - private Dictionary _dictionary; - - public ThreadSafeDictionary(ThreadSafeDictionaryValueFactory valueFactory) - { - _valueFactory = valueFactory; - } - - private TValue Get(TKey key) - { - if (_dictionary == null) - return AddValue(key); - TValue value; - if (!_dictionary.TryGetValue(key, out value)) - return AddValue(key); - return value; - } - - private TValue AddValue(TKey key) - { - TValue value = _valueFactory(key); - lock (_lock) - { - if (_dictionary == null) - { - _dictionary = new Dictionary(); - _dictionary[key] = value; - } - else - { - TValue val; - if (_dictionary.TryGetValue(key, out val)) - return val; - Dictionary dict = new Dictionary(_dictionary); - dict[key] = value; - _dictionary = dict; - } - } - return value; - } - - public void Add(TKey key, TValue value) - { - throw new NotImplementedException(); - } - - public bool ContainsKey(TKey key) - { - return _dictionary.ContainsKey(key); - } - - public ICollection Keys - { - get { return _dictionary.Keys; } - } - - public bool Remove(TKey key) - { - throw new NotImplementedException(); - } - - public bool TryGetValue(TKey key, out TValue value) - { - value = this[key]; - return true; - } - - public ICollection Values - { - get { return _dictionary.Values; } - } - - public TValue this[TKey key] - { - get { return Get(key); } - set { throw new NotImplementedException(); } - } - - public void Add(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public void Clear() - { - throw new NotImplementedException(); - } - - public bool Contains(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - public int Count - { - get { return _dictionary.Count; } - } - - public bool IsReadOnly - { - get { throw new NotImplementedException(); } - } - - public bool Remove(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public IEnumerator> GetEnumerator() - { - return _dictionary.GetEnumerator(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return _dictionary.GetEnumerator(); - } - } - - } - } -} -// ReSharper restore LoopCanBeConvertedToQuery -// ReSharper restore RedundantExplicitArrayCreation -// ReSharper restore SuggestUseVarKeywordEvident diff --git a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs b/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs new file mode 100644 index 0000000000..0292039eaf --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs @@ -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, + }; + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj index f92b8d457d..bc912b97cc 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -8,6 +8,10 @@ true + + + + diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs index e3f91a6fd0..c65b4e4680 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs @@ -3,6 +3,8 @@ using System; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Xunit; @@ -10,10 +12,7 @@ namespace Microsoft.JSInterop.Tests { public class DotNetDispatcherTest { - private readonly static string thisAssemblyName - = typeof(DotNetDispatcherTest).Assembly.GetName().Name; - private readonly TestJSRuntime jsRuntime - = new TestJSRuntime(); + private readonly static string thisAssemblyName = typeof(DotNetDispatcherTest).Assembly.GetName().Name; [Fact] public void CannotInvokeWithEmptyAssemblyName() @@ -24,7 +23,7 @@ namespace Microsoft.JSInterop.Tests }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); - Assert.Equal("assemblyName", ex.ParamName); + Assert.Equal("AssemblyName", ex.ParamName); } [Fact] @@ -73,7 +72,7 @@ namespace Microsoft.JSInterop.Tests Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); } - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + [Fact] public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime => { // Arrange/Act @@ -90,7 +89,7 @@ namespace Microsoft.JSInterop.Tests { // Arrange/Act var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); - var result = Json.Deserialize(resultJson); + var result = JsonSerializer.Parse(resultJson, JsonSerializerOptionsProvider.Options); // Assert Assert.Equal("Test", result.StringVal); @@ -102,50 +101,81 @@ namespace Microsoft.JSInterop.Tests { // Arrange/Act var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); - var result = Json.Deserialize(resultJson); + var result = JsonSerializer.Parse(resultJson, JsonSerializerOptionsProvider.Options); // Assert Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); Assert.Equal(456, result.IntVal); }); - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + [Fact] public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => { // Arrange: Track a .NET object to use as an arg var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; - jsRuntime.Invoke("unimportant", new DotNetObjectRef(arg3)); + var objectRef = DotNetObjectRef.Create(arg3); + jsRuntime.Invoke("unimportant", objectRef); // Arrange: Remaining args - var argsJson = Json.Serialize(new object[] { + var argsJson = JsonSerializer.ToString(new object[] + { new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, - "__dotNetObject:1" - }); + objectRef + }, JsonSerializerOptionsProvider.Options); // Act var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); - var result = Json.Deserialize(resultJson); + var result = JsonDocument.Parse(resultJson); + var root = result.RootElement; // Assert: First result value marshalled via JSON - var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO)); + var resultDto1 = JsonSerializer.Parse(root[0].GetRawText(), JsonSerializerOptionsProvider.Options); + Assert.Equal("ANOTHER STRING", resultDto1.StringVal); Assert.Equal(756, resultDto1.IntVal); // Assert: Second result value marshalled by ref - var resultDto2Ref = (string)result[1]; - Assert.Equal("__dotNetObject:2", resultDto2Ref); - var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2); + var resultDto2Ref = root[1]; + Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _)); + Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _)); + + Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey, out var property)); + var resultDto2 = Assert.IsType(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())); Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(1299, resultDto2.IntVal); }); - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + [Fact] + public Task InvokingWithIncorrectUseOfDotNetObjectRefThrows() => WithJSRuntime(jsRuntime => + { + // Arrange + var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage); + var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; + var objectRef = DotNetObjectRef.Create(arg3); + jsRuntime.Invoke("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(() => + 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' to receive the incoming value.", ex.Message); + }); + + [Fact] public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => { // Arrange: Track some instance var targetInstance = new SomePublicType(); - jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); + var objectRef = DotNetObjectRef.Create(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); // Act var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); @@ -155,12 +185,13 @@ namespace Microsoft.JSInterop.Tests Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); }); - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + [Fact] public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime => { // Arrange: Track some instance var targetInstance = new DerivedClass(); - jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); + var objectRef = DotNetObjectRef.Create(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); // Act var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); @@ -178,7 +209,7 @@ namespace Microsoft.JSInterop.Tests // Arrange: Track some instance, then dispose it var targetInstance = new SomePublicType(); - var objectRef = new DotNetObjectRef(targetInstance); + var objectRef = DotNetObjectRef.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); objectRef.Dispose(); @@ -196,7 +227,7 @@ namespace Microsoft.JSInterop.Tests // Arrange: Track some instance, then dispose it var targetInstance = new SomePublicType(); - var objectRef = new DotNetObjectRef(targetInstance); + var objectRef = DotNetObjectRef.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); DotNetDispatcher.ReleaseDotNetObject(1); @@ -206,23 +237,23 @@ namespace Microsoft.JSInterop.Tests Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); }); - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + [Fact] public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime => { // Arrange: Track some instance plus another object we'll pass as a param var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; jsRuntime.Invoke("unimportant", - new DotNetObjectRef(targetInstance), - new DotNetObjectRef(arg2)); - var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]"; + DotNetObjectRef.Create(targetInstance), + DotNetObjectRef.Create(arg2)); + var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); // Assert - Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson); - var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); + Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); + var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3); Assert.Equal(1235, resultDto.IntVal); Assert.Equal("MY STRING", resultDto.StringVal); }); @@ -231,7 +262,7 @@ namespace Microsoft.JSInterop.Tests public void CannotInvokeWithIncorrectNumberOfParams() { // Arrange - var argsJson = Json.Serialize(new object[] { 1, 2, 3, 4 }); + var argsJson = JsonSerializer.ToString(new object[] { 1, 2, 3, 4 }, JsonSerializerOptionsProvider.Options); // Act/Assert var ex = Assert.Throws(() => @@ -242,50 +273,50 @@ namespace Microsoft.JSInterop.Tests Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message); } - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + [Fact] public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime => { // Arrange: Track some instance plus another object we'll pass as a param var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; - jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance), new DotNetObjectRef(arg2)); + var arg1Ref = DotNetObjectRef.Create(targetInstance); + var arg2Ref = DotNetObjectRef.Create(arg2); + jsRuntime.Invoke("unimportant", arg1Ref, arg2Ref); // Arrange: all args - var argsJson = Json.Serialize(new object[] + var argsJson = JsonSerializer.ToString(new object[] { new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, - "__dotNetObject:2" - }); + arg2Ref, + }, JsonSerializerOptionsProvider.Options); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); await resultTask; - var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); - var resultValue = (SimpleJson.JsonArray)result[2]; + var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement; + var resultValue = result[2]; // Assert: Correct info to complete the async call Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier); - Assert.Equal(3, result.Count); - Assert.Equal(callId, result[0]); - Assert.True((bool)result[1]); // Success flag + Assert.Equal(3, result.GetArrayLength()); + Assert.Equal(callId, result[0].GetString()); + Assert.True(result[1].GetBoolean()); // Success flag // Assert: First result value marshalled via JSON - var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO)); + var resultDto1 = JsonSerializer.Parse(resultValue[0].GetRawText(), JsonSerializerOptionsProvider.Options); Assert.Equal("STRING VIA JSON", resultDto1.StringVal); Assert.Equal(2000, resultDto1.IntVal); // Assert: Second result value marshalled by ref - var resultDto2Ref = (string)resultValue[1]; - Assert.Equal("__dotNetObject:3", resultDto2Ref); - var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); + var resultDto2Ref = JsonSerializer.Parse>(resultValue[1].GetRawText(), JsonSerializerOptionsProvider.Options); + var resultDto2 = resultDto2Ref.Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(2468, resultDto2.IntVal); }); - [Fact] public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime => { @@ -299,13 +330,13 @@ namespace Microsoft.JSInterop.Tests await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert - var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); - Assert.Equal(callId, result[0]); - Assert.False((bool)result[1]); // Fails + var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement; + Assert.Equal(callId, result[0].GetString()); + Assert.False(result[1].GetBoolean()); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = (string)result[2]; + var exception = result[2].GetString(); Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); }); @@ -322,17 +353,16 @@ namespace Microsoft.JSInterop.Tests await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert - var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); - Assert.Equal(callId, result[0]); - Assert.False((bool)result[1]); // Fails + var result = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson).RootElement; + Assert.Equal(callId, result[0].GetString()); + Assert.False(result[1].GetBoolean()); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = (string)result[2]; + var exception = result[2].GetString(); Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); }); - Task WithJSRuntime(Action testCode) { return WithJSRuntime(jsRuntime => @@ -379,7 +409,7 @@ namespace Microsoft.JSInterop.Tests => new TestDTO { StringVal = "Test", IntVal = 123 }; [JSInvokable("InvocableStaticWithParams")] - public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef) + public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, DotNetObjectRef dtoByRef) => new object[] { new TestDTO // Return via JSON marshalling @@ -387,13 +417,17 @@ namespace Microsoft.JSInterop.Tests StringVal = dtoViaJson.StringVal.ToUpperInvariant(), IntVal = dtoViaJson.IntVal + incrementAmounts.Sum() }, - new DotNetObjectRef(new TestDTO // Return by ref + DotNetObjectRef.Create(new TestDTO // Return by ref { - StringVal = dtoByRef.StringVal.ToUpperInvariant(), - IntVal = dtoByRef.IntVal + incrementAmounts.Sum() + StringVal = dtoByRef.Value.StringVal.ToUpperInvariant(), + IntVal = dtoByRef.Value.IntVal + incrementAmounts.Sum() }) }; + [JSInvokable(nameof(IncorrectDotNetObjectRefUsage))] + public static object[] IncorrectDotNetObjectRefUsage(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef) + => throw new InvalidOperationException("Shouldn't be called"); + [JSInvokable] public static TestDTO InvokableMethodWithoutCustomIdentifier() => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 }; @@ -405,14 +439,15 @@ namespace Microsoft.JSInterop.Tests } [JSInvokable] - public object[] InvokableInstanceMethod(string someString, TestDTO someDTO) + public object[] InvokableInstanceMethod(string someString, DotNetObjectRef someDTORef) { + var someDTO = someDTORef.Value; // Returning an array to make the point that object references // can be embedded anywhere in the result return new object[] { $"You passed {someString}", - new DotNetObjectRef(new TestDTO + DotNetObjectRef.Create(new TestDTO { IntVal = someDTO.IntVal + 1, StringVal = someDTO.StringVal.ToUpperInvariant() @@ -421,9 +456,10 @@ namespace Microsoft.JSInterop.Tests } [JSInvokable] - public async Task InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef) + public async Task InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectRef dtoByRefWrapper) { await Task.Delay(50); + var dtoByRef = dtoByRefWrapper.Value; return new object[] { new TestDTO // Return via JSON @@ -431,7 +467,7 @@ namespace Microsoft.JSInterop.Tests StringVal = dtoViaJson.StringVal.ToUpperInvariant(), IntVal = dtoViaJson.IntVal * 2, }, - new DotNetObjectRef(new TestDTO // Return by ref + DotNetObjectRef.Create(new TestDTO // Return by ref { StringVal = dtoByRef.StringVal.ToUpperInvariant(), IntVal = dtoByRef.IntVal * 2, diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs index 1bdec6d465..2b46831a16 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -11,58 +10,44 @@ namespace Microsoft.JSInterop.Tests public class DotNetObjectRefTest { [Fact] - public void CanAccessValue() + public Task CanAccessValue() => WithJSRuntime(_ => { var obj = new object(); - Assert.Same(obj, new DotNetObjectRef(obj).Value); - } + Assert.Same(obj, DotNetObjectRef.Create(obj).Value); + }); [Fact] - public void CanAssociateWithSameRuntimeMultipleTimes() - { - var objRef = new DotNetObjectRef(new object()); - var jsRuntime = new TestJsRuntime(); - objRef.EnsureAttachedToJsRuntime(jsRuntime); - objRef.EnsureAttachedToJsRuntime(jsRuntime); - } - - [Fact] - public void CannotAssociateWithDifferentRuntimes() - { - var objRef = new DotNetObjectRef(new object()); - var jsRuntime1 = new TestJsRuntime(); - var jsRuntime2 = new TestJsRuntime(); - objRef.EnsureAttachedToJsRuntime(jsRuntime1); - - var ex = Assert.Throws( - () => objRef.EnsureAttachedToJsRuntime(jsRuntime2)); - Assert.Contains("Do not attempt to re-use", ex.Message); - } - - [Fact] - public void NotifiesAssociatedJsRuntimeOfDisposal() + public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime => { // Arrange - var objRef = new DotNetObjectRef(new object()); - var jsRuntime = new TestJsRuntime(); - objRef.EnsureAttachedToJsRuntime(jsRuntime); + var objRef = DotNetObjectRef.Create(new object()); + var trackingId = objRef.__dotNetObject; // Act objRef.Dispose(); // Assert - Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs); + var ex = Assert.Throws(() => 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 testCode) { - public List UntrackedRefs = new List(); + // 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 InvokeAsync(string identifier, params object[] args) - => throw new NotImplementedException(); - - public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) - => UntrackedRefs.Add(dotNetObjectRef); + var runtime = new TestJSRuntime(); + JSRuntime.SetCurrentJSRuntime(runtime); + testCode(runtime); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs index a13d53677a..dda3f74184 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs @@ -10,15 +10,15 @@ namespace Microsoft.JSInterop.Tests { public class JSInProcessRuntimeBaseTest { - [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1807#issuecomment-470756811")] + [Fact] public void DispatchesSyncCallsAndDeserializesResults() { // Arrange var runtime = new TestJSInProcessRuntime { - NextResultJson = Json.Serialize( - new TestDTO { IntValue = 123, StringValue = "Hello" }) + NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}" }; + JSRuntime.SetCurrentJSRuntime(runtime); // Act var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true); @@ -36,18 +36,19 @@ namespace Microsoft.JSInterop.Tests { // Arrange var runtime = new TestJSInProcessRuntime { NextResultJson = null }; + JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); var obj3 = new object(); // Act // Showing we can pass the DotNetObject either as top-level args or nested - var syncResult = runtime.Invoke("test identifier", - new DotNetObjectRef(obj1), + var syncResult = runtime.Invoke>("test identifier", + DotNetObjectRef.Create(obj1), new Dictionary { - { "obj2", new DotNetObjectRef(obj2) }, - { "obj3", new DotNetObjectRef(obj3) } + { "obj2", DotNetObjectRef.Create(obj2) }, + { "obj3", DotNetObjectRef.Create(obj3) }, }); // Assert: Handles null result string @@ -56,12 +57,12 @@ namespace Microsoft.JSInterop.Tests // Assert: Serialized as expected var call = runtime.InvokeCalls.Single(); Assert.Equal("test identifier", call.Identifier); - Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\"}]", call.ArgsJson); + Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); - Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); - Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); + Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1)); + Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2)); + Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3)); } [Fact] @@ -70,20 +71,22 @@ namespace Microsoft.JSInterop.Tests // Arrange var runtime = new TestJSInProcessRuntime { - NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]" + NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]" }; + JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); // Act - var syncResult = runtime.Invoke("test identifier", - new DotNetObjectRef(obj1), + var syncResult = runtime.Invoke[]>( + "test identifier", + DotNetObjectRef.Create(obj1), "some other arg", - new DotNetObjectRef(obj2)); + DotNetObjectRef.Create(obj2)); var call = runtime.InvokeCalls.Single(); // Assert - Assert.Equal(new[] { obj2, obj1 }, syncResult); + Assert.Equal(new[] { obj2, obj1 }, syncResult.Select(r => r.Value)); } class TestDTO diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs index ab048e812f..82126836d8 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs @@ -1,10 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.JSInterop.Internal; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using Microsoft.JSInterop.Internal; using Xunit; namespace Microsoft.JSInterop.Tests @@ -47,12 +48,13 @@ namespace Microsoft.JSInterop.Tests var task = runtime.InvokeAsync("test identifier", Array.Empty()); Assert.False(unrelatedTask.IsCompleted); Assert.False(task.IsCompleted); + using var jsonDocument = JsonDocument.Parse("\"my result\""); // Act/Assert: Task can be completed runtime.OnEndInvoke( runtime.BeginInvokeCalls[1].AsyncHandle, /* succeeded: */ true, - "my result"); + new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement)); Assert.False(unrelatedTask.IsCompleted); Assert.True(task.IsCompleted); Assert.Equal("my result", task.Result); @@ -69,12 +71,13 @@ namespace Microsoft.JSInterop.Tests var task = runtime.InvokeAsync("test identifier", Array.Empty()); Assert.False(unrelatedTask.IsCompleted); Assert.False(task.IsCompleted); + using var jsonDocument = JsonDocument.Parse("\"This is a test exception\""); // Act/Assert: Task can be failed runtime.OnEndInvoke( runtime.BeginInvokeCalls[1].AsyncHandle, /* succeeded: */ false, - "This is a test exception"); + new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement)); Assert.False(unrelatedTask.IsCompleted); Assert.True(task.IsCompleted); @@ -106,20 +109,21 @@ namespace Microsoft.JSInterop.Tests { // Arrange var runtime = new TestJSRuntime(); + JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); var obj3 = new object(); // Act // Showing we can pass the DotNetObject either as top-level args or nested - var obj1Ref = new DotNetObjectRef(obj1); - var obj1DifferentRef = new DotNetObjectRef(obj1); + var obj1Ref = DotNetObjectRef.Create(obj1); + var obj1DifferentRef = DotNetObjectRef.Create(obj1); runtime.InvokeAsync("test identifier", obj1Ref, new Dictionary { - { "obj2", new DotNetObjectRef(obj2) }, - { "obj3", new DotNetObjectRef(obj3) }, + { "obj2", DotNetObjectRef.Create(obj2) }, + { "obj3", DotNetObjectRef.Create(obj3) }, { "obj1SameRef", obj1Ref }, { "obj1DifferentRef", obj1DifferentRef }, }); @@ -127,28 +131,13 @@ namespace Microsoft.JSInterop.Tests // Assert: Serialized as expected var call = runtime.BeginInvokeCalls.Single(); Assert.Equal("test identifier", call.Identifier); - Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\",\"obj1SameRef\":\"__dotNetObject:1\",\"obj1DifferentRef\":\"__dotNetObject:4\"}]", call.ArgsJson); + Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); - Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); - Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); - Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4)); - } - - [Fact] - public void SupportsCustomSerializationForArguments() - { - // Arrange - var runtime = new TestJSRuntime(); - - // Arrange/Act - runtime.InvokeAsync("test identifier", - new WithCustomArgSerializer()); - - // Asssert - var call = runtime.BeginInvokeCalls.Single(); - Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson); + Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1)); + Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2)); + Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3)); + Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4)); } class TestJSRuntime : JSRuntimeBase @@ -172,20 +161,8 @@ namespace Microsoft.JSInterop.Tests }); } - public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException) - => EndInvokeJS(asyncHandle, succeeded, resultOrException); - } - - class WithCustomArgSerializer : ICustomArgSerializer - { - public object ToJsonPrimitive() - { - return new Dictionary - { - { "key1", "value1" }, - { "key2", 123 }, - }; - } + public void OnEndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult callResult) + => EndInvokeJS(asyncHandle, succeeded, callResult); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index b8a1c363dc..ca8c96df60 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -29,9 +29,6 @@ namespace Microsoft.JSInterop.Tests { public Task InvokeAsync(string identifier, params object[] args) => throw new NotImplementedException(); - - public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) - => throw new NotImplementedException(); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JsonUtilTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JsonUtilTest.cs deleted file mode 100644 index 2b239faab9..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/test/JsonUtilTest.cs +++ /dev/null @@ -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(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 { "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 { { "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(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 { { "Ducks", true }, { "Geese", false } }, person.Allergies); - } - - [Fact] - public void CanDeserializeWithCaseInsensitiveKeys() - { - // Arrange - var json = "{\"ID\":1844,\"NamE\":\"Athos\"}"; - - // Act - var person = Json.Deserialize(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(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(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(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(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(() => - { - Json.Deserialize("{}"); - }); - - 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(() => - { - Json.Deserialize("{}"); - }); - - 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(() => - { - Json.Deserialize(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(() => - 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 Nicknames { get; set; } - public DateTimeOffset BirthInstant { get; set; } - public TimeSpan Age { get; set; } - public IDictionary 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; } - } - } -}