From 51eb7b8f2481de0b05592b79cc4feb1c390cc0be Mon Sep 17 00:00:00 2001 From: Georg von Kries Date: Wed, 26 Jun 2024 19:35:31 +0200 Subject: [PATCH] Adds more features back that were lost due to the removal of Newtonsoft (#16292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Sébastien Ros Co-authored-by: Mike Alhayek Co-authored-by: Hisham Bin Ateya --- .../Json/Dynamic/JsonDynamicArray.cs | 8 +- .../Json/Dynamic/JsonDynamicBase.cs | 13 + .../Json/Dynamic/JsonDynamicObject.cs | 15 +- .../Json/Dynamic/JsonDynamicValue.cs | 540 +++++++++++++++--- .../Serialization/JsonDynamicJsonConverter.cs | 25 + .../Data/JsonDynamicTests.cs | 484 ++++++++++++++++ 6 files changed, 1002 insertions(+), 83 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicBase.cs create mode 100644 src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/JsonDynamicJsonConverter.cs diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicArray.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicArray.cs index dd1ff26e59e..575267e8a87 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicArray.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicArray.cs @@ -5,13 +5,15 @@ using System.Linq; using System.Reflection; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; #nullable enable namespace System.Text.Json.Dynamic; [DebuggerDisplay("JsonDynamicArray[{Count}]")] -public class JsonDynamicArray : DynamicObject, IEnumerable, IEnumerable +[JsonConverter(typeof(JsonDynamicJsonConverter))] +public sealed class JsonDynamicArray : JsonDynamicBase, IEnumerable, IEnumerable { private readonly JsonArray _jsonArray; private readonly Dictionary _dictionary = []; @@ -22,6 +24,8 @@ public class JsonDynamicArray : DynamicObject, IEnumerable, IEnumerable public int Count => _jsonArray.Count; + public override JsonNode Node => _jsonArray; + public object? this[int index] { get => GetValue(index); @@ -140,7 +144,7 @@ public void SetValue(int index, object? value) } } - IEnumerator IEnumerable.GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() => _jsonArray.AsEnumerable().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicBase.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicBase.cs new file mode 100644 index 00000000000..2066b3e32f0 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicBase.cs @@ -0,0 +1,13 @@ +using System.Dynamic; +using System.Text.Json.Nodes; + +#nullable enable + +namespace System.Text.Json.Dynamic; + +public abstract class JsonDynamicBase : DynamicObject +{ + public abstract JsonNode? Node { get; } + + public T? ToObject() => Node.ToObject(); +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicObject.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicObject.cs index 432285a9b97..4e188e7babb 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicObject.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicObject.cs @@ -3,6 +3,7 @@ using System.Dynamic; using System.Reflection; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Text.Json.Settings; #nullable enable @@ -10,10 +11,10 @@ namespace System.Text.Json.Dynamic; [DebuggerDisplay("JsonDynamicObject[{Count}]")] -public class JsonDynamicObject : DynamicObject +[JsonConverter(typeof(JsonDynamicJsonConverter))] +public sealed class JsonDynamicObject : JsonDynamicBase { private readonly JsonObject _jsonObject; - private readonly Dictionary _dictionary = []; public JsonDynamicObject() @@ -28,10 +29,7 @@ public JsonDynamicObject(JsonObject jsonObject) public int Count => _jsonObject.Count; - public void Merge(JsonNode? content, JsonMergeSettings? settings = null) - { - _jsonObject.Merge(content, settings); - } + public override JsonNode Node => _jsonObject; public object? this[string key] { @@ -39,6 +37,11 @@ public object? this[string key] set => SetValue(key, value); } + public void Merge(JsonNode? content, JsonMergeSettings? settings = null) + { + _jsonObject.Merge(content, settings); + } + public override bool TryGetMember(GetMemberBinder binder, out object? result) { if (binder.Name == "{No Member}") diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicValue.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicValue.cs index 3257e3bd7a9..9ebd407573e 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicValue.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Dynamic/JsonDynamicValue.cs @@ -1,27 +1,94 @@ -using System.Collections.Frozen; -using System.Dynamic; using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; +using System.Numerics; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace System.Text.Json.Dynamic; #nullable enable -public class JsonDynamicValue : DynamicObject, IConvertible +[JsonConverter(typeof(JsonDynamicJsonConverter))] +public sealed class JsonDynamicValue : JsonDynamicBase, IComparable, IComparable, IConvertible, IEquatable { + private readonly JsonValue? _jsonValue; + public JsonDynamicValue(JsonValue? jsonValue) { - JsonValue = jsonValue; + _jsonValue = jsonValue; } - public JsonValue? JsonValue { get; } + public override JsonNode? Node => _jsonValue; - public override DynamicMetaObject GetMetaObject(Expression parameter) + int IComparable.CompareTo(object? obj) { - return new JsonDynamicMetaObject(parameter, this); + if (obj is null) + { + return 1; + } + + object? otherValue; + JsonValueKind valueKind; + if (obj is JsonDynamicValue value) + { + otherValue = value._jsonValue.GetObjectValue(); + valueKind = value._jsonValue?.GetValueKind() ?? _jsonValue?.GetValueKind() ?? JsonValueKind.Undefined; + } + else + { + otherValue = obj; + valueKind = _jsonValue?.GetValueKind() ?? JsonValueKind.Undefined; + } + + return Compare(_jsonValue.GetObjectValue(), otherValue, valueKind); + } + + public int CompareTo(JsonDynamicValue? other) + { + if (other is null) + { + return 1; + } + + var valueKind = other._jsonValue?.GetValueKind() ?? _jsonValue?.GetValueKind() ?? JsonValueKind.Undefined; + + return Compare(_jsonValue.GetObjectValue(), other._jsonValue.GetObjectValue(), valueKind); + } + + public override bool Equals(object? obj) + => Equals(obj as JsonDynamicValue); + + public bool Equals(JsonDynamicValue? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (ReferenceEquals(other, null)) + { + return false; + } + + var obj = _jsonValue.GetObjectValue(); + var objOther = other._jsonValue.GetObjectValue(); + + if (ReferenceEquals(obj, objOther)) + { + return true; + } + + if (ReferenceEquals(obj, null) || + ReferenceEquals(objOther, null)) + { + return false; + } + + return obj.Equals(objOther); + } + + public override int GetHashCode() + { + return _jsonValue.GetObjectValue()?.GetHashCode() ?? 0; } public override string ToString() @@ -41,12 +108,12 @@ public string ToString(IFormatProvider? formatProvider) public string ToString(string? format, IFormatProvider? formatProvider) { - if (JsonValue == null) + if (_jsonValue == null) { return string.Empty; } - var value = JsonValue.GetObjectValue(); + var value = _jsonValue.GetObjectValue(); if (value is IFormattable formattable) { return formattable.ToString(format, formatProvider); @@ -59,7 +126,7 @@ public string ToString(string? format, IFormatProvider? formatProvider) TypeCode IConvertible.GetTypeCode() { - if (JsonValue == null) + if (_jsonValue == null) { return TypeCode.Empty; } @@ -129,7 +196,7 @@ string IConvertible.ToString(IFormatProvider? provider) object IConvertible.ToType(Type conversionType, IFormatProvider? provider) { - return JsonValue?.ToObject(conversionType) + return _jsonValue?.ToObject(conversionType) ?? throw new InvalidOperationException($"Cannot convert {this} to {conversionType}"); } @@ -148,147 +215,182 @@ ulong IConvertible.ToUInt64(IFormatProvider? provider) return (ulong)this; } + public static bool operator ==(JsonDynamicValue left, JsonDynamicValue right) + { + if (ReferenceEquals(left, null)) + { + return ReferenceEquals(right, null); + } + + return left.Equals(right); + } + + public static bool operator !=(JsonDynamicValue left, JsonDynamicValue right) + { + return !(left == right); + } + + public static bool operator <(JsonDynamicValue left, JsonDynamicValue right) + { + return ReferenceEquals(left, null) ? !ReferenceEquals(right, null) : left.CompareTo(right) < 0; + } + + public static bool operator <=(JsonDynamicValue left, JsonDynamicValue right) + { + return ReferenceEquals(left, null) || left.CompareTo(right) <= 0; + } + + public static bool operator >(JsonDynamicValue left, JsonDynamicValue right) + { + return !ReferenceEquals(left, null) && left.CompareTo(right) > 0; + } + + public static bool operator >=(JsonDynamicValue left, JsonDynamicValue right) + { + return ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.CompareTo(right) >= 0; + } + public static explicit operator bool(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Boolean"); } public static explicit operator bool?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator byte(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Byte"); } public static explicit operator byte?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator char(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Char"); } public static explicit operator char?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator DateTime(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to DateTime"); } public static explicit operator DateTime?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator DateTimeOffset(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to DateTimeOffset"); } public static explicit operator DateTimeOffset?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator decimal(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Decimal"); } public static explicit operator decimal?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator double(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Double"); } public static explicit operator double?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator Guid(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Guid"); } public static explicit operator Guid?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator short(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Int16"); } public static explicit operator short?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator int(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Int32"); } public static explicit operator int?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator long(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Int64"); } public static explicit operator long?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator sbyte(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to SByte"); } public static explicit operator sbyte?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator float(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to Float"); } public static explicit operator float?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator string?(JsonDynamicValue value) @@ -298,40 +400,40 @@ public static explicit operator float(JsonDynamicValue value) public static explicit operator ushort(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to UInt32"); } public static explicit operator ushort?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator uint(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to UInt32"); } public static explicit operator uint?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator ulong(JsonDynamicValue value) { - return value?.JsonValue?.GetValue() + return value?._jsonValue?.GetValue() ?? throw new InvalidCastException($"Cannot convert {value} to UInt64"); } public static explicit operator ulong?(JsonDynamicValue value) { - return value?.JsonValue?.GetValue(); + return value?._jsonValue?.GetValue(); } public static explicit operator byte[]?(JsonDynamicValue value) { - if (value?.JsonValue.GetObjectValue() is string str) + if (value?._jsonValue.GetObjectValue() is string str) { return Convert.FromBase64String(str); } @@ -341,7 +443,7 @@ public static explicit operator ulong(JsonDynamicValue value) public static explicit operator TimeSpan(JsonDynamicValue value) { - if (value?.JsonValue?.GetObjectValue() is string str) + if (value?._jsonValue?.GetObjectValue() is string str) { return TimeSpan.Parse(str, CultureInfo.InvariantCulture); } @@ -351,7 +453,7 @@ public static explicit operator TimeSpan(JsonDynamicValue value) public static explicit operator TimeSpan?(JsonDynamicValue value) { - var str = value?.JsonValue?.GetObjectValue() as string; + var str = value?._jsonValue?.GetObjectValue() as string; return str is not null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) @@ -360,46 +462,334 @@ public static explicit operator TimeSpan(JsonDynamicValue value) public static explicit operator Uri?(JsonDynamicValue value) { - return new(value?.JsonValue?.GetValue() ?? string.Empty); + return new(value?._jsonValue?.GetValue() ?? string.Empty); + } + + public static implicit operator JsonDynamicValue(bool value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(bool? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(byte value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(byte? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(char value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(char? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(DateTime value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); } - private sealed class JsonDynamicMetaObject : DynamicMetaObject + public static implicit operator JsonDynamicValue(DateTime? value) { - private static readonly FrozenDictionary _cachedReflectionInfo = typeof(JsonDynamicValue) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(method => method.Name == "op_Explicit") - .ToFrozenDictionary(method => method.ReturnType); + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(DateTimeOffset value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(DateTimeOffset? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(decimal value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(decimal? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } - public JsonDynamicMetaObject(Expression expression, JsonDynamicValue value) - : base(expression, BindingRestrictions.Empty, value) + public static implicit operator JsonDynamicValue(double value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(double? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(Guid value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(Guid? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(short value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(short? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(int value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(int? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(long value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(long? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(sbyte value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(sbyte? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(float value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(float? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(string value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(ushort value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(ushort? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(uint value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(uint? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(ulong value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(ulong? value) + { + return new JsonDynamicValue(JsonValue.Create(value, JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(byte[] value) + { + return new JsonDynamicValue(JsonValue.Create(Convert.ToBase64String(value), JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(TimeSpan value) + { + return new JsonDynamicValue(JsonValue.Create(value.ToString(), JOptions.Node)); + } + + public static implicit operator JsonDynamicValue(TimeSpan? value) + { + return new JsonDynamicValue(value.HasValue ? JsonValue.Create(value.ToString(), JOptions.Node) : null); + } + + public static implicit operator JsonDynamicValue(Uri? value) + { + return new JsonDynamicValue(value != null ? JsonValue.Create(value.ToString(), JOptions.Node) : null); + } + + public static implicit operator JsonValue?(JsonDynamicValue value) => value._jsonValue; + + public static implicit operator JsonDynamicValue(JsonValue value) => new(value); + + private static int Compare(object? objA, object? objB, JsonValueKind valueType) + { + if (objA == objB) + { + return 0; + } + + if (objB == null) + { + return 1; + } + + if (objA == null) + { + return -1; + } + + switch (valueType) + { + case JsonValueKind.False: + case JsonValueKind.True: + + var b1 = Convert.ToBoolean(objA, CultureInfo.InvariantCulture); + var b2 = Convert.ToBoolean(objB, CultureInfo.InvariantCulture); + + return b1.CompareTo(b2); + + case JsonValueKind.Number: + + if (objA is BigInteger integerA) + { + return CompareBigInteger(integerA, objB); + } + if (objB is BigInteger integerB) + { + return -CompareBigInteger(integerB, objA); + } + + if (objA is ulong || objB is ulong || objA is decimal || objB is decimal) + { + return Convert.ToDecimal(objA, CultureInfo.InvariantCulture).CompareTo(Convert.ToDecimal(objB, CultureInfo.InvariantCulture)); + } + else if (objA is float || objB is float || objA is double || objB is double) + { + var d1 = Convert.ToDouble(objA, CultureInfo.InvariantCulture); + var d2 = Convert.ToDouble(objB, CultureInfo.InvariantCulture); + + return d1.CompareTo(d2); + } + else + { + return Convert.ToInt64(objA, CultureInfo.InvariantCulture).CompareTo(Convert.ToInt64(objB, CultureInfo.InvariantCulture)); + } + + case JsonValueKind.String: + + var s1 = Convert.ToString(objA, CultureInfo.InvariantCulture); + var s2 = Convert.ToString(objB, CultureInfo.InvariantCulture); + + return string.CompareOrdinal(s1, s2); + + default: + throw new NotImplementedException($"Comparing {objA?.GetType()} to {objB?.GetType()} is currently not implemented."); + } + } + + private static int CompareBigInteger(BigInteger i1, object i2) + { + int result = i1.CompareTo(ToBigInteger(i2)); + + if (result != 0) + { + return result; + } + + // Converting a fractional number to a BigInteger will lose the fraction + // check for fraction if result is two numbers are equal + if (i2 is decimal d1) + { + return (0m).CompareTo(Math.Abs(d1 - Math.Truncate(d1))); + } + else if (i2 is double || i2 is float) { + double d = Convert.ToDouble(i2, CultureInfo.InvariantCulture); + return (0d).CompareTo(Math.Abs(d - Math.Truncate(d))); } + return result; - // The 'BindConvert()' method is automatically invoked to handle type conversion when casting - // dynamic types to static types in C#. - // - // For example, when extracting a 'DateTime' value from a dynamically typed - // content item's field: - // - // dynamic contentItem = [...]; // Assume contentItem is initialized properly - // - // 'BindConvert()' is called implicitly to convert contentItem.Content.MyPart.MyField.Value - // to 'DateTime' with similar behavior to the following: - // var dateTimeValue = (DateTime)contentItem.Content.MyPart.MyField.Value; - public override DynamicMetaObject BindConvert(ConvertBinder binder) + static BigInteger ToBigInteger(object value) { - var targetType = binder.Type; + if (value is BigInteger integer) + { + return integer; + } + + if (value is string s) + { + return BigInteger.Parse(s, CultureInfo.InvariantCulture); + } - if (_cachedReflectionInfo.TryGetValue(targetType, out var castMethod)) + if (value is float f) + { + return new BigInteger(f); + } + if (value is double d) + { + return new BigInteger(d); + } + if (value is decimal @decimal) + { + return new BigInteger(@decimal); + } + if (value is int i) + { + return new BigInteger(i); + } + if (value is long l) { - var convertExpression = Expression.Convert(Expression.Convert(Expression, typeof(JsonDynamicValue)), targetType, castMethod); + return new BigInteger(l); + } + if (value is uint u) + { + return new BigInteger(u); + } + if (value is ulong @ulong) + { + return new BigInteger(@ulong); + } - return new DynamicMetaObject(convertExpression, BindingRestrictions.GetTypeRestriction(Expression, typeof(JsonDynamicValue))); + if (value is byte[] bytes) + { + return new BigInteger(bytes); } - // Fallback to default behavior. - return base.BindConvert(binder); + throw new InvalidCastException($"Cannot convert {value.GetType()} to BigInteger."); } } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/JsonDynamicJsonConverter.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/JsonDynamicJsonConverter.cs new file mode 100644 index 00000000000..5fee1131f45 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/JsonDynamicJsonConverter.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Dynamic; + +#nullable enable + +namespace System.Text.Json.Serialization; + +public sealed class JsonDynamicJsonConverter : JsonConverter where T : JsonDynamicBase +{ + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException($"Deserializing a {typeof(T).Name} is not supported."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value.Node != null) + { + value.Node.WriteTo(writer, options); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/test/OrchardCore.Tests/Data/JsonDynamicTests.cs b/test/OrchardCore.Tests/Data/JsonDynamicTests.cs index 5060531a1d8..ee27af8a83f 100644 --- a/test/OrchardCore.Tests/Data/JsonDynamicTests.cs +++ b/test/OrchardCore.Tests/Data/JsonDynamicTests.cs @@ -1,5 +1,9 @@ +using System.Dynamic; +using System.Text.Json; using System.Text.Json.Dynamic; using System.Text.Json.Nodes; +using OrchardCore.ContentFields.Fields; +using OrchardCore.ContentManagement; namespace OrchardCore.Tests.Data; @@ -337,4 +341,484 @@ public void JsonDynamicValueMustConvertToUri() Assert.Equal(expectedValue, (Uri)myDynamic); } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToBool() + { + var expectedValue = true; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableBool() + { + bool? expectedValue = true; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToByte() + { + byte expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableBye() + { + byte? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToChar() + { + var expectedValue = 'A'; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableChar() + { + char? expectedValue = 'B'; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToDateTime() + { + var expectedValue = DateTime.UtcNow; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableDateTime() + { + DateTime? expectedValue = DateTime.UtcNow; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToDateTimeOffset() + { + DateTimeOffset expectedValue = DateTimeOffset.UtcNow; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullablDateTimeOffset() + { + DateTimeOffset? expectedValue = DateTimeOffset.UtcNow; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToDecimal() + { + decimal expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableDecimal() + { + decimal? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToDouble() + { + double expectedValue = 42.42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableDouble() + { + double? expectedValue = 42.42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToGuid() + { + Guid expectedValue = Guid.NewGuid(); + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableGuid() + { + Guid? expectedValue = Guid.NewGuid(); + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToInt16() + { + short expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableInt16() + { + short? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToInt32() + { + int expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableInt32() + { + int? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToInt64() + { + long expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableInt64() + { + long? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToSByte() + { + sbyte expectedValue = -42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableSByte() + { + sbyte? expectedValue = -42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToSingle() + { + float expectedValue = 42.42F; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableSingle() + { + float? expectedValue = 42.42F; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToString() + { + var expectedValue = "A test string value"; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToUInt16() + { + ushort expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableUInt16() + { + ushort? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToUInt32() + { + uint expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableUInt32() + { + uint? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToUInt64() + { + ulong expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableUInt64() + { + ulong? expectedValue = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToByteArray() + { + var expectedValue = Encoding.UTF8.GetBytes("A string in a byte array"); + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue)); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToTimeSpan() + { + var expectedValue = TimeSpan.FromSeconds(42); + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue.ToString())); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToNullableTimeSpan() + { + var expectedValue = TimeSpan.FromSeconds(42); + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue.ToString())); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + [Fact] + public void JsonDynamicValueCanImplicitlyConvertToUri() + { + var expectedValue = new Uri("https://www.orchardcore.net"); + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(expectedValue.ToString())); + + Assert.True(expectedValue == myDynamic); + Assert.False(expectedValue != myDynamic); + } + + // Note: Direct comparison for additional types must be added later. Currently only + // numbers, booleans and strings are supported. + [Fact] + public void JsonDynamicValueIsComparableToInt32() + { + int value = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(value)); + + Assert.True(value >= myDynamic); + Assert.True(value + 10 > myDynamic); + + Assert.False(value - 10 >= myDynamic); + Assert.False(value > myDynamic); + + Assert.True(value <= myDynamic); + Assert.True(value - 10 < myDynamic); + + Assert.False(value + 10 <= myDynamic); + Assert.False(value < myDynamic); + } + + [Fact] + public void JsonDynamicValueIsComparableToInt64() + { + long value = 42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(value)); + + Assert.True(value >= myDynamic); + Assert.True(value + 10 > myDynamic); + + Assert.False(value - 10 >= myDynamic); + Assert.False(value > myDynamic); + + Assert.True(value <= myDynamic); + Assert.True(value - 10 < myDynamic); + + Assert.False(value + 10 <= myDynamic); + Assert.False(value < myDynamic); + } + + [Fact] + public void JsonDynamicValueIsComparableToDouble() + { + double value = 42.42; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(value)); + + Assert.True(value >= myDynamic); + Assert.True(value + 10 > myDynamic); + + Assert.False(value - 10 >= myDynamic); + Assert.False(value > myDynamic); + + Assert.True(value <= myDynamic); + Assert.True(value - 10 < myDynamic); + + Assert.False(value + 10 <= myDynamic); + Assert.False(value < myDynamic); + } + + [Fact] + public void JsonDynamicValueIsComparableToString() + { + var value = "A string value"; + dynamic myDynamic = new JsonDynamicValue(JsonValue.Create(value)); + + Assert.True(value >= myDynamic); + Assert.True("B " + value > myDynamic); + + Assert.False("0 " + value >= myDynamic); + Assert.False(value > myDynamic); + + Assert.True(value <= myDynamic); + Assert.True("0 " + value < myDynamic); + + Assert.False("B " + value <= myDynamic); + Assert.False(value < myDynamic); + } + + [Fact] + public void SerializingJsonDynamicValueMustWriteValueOnly() + { + // Arrange + var contentItem = new ContentItem(); + contentItem.Alter(part => + { + part.TextFieldProp = new TextField { Text = "test" }; + part.NumericFieldProp = new NumericField { Value = 123 }; + part.BooleanFieldProp = new BooleanField { Value = true }; + }); + + // Act + dynamic expandoValue = new ExpandoObject(); + expandoValue.stringValue = contentItem.Content.TestPart.TextFieldProp.Text; + expandoValue.numberValue = contentItem.Content.TestPart.NumericFieldProp.Value; + expandoValue.booleanValue = contentItem.Content.TestPart.BooleanFieldProp.Value; + var jsonStr = JConvert.SerializeObject((ExpandoObject)expandoValue); + + // Assert + Assert.Equal("{\"stringValue\":\"test\",\"numberValue\":123,\"booleanValue\":true}", jsonStr); + } + + public sealed class TestPart : ContentPart + { + public TextField TextFieldProp { get; set; } + + public NumericField NumericFieldProp { get; set; } + + public BooleanField BooleanFieldProp { get; set; } + } }