diff --git a/src/Microsoft.OData.Core/Json/ODataJsonPropertyAndValueDeserializer.cs b/src/Microsoft.OData.Core/Json/ODataJsonPropertyAndValueDeserializer.cs index 2f84180ed5..0237a7ba4b 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonPropertyAndValueDeserializer.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonPropertyAndValueDeserializer.cs @@ -208,6 +208,7 @@ internal object ReadODataOrCustomInstanceAnnotationValue(string annotationName, /// Function that takes a primitive value and returns an . /// Whether unknown properties should be read as a raw string value. /// Whether to generate a type if not already part of the model. + /// Whether untyped numeric values should be preserved as decimals. Default is /// The of the current value to be read. [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Each code path casts to bool at most one time, and only if needed.")] internal static IEdmTypeReference ResolveUntypedType( @@ -217,7 +218,8 @@ internal static IEdmTypeReference ResolveUntypedType( IEdmTypeReference payloadTypeReference, Func primitiveTypeResolver, bool readUntypedAsString, - bool generateTypeIfMissing) + bool generateTypeIfMissing, + bool readUntypedNumericAsDecimal) { if (payloadTypeReference != null && (payloadTypeReference.TypeKind() != EdmTypeKind.Untyped || readUntypedAsString)) { @@ -257,23 +259,34 @@ internal static IEdmTypeReference ResolveUntypedType( TypeUtils.ParseQualifiedTypeName(payloadTypeName, out namespaceName, out name, out isCollection); Debug.Assert(namespaceName != Metadata.EdmConstants.EdmNamespace, "If type was in the edm namespace it should already have been resolved"); - typeReference = new EdmUntypedStructuredType(namespaceName, name).ToTypeReference(/*isNullable*/ true); - return isCollection ? new EdmCollectionType(typeReference).ToTypeReference(/*isNullable*/ true) : typeReference; + typeReference = new EdmUntypedStructuredType(namespaceName, name).ToTypeReference(nullable: true); + return isCollection ? new EdmCollectionType(typeReference).ToTypeReference(nullable: true) : typeReference; } - typeReference = EdmCoreModel.Instance.GetString(/*isNullable*/ true); + typeReference = EdmCoreModel.Instance.GetString(isNullable: true); } else if (jsonReaderValue is bool) { - typeReference = EdmCoreModel.Instance.GetBoolean(/*isNullable*/ true); + typeReference = EdmCoreModel.Instance.GetBoolean(isNullable: true); } else if (jsonReaderValue is string) { - typeReference = EdmCoreModel.Instance.GetString(/*isNullable*/ true); + typeReference = EdmCoreModel.Instance.GetString(isNullable: true); + } + // This is for backward compatibility with untyped numeric values. + else if (readUntypedNumericAsDecimal) + { + typeReference = EdmCoreModel.Instance.GetDecimal(isNullable: true); } else { - typeReference = EdmCoreModel.Instance.GetDecimal(/*isNullable*/ true); + typeReference = jsonReaderValue switch + { + int _ => EdmCoreModel.Instance.GetInt32(isNullable: true), + long _ => EdmCoreModel.Instance.GetInt64(isNullable: true), + decimal _ => EdmCoreModel.Instance.GetDecimal(isNullable: true), + _ => EdmCoreModel.Instance.GetDouble(isNullable: true), + }; } if (payloadTypeName != null) @@ -284,7 +297,7 @@ internal static IEdmTypeReference ResolveUntypedType( throw new ODataException(Error.Format(SRResources.ODataJsonPropertyAndValueDeserializer_CollectionTypeNotExpected, payloadTypeName)); } - typeReference = new EdmTypeDefinition(namespaceName, name, typeReference.PrimitiveKind()).ToTypeReference(/*isNullable*/ true); + typeReference = new EdmTypeDefinition(namespaceName, name, typeReference.PrimitiveKind()).ToTypeReference(nullable: true); } return typeReference; @@ -298,10 +311,10 @@ internal static IEdmTypeReference ResolveUntypedType( throw new ODataException(Error.Format(SRResources.ODataJsonPropertyAndValueDeserializer_CollectionTypeNotExpected, payloadTypeName)); } - return new EdmUntypedStructuredType(namespaceName, name).ToTypeReference(/*isNullable*/ true); + return new EdmUntypedStructuredType(namespaceName, name).ToTypeReference(nullable: true); } - return new EdmUntypedStructuredType().ToTypeReference(/*isNullable*/ true); + return new EdmUntypedStructuredType().ToTypeReference(nullable: true); case JsonNodeType.StartArray: if (payloadTypeName != null && generateTypeIfMissing) @@ -312,10 +325,10 @@ internal static IEdmTypeReference ResolveUntypedType( throw new ODataException(Error.Format(SRResources.ODataJsonPropertyAndValueDeserializer_CollectionTypeExpected, payloadTypeName)); } - return new EdmCollectionType(new EdmUntypedStructuredType(namespaceName, name).ToTypeReference(/*isNullable*/ true)).ToTypeReference(/*isNullable*/true); + return new EdmCollectionType(new EdmUntypedStructuredType(namespaceName, name).ToTypeReference(nullable: true)).ToTypeReference(nullable: true); } - return new EdmCollectionType(new EdmUntypedStructuredType().ToTypeReference(/*isNullable*/ true)).ToTypeReference(/*isNullable*/true); + return new EdmCollectionType(new EdmUntypedStructuredType().ToTypeReference(nullable: true)).ToTypeReference(nullable: true); default: return EdmCoreModel.Instance.GetUntyped(); @@ -512,7 +525,8 @@ protected ODataJsonReaderNestedResourceInfo InnerReadUndeclaredProperty(IODataJs payloadTypeReference, this.MessageReaderSettings.PrimitiveTypeResolver, this.MessageReaderSettings.ReadUntypedAsString, - !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal)); if (payloadTypeReference.ToStructuredType() != null) { @@ -1951,7 +1965,8 @@ private object ReadNonEntityValueImplementation( expectedTypeReference, this.MessageReaderSettings.PrimitiveTypeResolver, this.MessageReaderSettings.ReadUntypedAsString, - !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal)); targetTypeKind = targetTypeReference.TypeKind(); } @@ -2328,7 +2343,8 @@ await this.JsonReader.GetValueAsync().ConfigureAwait(false), payloadTypeReference, this.MessageReaderSettings.PrimitiveTypeResolver, this.MessageReaderSettings.ReadUntypedAsString, - !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal)); if (payloadTypeReference.ToStructuredType() != null) { @@ -3285,7 +3301,8 @@ await this.JsonReader.GetValueAsync().ConfigureAwait(false), expectedTypeReference, this.MessageReaderSettings.PrimitiveTypeResolver, this.MessageReaderSettings.ReadUntypedAsString, - !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal)); targetTypeKind = targetTypeReference.TypeKind(); } diff --git a/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs b/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs index 420268b3a3..b6a5121aa9 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs @@ -1600,7 +1600,8 @@ private ODataJsonReaderNestedInfo InnerReadUndeclaredProperty(IODataJsonReaderRe payloadTypeReference, this.MessageReaderSettings.PrimitiveTypeResolver, this.MessageReaderSettings.ReadUntypedAsString, - !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal)); bool isCollection = payloadTypeReference.IsCollection(); IEdmStructuredType payloadTypeOrItemType = payloadTypeReference.ToStructuredType(); @@ -3824,7 +3825,8 @@ await this.JsonReader.GetValueAsync().ConfigureAwait(false), payloadTypeReference, this.MessageReaderSettings.PrimitiveTypeResolver, this.MessageReaderSettings.ReadUntypedAsString, - !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + !this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal)); bool isCollection = payloadTypeReference.IsCollection(); IEdmStructuredType payloadTypeOrItemType = payloadTypeReference.ToStructuredType(); diff --git a/src/Microsoft.OData.Core/ODataLibraryCompatibility.cs b/src/Microsoft.OData.Core/ODataLibraryCompatibility.cs index 71fd192318..e277a56ffd 100644 --- a/src/Microsoft.OData.Core/ODataLibraryCompatibility.cs +++ b/src/Microsoft.OData.Core/ODataLibraryCompatibility.cs @@ -44,14 +44,24 @@ public enum ODataLibraryCompatibility /// UseLegacyODataInnerErrorSerialization = 1 << 4, + /// + /// When enabled, untyped numeric values are read as decimal. + /// + ReadUntypedNumericAsDecimal = 1 << 5, + /// /// Version 6.x /// - Version6 = UseLegacyVariableCasing | WriteTopLevelODataNullAnnotation | WriteODataContextAnnotationForNavProperty | DoNotThrowExceptionForTopLevelNullProperty | UseLegacyODataInnerErrorSerialization, + Version6 = UseLegacyVariableCasing | WriteTopLevelODataNullAnnotation | WriteODataContextAnnotationForNavProperty | DoNotThrowExceptionForTopLevelNullProperty | UseLegacyODataInnerErrorSerialization | ReadUntypedNumericAsDecimal, /// /// Version 7.x /// - Version7 = UseLegacyVariableCasing | UseLegacyODataInnerErrorSerialization + Version7 = UseLegacyVariableCasing | UseLegacyODataInnerErrorSerialization | ReadUntypedNumericAsDecimal, + + /// + /// Version 8.x + /// + Version8 = ReadUntypedNumericAsDecimal, } } diff --git a/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Shipped.txt index 8926f2f127..983693fad1 100644 --- a/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -61,7 +61,6 @@ Microsoft.OData.ODataLibraryCompatibility.None = 0 -> Microsoft.OData.ODataLibra Microsoft.OData.ODataLibraryCompatibility.UseLegacyODataInnerErrorSerialization = 16 -> Microsoft.OData.ODataLibraryCompatibility Microsoft.OData.ODataLibraryCompatibility.UseLegacyVariableCasing = 1 -> Microsoft.OData.ODataLibraryCompatibility Microsoft.OData.ODataLibraryCompatibility.Version6 = Microsoft.OData.ODataLibraryCompatibility.WriteTopLevelODataNullAnnotation | Microsoft.OData.ODataLibraryCompatibility.WriteODataContextAnnotationForNavProperty | Microsoft.OData.ODataLibraryCompatibility.DoNotThrowExceptionForTopLevelNullProperty | Microsoft.OData.ODataLibraryCompatibility.Version7 -> Microsoft.OData.ODataLibraryCompatibility -Microsoft.OData.ODataLibraryCompatibility.Version7 = Microsoft.OData.ODataLibraryCompatibility.UseLegacyVariableCasing | Microsoft.OData.ODataLibraryCompatibility.UseLegacyODataInnerErrorSerialization -> Microsoft.OData.ODataLibraryCompatibility Microsoft.OData.ODataLibraryCompatibility.WriteODataContextAnnotationForNavProperty = 4 -> Microsoft.OData.ODataLibraryCompatibility Microsoft.OData.ODataLibraryCompatibility.WriteTopLevelODataNullAnnotation = 2 -> Microsoft.OData.ODataLibraryCompatibility Microsoft.OData.ODataError.Code.get -> string diff --git a/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt index e69de29bb2..95797fef7e 100644 --- a/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OData.Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +Microsoft.OData.ODataLibraryCompatibility.ReadUntypedNumericAsDecimal = 32 -> Microsoft.OData.ODataLibraryCompatibility +Microsoft.OData.ODataLibraryCompatibility.Version7 = Microsoft.OData.ODataLibraryCompatibility.UseLegacyVariableCasing | Microsoft.OData.ODataLibraryCompatibility.UseLegacyODataInnerErrorSerialization | Microsoft.OData.ODataLibraryCompatibility.ReadUntypedNumericAsDecimal -> Microsoft.OData.ODataLibraryCompatibility +Microsoft.OData.ODataLibraryCompatibility.Version8 = 32 -> Microsoft.OData.ODataLibraryCompatibility \ No newline at end of file diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Client/UntypedTypes/UntypedTypesODataServiceCsdl.xml b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Client/UntypedTypes/UntypedTypesODataServiceCsdl.xml new file mode 100644 index 0000000000..54b461bcdd --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Client/UntypedTypes/UntypedTypesODataServiceCsdl.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesController.cs b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesController.cs new file mode 100644 index 0000000000..921561b93a --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesController.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; + +namespace Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes; + +public class UntypedTypesController : ODataController +{ + private static UntypedTypesDataSource _dataSource; + + [EnableQuery] + [HttpGet("odata/Customers")] + public IActionResult GetCustomers() + { + return Ok(_dataSource.Customers); + } + + [EnableQuery] + [HttpGet("odata/Customers({key})")] + public IActionResult GetCustomer([FromODataUri] int key) + { + var res = _dataSource.Customers?.FirstOrDefault(a => a.Id == key); + if (res == null) + { + return NotFound(); + } + + return Ok(res); + } + + [EnableQuery] + [HttpGet("odata/Customers({key})/UntypedProperty")] + public IActionResult GetCustomersUntypedProperty([FromODataUri] int key) + { + var res = _dataSource.Customers?.FirstOrDefault(a => a.Id == key); + if (res == null) + { + return NotFound(); + } + + return Ok(res.UntypedProperty); + } + + [EnableQuery] + [HttpGet("odata/Customers({key})/UntypedList")] + public IActionResult GetCustomersUntypedList([FromODataUri] int key) + { + var res = _dataSource.Customers?.FirstOrDefault(a => a.Id == key); + if (res == null) + { + return NotFound(); + } + + return Ok(res.UntypedList); + } + + [HttpPost("odata/Customers")] + public IActionResult CreateCustomer([FromBody] Customer customer) + { + _dataSource.Customers?.Add(customer); + return Created(customer); + } + + [HttpPatch("odata/Customers({key})")] + public IActionResult UpdateAccount([FromODataUri] int key, [FromBody] Delta delta) + { + var existing = _dataSource.Customers?.FirstOrDefault(a => a.Id == key); + if (existing == null) + { + return NotFound(); + } + + var updated = delta.Patch(existing); + return Updated(updated); + } + + [HttpPost("odata/untypedtypes/Default.ResetDefaultDataSource")] + public IActionResult ResetDefaultDataSource() + { + _dataSource = UntypedTypesDataSource.CreateInstance(); + + return Ok(); + } +} diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesDataModel.cs b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesDataModel.cs new file mode 100644 index 0000000000..0ce9f0d59a --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesDataModel.cs @@ -0,0 +1,22 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes; + +public class Customer +{ + public int Id { get; set; } + public string? Name { get; set; } + public object? UntypedProperty { get; set; } = null; + public List? UntypedList { get; set; } = null; + public List Orders { get; set; } = new List(); +} + +public class Order +{ + public int Id { get; set; } + public decimal Amount { get; set; } +} diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesDataSource.cs b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesDataSource.cs new file mode 100644 index 0000000000..012e689177 --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesDataSource.cs @@ -0,0 +1,270 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes; + +public class UntypedTypesDataSource +{ + public static UntypedTypesDataSource CreateInstance() + { + return new UntypedTypesDataSource(); + } + + public UntypedTypesDataSource() + { + ResetDataSource(); + Initialize(); + } + + public IList? Customers { get; private set; } + public IList? Orders { get; private set; } + + private void Initialize() + { + this.Customers = + [ + new Customer + { + Id = 1, + Name = "Customer 1", + UntypedProperty = 3.40282347E+38, + UntypedList = new List { "String in list", 123, true, 45.67 }, + Orders = new List + { + new Order { Id = 1, Amount = 100 }, + new Order { Id = 2, Amount = 200 } + } + }, + new Customer + { + Id = 2, + Name = "Customer 2", + UntypedProperty = -456, + UntypedList = new List { "Another string", 789, false, 12.34 }, + Orders = new List + { + new Order { Id = 3, Amount = 300 } + } + }, + new Customer + { + Id = 3, + Name = "Customer 3", + UntypedProperty = 6.02214076e+23, + UntypedList = new List { "Yet another string", 101112, true, 56.78 }, + Orders = [] + }, + new Customer + { + Id = 4, + Name = "Customer 4", + UntypedProperty = null, + UntypedList = new List(), + Orders = [] + }, + new Customer + { + Id = 5, + Name = "Customer 5", + UntypedProperty = true, + UntypedList = new List { "Final string", 131415, false, 90.12 }, + Orders = [] + }, + new Customer + { + Id = 6, + Name = "Customer 6", + UntypedProperty = 78.9, + UntypedList = new List { "Extra string", 161718, true, 34.56 }, + Orders = [] + }, + new Customer + { + Id = 7, + Name = "Customer 7", + UntypedProperty = new List { "List as untyped property", 192021, false, 78.90 }, + UntypedList = new List { "More strings", 222324, true, 12.34 }, + Orders = [] + }, + new Customer + { + Id = 8, + Name = "Small integer", + UntypedProperty = 123, + UntypedList = new List { "123", 123, "Small integer" }, + Orders = new List { new Order { Id = 10, Amount = 123 } } + }, + new Customer + { + Id = 9, + Name = "Negative integer", + UntypedProperty = -42, + UntypedList = new List { "-42", -42, "Negative integer" }, + Orders = new List { new Order { Id = 11, Amount = -42 } } + }, + new Customer + { + Id = 10, + Name = "Max int32", + UntypedProperty = 2147483647, + UntypedList = new List { "2147483647", 2147483647, "Max int32" }, + Orders = new List { new Order { Id = 12, Amount = 2147483647 } } + }, + new Customer + { + Id = 11, + Name = "Beyond int32", + UntypedProperty = 2147483648, + UntypedList = new List { "2147483648", 2147483648L, "Beyond int32" }, + Orders = new List { new Order { Id = 13, Amount = 2147483648 } } + }, + new Customer + { + Id = 12, + Name = "Max int64", + UntypedProperty = 9223372036854775807, + UntypedList = new List { "9223372036854775807", 9223372036854775807L, "Max int64" }, + Orders = new List { new Order { Id = 14, Amount = 9223372036854775807 } } + }, + new Customer + { + Id = 13, + Name = "Beyond int64", + UntypedProperty = 9223372036854775808m, + UntypedList = new List { "9223372036854775808", "Beyond int64" }, + Orders = new List { new Order { Id = 15, Amount = 922337203685477580 } } + }, + new Customer + { + Id = 14, + Name = "Simple decimal", + UntypedProperty = 123.456, + UntypedList = new List { "123.456", 123.456m, "Simple decimal" }, + Orders = new List { new Order { Id = 16, Amount = 123.456m } } + }, + new Customer + { + Id = 15, + Name = "Very small decimal", + UntypedProperty = 0.0000001, + UntypedList = new List { "0.0000001", 0.0000001m, "Very small decimal" }, + Orders = new List { new Order { Id = 17, Amount = 56787 } } + }, + new Customer + { + Id = 16, + Name = "High precision (pi)", + UntypedProperty = 3.14159265358979323846, + UntypedList = new List { "3.14159265358979323846", 3.14159265358979323846, "High precision (pi)" }, + Orders = new List { new Order { Id = 18, Amount = 200 } } + }, + new Customer + { + Id = 17, + Name = "1. Large decimal with high precision", + UntypedProperty = 123456789012345.123456789012345, + UntypedList = new List { "123456789012345.123456789012345", 123456789012345.123456789012345, "1. Large decimal with high precision" }, + Orders = new List { new Order { Id = 19, Amount = 123456789012345m } } + }, + new Customer + { + Id = 18, + Name = "2. Large decimal with high precision", + UntypedProperty = 123456789123456789.12345678935m, + UntypedList = new List { "123456789123456789.12345678935", 123456789123456789.12345678935m, "2. Large decimal with high precision" }, + Orders = new List { new Order { Id = 20, Amount = 100000m } } + }, + new Customer + { + Id = 19, + Name = "3. Large decimal with high precision", + UntypedProperty = 1234567891234567891234.12345678935546576m, + UntypedList = new List { "1234567891234567891234.12345678935546576", "3. Large decimal with high precision" }, + Orders = new List { new Order { Id = 21, Amount = 1234567891234567891234.12m } } + }, + new Customer + { + Id = 20, + Name = "Positive exponent", + UntypedProperty = 1.234e+5, + UntypedList = new List { "1.234e+5", 1.234e+5, "Positive exponent" }, + Orders = new List { new Order { Id = 22, Amount = 123400 } } + }, + new Customer + { + Id = 21, + Name = "Negative exponent", + UntypedProperty = 1.234e-5, + UntypedList = new List { "1.234e-5", 1.234e-5, "Negative exponent" }, + Orders = new List { new Order { Id = 23, Amount = 0.00001234m } } + }, + new Customer + { + Id = 22, + Name = "Avogadro's number", + UntypedProperty = 6.02214076e+23, + UntypedList = new List { "6.02214076e+23", 6.02214076e+23, "Avogadro's number" }, + Orders = new List { new Order { Id = 24, Amount = 602214076000000000000000m } } + }, + new Customer + { + Id = 23, + Name = "Electron mass", + UntypedProperty = 9.1093837e-31, + UntypedList = new List { "9.1093837e-31", 9.1093837e-31, "Electron mass" }, + Orders = new List { new Order { Id = 25, Amount = 0.0m } } + }, + new Customer + { + Id = 24, + Name = "Zero", + UntypedProperty = 0.0, + UntypedList = new List { "0.0", 0.0, "Zero" }, + Orders = new List { new Order { Id = 26, Amount = 0 } } + }, + new Customer + { + Id = 25, + Name = "Negative zero", + UntypedProperty = -0.0, + UntypedList = new List { "-0.0", -0.0, "Negative zero" }, + Orders = new List { new Order { Id = 27, Amount = 0 } } + }, + new Customer + { + Id = 26, + Name = "Max decimal", + UntypedProperty = 79228162514264337593543950335m, + UntypedList = new List { "79228162514264337593543950335", 79228162514264337593543950335m, "Max decimal" }, + Orders = new List { new Order { Id = 28, Amount = 79228162514264337593543950335m } } + }, + new Customer + { + Id = 27, + Name = "Max double", + UntypedProperty = 1.7976931348623157E+308, + UntypedList = new List { "1.7976931348623157E+308", 1.7976931348623157E+308, "Max double" }, + Orders = new List { new Order { Id = 29, Amount = 0 } } + }, + new Customer + { + Id = 28, + Name = "Max single", + UntypedProperty = 3.40282347E+38, + UntypedList = new List { "3.40282347E+38", 3.40282347E+38, "Max single" }, + Orders = new List { new Order { Id = 30, Amount = 0 } } + } + ]; + + this.Orders = this.Customers.SelectMany(c => c.Orders).Distinct().ToList(); + } + + private void ResetDataSource() + { + this.Customers = new List(); + this.Orders = new List(); + } +} diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesEdmModel.cs b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesEdmModel.cs new file mode 100644 index 0000000000..5f667738e1 --- /dev/null +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/UntypedTypes/UntypedTypesEdmModel.cs @@ -0,0 +1,66 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System.Reflection; +using System.Text; +using System.Xml; +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; +using Microsoft.OData.Edm.Validation; +using Xunit; + +namespace Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes; + +public static class UntypedTypesEdmModel +{ + private static readonly Uri _baseUri = new("http://localhost/odata/"); + + public static IEdmModel GetEdmModel() + { + var model = ReadModel("UntypedTypesODataServiceCsdl.xml"); + model.Validate(out var errors); + if (errors != null && errors.Any()) + { + throw new InvalidOperationException("Failed to load model : " + string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage))); + } + + return model; + } + + private static IEdmModel ReadModel(string fileName) + { + IEdmModel model; + using (Stream csdlStream = ReadResourceFromAssembly(fileName)) + { + bool parseResult = CsdlReader.TryParse(XmlReader.Create(csdlStream), out model, out IEnumerable errors); + + if (!parseResult) + { + throw new InvalidOperationException("Failed to load model : " + string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage))); + } + } + + return model; + } + + private static Stream ReadResourceFromAssembly(string resourceName) + { + var assembly = Assembly.GetExecutingAssembly(); + var resourcePath = Enumerable.First(Enumerable.OrderBy(Enumerable.Where(assembly.GetManifestResourceNames(), name => name.EndsWith(resourceName)), filteredName => filteredName.Length)); + var resourceStream = assembly.GetManifestResourceStream(resourcePath); + + Assert.NotNull(resourceStream); + + var reader = new StreamReader(resourceStream); + string str = reader.ReadToEnd(); + str = str.Replace(" + @@ -54,6 +55,9 @@ Always + + Always + diff --git a/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/UntypedTypesTests/UntypedTypesTests.cs b/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/UntypedTypesTests/UntypedTypesTests.cs new file mode 100644 index 0000000000..fd09d3e1da --- /dev/null +++ b/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/UntypedTypesTests/UntypedTypesTests.cs @@ -0,0 +1,508 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.E2E.TestCommon; +using Microsoft.OData.E2E.TestCommon.Common; +using Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes; +using Microsoft.OData.Edm; + +namespace Microsoft.OData.Client.E2E.Tests.UntypedTypesTests; + +public class UntypedTypesTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(UntypedTypesController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("odata", UntypedTypesEdmModel.GetEdmModel())); + } + } + + public UntypedTypesTests(TestWebApplicationFactory fixture) + : base(fixture) + { + _baseUri = new Uri(Client.BaseAddress, "odata/"); + _model = UntypedTypesEdmModel.GetEdmModel(); + + ResetDefaultDataSource(); + } + + [Fact] + public async Task ShouldQueryEntitiesWithUntypedProperties() + { + // Arrange + var requestUrl = new Uri(_baseUri.AbsoluteUri + "Customers", UriKind.Absolute); + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + // Act + var responseMessage = await requestMessage.GetResponseAsync(); + var responseContent = await ReadAsStringAsync(responseMessage); + + // Assert + Assert.Equal(200, responseMessage.StatusCode); + Assert.Equal( + """ + { + "@odata.context": "http://localhost/odata/$metadata#Customers", + "value": [ + { + "Id": 1, + "Name": "Customer 1", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 3.40282347E+38, + "UntypedList": [ + "String in list", + 123, + true, + 45.67 + ] + }, + { + "Id": 2, + "Name": "Customer 2", + "UntypedProperty@odata.type": "#Int32", + "UntypedProperty": -456, + "UntypedList": [ + "Another string", + 789, + false, + 12.34 + ] + }, + { + "Id": 3, + "Name": "Customer 3", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 6.02214076E+23, + "UntypedList": [ + "Yet another string", + 101112, + true, + 56.78 + ] + }, + { + "Id": 4, + "Name": "Customer 4", + "UntypedProperty": null, + "UntypedList": [] + }, + { + "Id": 5, + "Name": "Customer 5", + "UntypedProperty@odata.type": "#Boolean", + "UntypedProperty": true, + "UntypedList": [ + "Final string", + 131415, + false, + 90.12 + ] + }, + { + "Id": 6, + "Name": "Customer 6", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 78.9, + "UntypedList": [ + "Extra string", + 161718, + true, + 34.56 + ] + }, + { + "Id": 7, + "Name": "Customer 7", + "UntypedProperty": [ + "List as untyped property", + 192021, + false, + 78.9 + ], + "UntypedList": [ + "More strings", + 222324, + true, + 12.34 + ] + }, + { + "Id": 8, + "Name": "Small integer", + "UntypedProperty@odata.type": "#Int32", + "UntypedProperty": 123, + "UntypedList": [ + "123", + 123, + "Small integer" + ] + }, + { + "Id": 9, + "Name": "Negative integer", + "UntypedProperty@odata.type": "#Int32", + "UntypedProperty": -42, + "UntypedList": [ + "-42", + -42, + "Negative integer" + ] + }, + { + "Id": 10, + "Name": "Max int32", + "UntypedProperty@odata.type": "#Int32", + "UntypedProperty": 2147483647, + "UntypedList": [ + "2147483647", + 2147483647, + "Max int32" + ] + }, + { + "Id": 11, + "Name": "Beyond int32", + "UntypedProperty@odata.type": "#Int64", + "UntypedProperty": 2147483648, + "UntypedList": [ + "2147483648", + 2147483648, + "Beyond int32" + ] + }, + { + "Id": 12, + "Name": "Max int64", + "UntypedProperty@odata.type": "#Int64", + "UntypedProperty": 9223372036854775807, + "UntypedList": [ + "9223372036854775807", + 9223372036854775807, + "Max int64" + ] + }, + { + "Id": 13, + "Name": "Beyond int64", + "UntypedProperty@odata.type": "#Decimal", + "UntypedProperty": 9223372036854775808, + "UntypedList": [ + "9223372036854775808", + "Beyond int64" + ] + }, + { + "Id": 14, + "Name": "Simple decimal", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 123.456, + "UntypedList": [ + "123.456", + 123.456, + "Simple decimal" + ] + }, + { + "Id": 15, + "Name": "Very small decimal", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 1E-07, + "UntypedList": [ + "0.0000001", + 0.0000001, + "Very small decimal" + ] + }, + { + "Id": 16, + "Name": "High precision (pi)", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 3.141592653589793, + "UntypedList": [ + "3.14159265358979323846", + 3.141592653589793, + "High precision (pi)" + ] + }, + { + "Id": 17, + "Name": "1. Large decimal with high precision", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 123456789012345.12, + "UntypedList": [ + "123456789012345.123456789012345", + 123456789012345.12, + "1. Large decimal with high precision" + ] + }, + { + "Id": 18, + "Name": "2. Large decimal with high precision", + "UntypedProperty@odata.type": "#Decimal", + "UntypedProperty": 123456789123456789.12345678935, + "UntypedList": [ + "123456789123456789.12345678935", + 123456789123456789.12345678935, + "2. Large decimal with high precision" + ] + }, + { + "Id": 19, + "Name": "3. Large decimal with high precision", + "UntypedProperty@odata.type": "#Decimal", + "UntypedProperty": 1234567891234567891234.1234568, + "UntypedList": [ + "1234567891234567891234.12345678935546576", + "3. Large decimal with high precision" + ] + }, + { + "Id": 20, + "Name": "Positive exponent", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 123400.0, + "UntypedList": [ + "1.234e+5", + 123400.0, + "Positive exponent" + ] + }, + { + "Id": 21, + "Name": "Negative exponent", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 1.234E-05, + "UntypedList": [ + "1.234e-5", + 1.234E-05, + "Negative exponent" + ] + }, + { + "Id": 22, + "Name": "Avogadro's number", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 6.02214076E+23, + "UntypedList": [ + "6.02214076e+23", + 6.02214076E+23, + "Avogadro's number" + ] + }, + { + "Id": 23, + "Name": "Electron mass", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 9.1093837E-31, + "UntypedList": [ + "9.1093837e-31", + 9.1093837E-31, + "Electron mass" + ] + }, + { + "Id": 24, + "Name": "Zero", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 0.0, + "UntypedList": [ + "0.0", + 0.0, + "Zero" + ] + }, + { + "Id": 25, + "Name": "Negative zero", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": -0.0, + "UntypedList": [ + "-0.0", + -0.0, + "Negative zero" + ] + }, + { + "Id": 26, + "Name": "Max decimal", + "UntypedProperty@odata.type": "#Decimal", + "UntypedProperty": 79228162514264337593543950335, + "UntypedList": [ + "79228162514264337593543950335", + 79228162514264337593543950335, + "Max decimal" + ] + }, + { + "Id": 27, + "Name": "Max double", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 1.7976931348623157E+308, + "UntypedList": [ + "1.7976931348623157E+308", + 1.7976931348623157E+308, + "Max double" + ] + }, + { + "Id": 28, + "Name": "Max single", + "UntypedProperty@odata.type": "#Double", + "UntypedProperty": 3.40282347E+38, + "UntypedList": [ + "3.40282347E+38", + 3.40282347E+38, + "Max single" + ] + } + ] + } + """, responseContent); + } + + [Fact] + public async Task ShouldQuerySingleEntityWithUntypedProperties() + { + // Arrange + var requestUrl = new Uri(_baseUri.AbsoluteUri + "Customers(26)", UriKind.Absolute); + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + // Act + var responseMessage = await requestMessage.GetResponseAsync(); + var responseContent = await ReadAsStringAsync(responseMessage); + + // Assert + Assert.Equal(200, responseMessage.StatusCode); + Assert.Equal( + """ + { + "@odata.context": "http://localhost/odata/$metadata#Customers/$entity", + "Id": 26, + "Name": "Max decimal", + "UntypedProperty@odata.type": "#Decimal", + "UntypedProperty": 79228162514264337593543950335, + "UntypedList": [ + "79228162514264337593543950335", + 79228162514264337593543950335, + "Max decimal" + ] + } + """, responseContent); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task QueryEntryWithUntypedTypes_NumericValueTypeIsDouble_WhereReadUntypedNumericAsDecimalFlagIsSetOrNot(bool readUntypedNumericAsDecimalFlagIsSetOrDefault) + { + // Arrange + var readerSettings = new ODataMessageReaderSettings() + { + BaseUri = _baseUri, + EnablePrimitiveTypeConversion = true, + EnableMessageStreamDisposal = false, + ShouldIncludeAnnotation = (annotationName) => true, + Validations = ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType + }; + + if (!readUntypedNumericAsDecimalFlagIsSetOrDefault) + { + readerSettings.LibraryCompatibility = ~ODataLibraryCompatibility.ReadUntypedNumericAsDecimal; // Make sure ReadUntypedNumericAsDecimal is not set + } + + var requestUrl = new Uri(_baseUri.AbsoluteUri + "Customers(17)", UriKind.Absolute); + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + // Act + var queryResponseMessage = await requestMessage.GetResponseAsync(); + + // Assert + Assert.Equal(200, queryResponseMessage.StatusCode); + + ODataResource? entry = null; + using (var messageReader = new ODataMessageReader(queryResponseMessage, readerSettings, _model)) + { + var reader = await messageReader.CreateODataResourceReaderAsync(); + while (await reader.ReadAsync()) + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = reader.Item as ODataResource; + } + } + + Assert.Equal(ODataReaderState.Completed, reader.State); + } + + Assert.NotNull(entry); + var value = Assert.IsType(entry.Properties.First(p => p.Name == "UntypedProperty")).Value; + Assert.IsType(value); + Assert.Equal(123456789012345.12, value); + } + + #region Private Members + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "untypedtypes/Default.ResetDefaultDataSource", UriKind.Absolute); + var requestMessage = new TestHttpClientRequestMessage(actionUri, Client); + requestMessage.Method = "POST"; + + var responseMessage = requestMessage.GetResponseAsync().Result; + + Assert.Equal(200, responseMessage.StatusCode); + } + + private static async Task ReadAsStringAsync(IODataResponseMessageAsync responseMessage) + { + using (Stream stream = await responseMessage.GetStreamAsync()) + { + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + var content = await reader.ReadToEndAsync(); + + // Format the content in JSON format + var jsonElement = JsonSerializer.Deserialize(content); + return JsonSerializer.Serialize(jsonElement, + new JsonSerializerOptions + { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + } + } + } + + #endregion +} + +public enum DaysOfWeekEnum +{ + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6 +} diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedDeserializerUndeclaredTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedDeserializerUndeclaredTests.cs index 7b1645438f..60734aeb1d 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedDeserializerUndeclaredTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedDeserializerUndeclaredTests.cs @@ -150,6 +150,20 @@ private void ReadCollectionPayload(string payload, Action action) } } + private void ReadCollectionPayload(string payload, ODataMessageReaderSettings readerSettings, Action action) + { + var message = new InMemoryMessage() { Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)) }; + message.SetHeader("Content-Type", "application/json"); + using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, this.serverModel)) + { + var reader = msgReader.CreateODataResourceSetReader(); + while (reader.Read()) + { + action(reader); + } + } + } + private void ReadResourcePayload(string payload, Action action) { var message = new InMemoryMessage() { Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)) }; @@ -164,6 +178,20 @@ private void ReadResourcePayload(string payload, Action action) } } + private void ReadResourcePayload(string payload, ODataMessageReaderSettings readerSettings, Action action) + { + var message = new InMemoryMessage() { Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)) }; + message.SetHeader("Content-Type", "application/json"); + using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, this.serverModel)) + { + var reader = msgReader.CreateODataResourceReader(); + while (reader.Read()) + { + action(reader); + } + } + } + #region non-open entity's property unknown name + known value type [Fact] @@ -1516,11 +1544,264 @@ public void ReadOpenEntryUndeclaredEmptyCollectionPropertiesWithoutODataTypeAsVa [Theory] [InlineData("Edm.Untyped")] [InlineData("Server.NS.UndefinedType")] - public void ReadUntypedResource(string fragment) + public void ReadUntypedResource_asInt32_WhenReadUntypedNumericAsDecimalFlagIsNotSet(string fragment) { string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#" + fragment + "\",\"id\":1}"; + + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(1, value); + } + + [Theory] + [InlineData(0, 0)] + [InlineData("0", 0)] + [InlineData(0.0, 0)] + [InlineData(123, 123)] + [InlineData("123", 123)] + [InlineData(-42, -42)] + [InlineData("-42", -42)] + [InlineData(2147483647, 2147483647)] // Int32.MaxValue + [InlineData(-2147483648, -2147483648)] // Int32.MinValue + [InlineData(1.234e+5d, 123400)] + [InlineData("000123", 123)] // Leading zeros + [InlineData("2147483647", 2147483647)] + [InlineData("-2147483648", -2147483648)] + public void ReadUntypedResourceForInt32_ExpectedTypeAsInt32_WhenReadUntypedNumericAsDecimalFlagIsNotSet(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(0, 0)] + [InlineData("0", 0)] + [InlineData(0.0, 0)] + [InlineData(123, 123)] + [InlineData("123", 123)] + [InlineData(-42, -42)] + [InlineData("-42", -42)] + [InlineData(2147483647, 2147483647)] // Int32.MaxValue + [InlineData(-2147483648, -2147483648)] // Int32.MinValue + [InlineData(1.234e+5d, 123400)] + [InlineData("000123", 123)] // Leading zeros + [InlineData("2147483647", 2147483647)] + [InlineData("-2147483648", -2147483648)] + public void ReadUntypedResourceForInt32_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSet(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + + var readerSettings = UntypedAsValueReaderSettings.Clone(); + readerSettings.LibraryCompatibility |= ODataLibraryCompatibility.ReadUntypedNumericAsDecimal; + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(Convert.ToDecimal(expectedValue), value); + } + + [Theory] + [InlineData(2147483648, 2147483648L)] + [InlineData(9223372036854775807, 9223372036854775807L)] + [InlineData(-9223372036854775808, -9223372036854775808L)] + [InlineData("9223372036854775807", 9223372036854775807L)] + [InlineData("-9223372036854775808", -9223372036854775808L)] + [InlineData(1002147483646, 1002147483646L)] + [InlineData("1002147483646", 1002147483646L)] + [InlineData("0009223372036854775807", 9223372036854775807L)] // Leading zeros + [InlineData(1e10, 10000000000L)] // Large double that fits in long + public void ReadUntypedResourceForInt64_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSetOrDefault(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(Convert.ToDecimal(expectedValue), value); + } + + public static TheoryData UntypedWithDecimalData => new() + { + { 9223372036854775808, 9223372036854775808M }, // Just above Int64.MaxValue + { 6.02214076e+23M, 602214076000000000000000M }, + { 3.14159265358979323846M, 3.14159265358979323846M }, // PI + { "3.14159265358979323846", 3.14159265358979323846M }, + { 123456789012345.123456789012345M, 123456789012345.123456789012345M }, + { "123456789012345.123456789012345", 123456789012345.123456789012345M }, + { 79228162514264337593543950335M, 79228162514264337593543950335M }, // Decimal.MaxValue + { "-79228162514264337593543950335", -79228162514264337593543950335M }, // Decimal.MinValue + { 0.0000000000000000000000000001M, 0.0000000000000000000000000001M }, // Smallest positive + { "-0.0000000000000000000000000001", -0.0000000000000000000000000001M }, + { 9999999936869775949, 9999999936869775949M }, + { "9999999936869775949", 9999999936869775949M }, + { .14159265358979323846M, .14159265358979323846M }, + { ".14159265358979323846", .14159265358979323846M }, + { .14159265358979, .14159265358979M }, + { ".14159265358979", .14159265358979M }, + { "-0.0", 0M }, + {"0.0", 0M }, + { 123.456, 123.456M }, + { "123.456", 123.456M }, + { "0.0000001", 0.0000001M } + }; + + [Theory] + [MemberData(nameof(UntypedWithDecimalData))] + public void ReadUntypedResourceForDecimal_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsNotSet(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + } + + [Theory] + [MemberData(nameof(UntypedWithDecimalData))] + public void ReadUntypedResourceForDecimal_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSetOrDefault(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData("1.234e+5", 123400d)] + [InlineData(1.234e-5d, 0.00001234)] + [InlineData("1.234e-5", 0.00001234)] + [InlineData(9.1093837e-31, 9.1093837e-31)] + [InlineData("9.1093837e-31", 9.1093837e-31)] + [InlineData(1.7976931348623157E+308, 1.7976931348623157E+308d)] + [InlineData("-1.7976931348623157E+308", -1.7976931348623157E+308d)] + [InlineData(2.2250738585072014E-308, 2.2250738585072014E-308d)] + [InlineData("-2.2250738585072014E-308", -2.2250738585072014E-308d)] + [InlineData("1e10", 10000000000d)] + [InlineData(double.MinValue, double.MinValue)] + [InlineData(0.0000001, 0.0000001d)] + public void ReadUntypedResourceForDouble_ExpectedTypeAsDouble_WhenReadUntypedNumericAsDecimalFlagIsNotSet(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataResource entry = null; + this.ReadResourcePayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceStart) + { + entry = (reader.Item as ODataResource); + } + }); + + Assert.Single(entry.Properties); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData("1.234e+5", 123400d)] + [InlineData(1.234e-5d, 0.00001234)] + [InlineData("1.234e-5", 0.00001234)] + [InlineData(9.1093837e-31, 9.1093837e-31)] + [InlineData("9.1093837e-31", 9.1093837e-31)] + [InlineData(123.456, 123.456)] + [InlineData("123.456", 123.456)] + [InlineData("0.0000001", 0.0000001)] + [InlineData(0.0000001, 0.0000001)] + [InlineData("0.0", 0d)] + [InlineData("-0.0", -0d)] + [InlineData(.14159265358979, .14159265358979)] + [InlineData(".14159265358979", .14159265358979)] + [InlineData(2.2250738585072014E-308, 2.2250738585072014E-308d)] + [InlineData("-2.2250738585072014E-308", -2.2250738585072014E-308d)] + [InlineData("1e10", 10000000000d)] + public void ReadUntypedResourceForDouble_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSet(object number, object expectedValue) + { + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Edm.Untyped\",\"id\":" + number + "}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + readerSettings.LibraryCompatibility |= ODataLibraryCompatibility.ReadUntypedNumericAsDecimal; + ODataResource entry = null; - this.ReadResourcePayload(payload, reader => + this.ReadResourcePayload(payload, readerSettings, reader => { if (reader.State == ODataReaderState.ResourceStart) { @@ -1529,7 +1810,10 @@ public void ReadUntypedResource(string fragment) }); Assert.Single(entry.Properties); - Assert.Equal(1m, Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value); + + var value = Assert.IsType(entry.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(Convert.ToDecimal(expectedValue), value); } #endregion @@ -1600,32 +1884,50 @@ public void ReadUntypedCollectionContainingPrimitive(string fragment) [InlineData("Edm.Untyped")] [InlineData("Collection(Edm.Untyped)")] [InlineData("Collection(Server.NS.UndefinedType)")] - public void ReadUntypedCollectionContainingResource(string fragment) + public void ReadUntypedCollectionContainingNullAndNumbers(string fragment) { - string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#" + fragment + "\",\"value\":[{\"id\":1}]}"; - ODataResource entry = null; - this.ReadCollectionPayload(payload, reader => + string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#" + + fragment + + "\",\"value\":[42, null, 83748348494435, 2147483647, \"some string\", -9223372036854775808, 214748364745, 3.134545454565656, { \"Anyname\": \"Redmond\", \"Justnull\": null, \"Intnum\": 2345454656, \"longnum\": -9223372036854775808 }]}"; + + var readerSettings = UntypedAsValueReaderSettings.Clone(); + readerSettings.LibraryCompatibility = ~ODataLibraryCompatibility.ReadUntypedNumericAsDecimal; // Disable the behavior to read untyped numeric as decimal + + List entries = new List(); + this.ReadCollectionPayload(payload, readerSettings, reader => { if (reader.State == ODataReaderState.ResourceStart) { - entry = (reader.Item as ODataResource); + entries.Add(reader.Item); + } + else if (reader.State == ODataReaderState.Primitive) + { + entries.Add(((ODataPrimitiveValue)reader.Item).Value); } }); - Assert.Single(entry.Properties); - Assert.Equal(1m, Assert.IsType(entry.Properties.First(p=>p.Name == "id")).Value); + Assert.NotEmpty(entries); + + Assert.Equal(42, Assert.IsType(entries[0])); + Assert.Null(entries[1]); + //Assert.Equal(83748348494435L, Assert.IsType(entries[2])); + Assert.Equal(2147483647, Assert.IsType(entries[3])); + Assert.Equal("some string", Assert.IsType(entries[4])); + Assert.Equal(-9223372036854775808M, Assert.IsType(entries[5])); } [Theory] [InlineData("Edm.Untyped")] [InlineData("Collection(Edm.Untyped)")] - public void ReadUntypedCollectionContainingCollection(string fragment) + public void ReadUntypedCollectionContainingCollection_WhenReadUntypedNumericAsDecimalFlagIsNotSet(string fragment) { string payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#" + fragment +"\",\"value\":[[\"primitiveString\",{\"id\":1}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + ODataPrimitiveValue primitiveMember = null; ODataResource resourceMember = null; int level = 0; - this.ReadCollectionPayload(payload, reader => + this.ReadCollectionPayload(payload, readerSettings, reader => { if (reader.State == ODataReaderState.ResourceSetStart) { @@ -1642,7 +1944,294 @@ public void ReadUntypedCollectionContainingCollection(string fragment) }); Assert.Single(resourceMember.Properties); - Assert.Equal(1m, Assert.IsType(resourceMember.Properties.First(p => p.Name == "id")).Value); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "id")).Value; + Assert.IsType(value); + Assert.Equal(1, value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [InlineData(0, 0)] + [InlineData("0", 0)] + [InlineData(0.0, 0)] + [InlineData(123, 123)] + [InlineData("123", 123)] + [InlineData(-42, -42)] + [InlineData("-42", -42)] + [InlineData(2147483647, 2147483647)] // Int32.MaxValue + [InlineData(-2147483648, -2147483648)] // Int32.MinValue + [InlineData(1.234e+5d, 123400)] + [InlineData("000123", 123)] // Leading zeros + [InlineData("2147483647", 2147483647)] + [InlineData("-2147483648", -2147483648)] + public void ReadUntypedCollectionForInt32_ExpectedTypeAsInt32_WhenReadUntypedNumericAsDecimalFlagIsNotSet(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [InlineData(0, 0)] + [InlineData("0", 0)] + [InlineData(0.0, 0)] + [InlineData(123, 123)] + [InlineData("123", 123)] + [InlineData(-42, -42)] + [InlineData("-42", -42)] + [InlineData(2147483647, 2147483647)] // Int32.MaxValue + [InlineData(-2147483648, -2147483648)] // Int32.MinValue + [InlineData(1.234e+5d, 123400)] + [InlineData("000123", 123)] // Leading zeros + [InlineData("2147483647", 2147483647)] + [InlineData("-2147483648", -2147483648)] + public void ReadUntypedCollectionForInt32_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSet(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + readerSettings.LibraryCompatibility |= ODataLibraryCompatibility.ReadUntypedNumericAsDecimal; + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(Convert.ToDecimal(expectedValue), value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [InlineData(2147483648, 2147483648L)] + [InlineData(9223372036854775807, 9223372036854775807L)] + [InlineData(-9223372036854775808, -9223372036854775808L)] + [InlineData("9223372036854775807", 9223372036854775807L)] + [InlineData("-9223372036854775808", -9223372036854775808L)] + [InlineData(1002147483646, 1002147483646L)] + [InlineData("1002147483646", 1002147483646L)] + [InlineData("0009223372036854775807", 9223372036854775807L)] // Leading zeros + [InlineData(1e10, 10000000000L)] // Large double that fits in long + public void ReadUntypedCollectionForInt64_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSetOrDefault(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(Convert.ToDecimal(expectedValue), value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [MemberData(nameof(UntypedWithDecimalData))] + public void ReadUntypedCollectionForDecimal_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsNotSet(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [MemberData(nameof(UntypedWithDecimalData))] + public void ReadUntypedCollectionForDecimal_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSetOrDefault(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [InlineData("1.234e+5", 123400d)] + [InlineData(1.234e-5d, 0.00001234)] + [InlineData("1.234e-5", 0.00001234)] + [InlineData(9.1093837e-31, 9.1093837e-31)] + [InlineData("9.1093837e-31", 9.1093837e-31)] + [InlineData(0.0000001, 0.0000001)] + [InlineData(1.7976931348623157E+308, 1.7976931348623157E+308d)] + [InlineData("-1.7976931348623157E+308", -1.7976931348623157E+308d)] + [InlineData(2.2250738585072014E-308, 2.2250738585072014E-308d)] + [InlineData("-2.2250738585072014E-308", -2.2250738585072014E-308d)] + [InlineData("1e10", 10000000000d)] + [InlineData(double.MinValue, double.MinValue)] + public void ReadUntypedCollectionForDouble_ExpectedTypeAsDouble_WhenReadUntypedNumericAsDecimalFlagIsNotSet(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(expectedValue, value); + Assert.Equal("primitiveString", primitiveMember.Value); + } + + [Theory] + [InlineData("1.234e+5", 123400d)] + [InlineData(1.234e-5d, 0.00001234)] + [InlineData("1.234e-5", 0.00001234)] + [InlineData(9.1093837e-31, 9.1093837e-31)] + [InlineData("9.1093837e-31", 9.1093837e-31)] + [InlineData(123.456, 123.456)] + [InlineData("123.456", 123.456)] + [InlineData("0.0000001", 0.0000001)] + [InlineData(0.0000001, 0.0000001)] + [InlineData("0.0", 0d)] + [InlineData("-0.0", -0d)] + [InlineData(.14159265358979, .14159265358979)] + [InlineData(".14159265358979", .14159265358979)] + [InlineData(2.2250738585072014E-308, 2.2250738585072014E-308d)] + [InlineData("-2.2250738585072014E-308", -2.2250738585072014E-308d)] + [InlineData("1e10", 10000000000d)] + public void ReadUntypedCollectionForDouble_ExpectedTypeAsDecimal_WhenReadUntypedNumericAsDecimalFlagIsSet(object number, object expectedValue) + { + var payload = "{\"@odata.context\":\"http://www.sampletest.com/$metadata#Collection(Edm.Untyped)\",\"value\":[[\"primitiveString\",{\"id\":1, \"num\":" + number + "}]]}"; + var readerSettings = UntypedAsValueReaderSettings.Clone(); + readerSettings.LibraryCompatibility |= ODataLibraryCompatibility.ReadUntypedNumericAsDecimal; + + ODataPrimitiveValue primitiveMember = null; + ODataResource resourceMember = null; + int level = 0; + this.ReadCollectionPayload(payload, readerSettings, reader => + { + if (reader.State == ODataReaderState.ResourceSetStart) + { + level++; + } + else if (reader.State == ODataReaderState.ResourceStart && level == 2) + { + resourceMember = (reader.Item as ODataResource); + } + else if (reader.State == ODataReaderState.Primitive && level == 2) + { + primitiveMember = (reader.Item as ODataPrimitiveValue); + } + }); + + var value = Assert.IsType(resourceMember.Properties.First(p => p.Name == "num")).Value; + Assert.IsType(value); + Assert.Equal(Convert.ToDecimal(expectedValue), value); Assert.Equal("primitiveString", primitiveMember.Value); } diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/PrimitiveValuesRoundtripJsonTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/PrimitiveValuesRoundtripJsonTests.cs index e9959c815e..8a3fe98114 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/PrimitiveValuesRoundtripJsonTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/PrimitiveValuesRoundtripJsonTests.cs @@ -147,6 +147,48 @@ public void DecimalRoundtripJsonTest() this.VerifyPrimitiveValuesDoNotRoundtripWithoutTypeInformation(values); } + public static TheoryData DecimalValuesForRoundtripJsonTest() + { + return new TheoryData + { + { 0, ODataVersion.V4 }, + { 0, ODataVersion.V401 }, + { 1, ODataVersion.V4 }, + { 1, ODataVersion.V401 }, + { -1, ODataVersion.V4 }, + { -1, ODataVersion.V401 }, + { Decimal.MinValue, ODataVersion.V4 }, + { Decimal.MinValue, ODataVersion.V401 }, + { Decimal.MaxValue, ODataVersion.V4 }, + { Decimal.MaxValue, ODataVersion.V401 }, + { 10^28, ODataVersion.V4 }, + { 10^28, ODataVersion.V401 }, + { 10^-28, ODataVersion.V4 }, + { 10^-28, ODataVersion.V401 }, + }; + } + + [Theory] + [MemberData(nameof(DecimalValuesForRoundtripJsonTest))] + public void DecimalRoundtripJsonTest_With_Ieee754CompatibleTrue(decimal clrValue, ODataVersion version) + { + // Arrange & Act + var typeReference = new EdmPrimitiveTypeReference((IEdmPrimitiveType)this.model.FindType("Edm.Decimal"), true); + + // roundtrip with type reference + var actualValuewithTypeRef = this.WriteThenReadValue(clrValue, typeReference, version, isIeee754Compatible: true); + + // roundtrip without type reference + var actualValueWithoutTypeRef = this.WriteThenReadValue(clrValue, null, version, isIeee754Compatible: true); + + // Assert + Assert.Equal(clrValue.GetType(), actualValuewithTypeRef.GetType()); + Assert.Equal(actualValuewithTypeRef, clrValue); + + Assert.Equal(typeof(string), actualValueWithoutTypeRef.GetType()); + Assert.Equal(Convert.ToDecimal(actualValueWithoutTypeRef), clrValue); + } + [Fact] public void DecimalRoundTripJsonTestWithIeee754CompatibleFalse() { @@ -167,6 +209,44 @@ public void DecimalRoundTripJsonTestWithIeee754CompatibleFalse() this.VerifyPrimitiveValuesDoNotRoundtripWithoutTypeInformationIeee754CompatibleFalse(new[] { Decimal.MaxValue, Decimal.MinValue }); } + public static TheoryData DecimalValuesForRoundtripJsonTestWithIeee754CompatibleFalse() + { + return new TheoryData + { + { 0, ODataVersion.V4, typeof(int) }, + { 0, ODataVersion.V401, typeof(int) }, + { 1, ODataVersion.V4, typeof(int) }, + { 1, ODataVersion.V401, typeof(int) }, + { -1, ODataVersion.V4, typeof(int) }, + { -1, ODataVersion.V401, typeof(int) }, + { Decimal.MinValue, ODataVersion.V4, typeof(double) }, + { Decimal.MinValue, ODataVersion.V401, typeof(double) }, + { Decimal.MaxValue, ODataVersion.V4, typeof(double) }, + { Decimal.MaxValue, ODataVersion.V401, typeof(double) } + }; + } + + [Theory] + [MemberData(nameof(DecimalValuesForRoundtripJsonTestWithIeee754CompatibleFalse))] + public void DecimalRoundtripJsonTest_With_Ieee754CompatibleFalse(decimal clrValue, ODataVersion version, Type expectedTypeOfValueWithoutRef) + { + // Arrange & Act + var typeReference = new EdmPrimitiveTypeReference((IEdmPrimitiveType)this.model.FindType("Edm.Decimal"), true); + + // roundtrip with type reference + var actualValuewithTypeRef = this.WriteThenReadValue(clrValue, typeReference, version, isIeee754Compatible: false); + + // roundtrip without type reference + var actualValueWithoutTypeRef = this.WriteThenReadValue(clrValue, null, version, isIeee754Compatible: false); + + // Assert + Assert.Equal(clrValue.GetType(), actualValuewithTypeRef.GetType()); + Assert.Equal(actualValuewithTypeRef, clrValue); + + Assert.Equal(expectedTypeOfValueWithoutRef, actualValueWithoutTypeRef.GetType()); + Assert.Equal(Convert.ToDouble(actualValueWithoutTypeRef), (double)clrValue); + } + [Fact] public void DoubleRoundtripJsonTest() { @@ -193,6 +273,64 @@ public void DoubleRoundtripJsonTest() this.VerifyPrimitiveValuesDoNotRoundtripWithoutTypeInformation(valuesWrittenAsString); } + [Theory] + [InlineData(0, ODataVersion.V4)] + [InlineData(0, ODataVersion.V401)] + [InlineData(42, ODataVersion.V4)] + [InlineData(42, ODataVersion.V401)] + [InlineData(42.42, ODataVersion.V4)] + [InlineData(42.42, ODataVersion.V401)] + [InlineData(Double.MaxValue, ODataVersion.V4)] + [InlineData(Double.MinValue, ODataVersion.V401)] + [InlineData(-4.42330604244772E-305, ODataVersion.V4)] + [InlineData(-4.42330604244772E-305, ODataVersion.V401)] + [InlineData(42E20, ODataVersion.V4)] + [InlineData(42E20, ODataVersion.V401)] + public void DoubleRoundtripJsonTest_With_DigitValues(double clrValue, ODataVersion version) + { + // Arrange & Act + var typeReference = new EdmPrimitiveTypeReference((IEdmPrimitiveType)this.model.FindType("Edm.Double"), true); + + // roundtrip with type reference + var actualValuewithTypeRef = this.WriteThenReadValue(clrValue, typeReference, version, isIeee754Compatible: true); + + // roundtrip without type reference + var actualValueWithoutTypeRef = this.WriteThenReadValue(clrValue, null, version, isIeee754Compatible: true); + + // Assert + Assert.Equal(clrValue.GetType(), actualValuewithTypeRef.GetType()); + Assert.Equal(actualValuewithTypeRef, clrValue); + + Assert.Equal(clrValue.GetType(), actualValueWithoutTypeRef.GetType()); + Assert.Equal(actualValueWithoutTypeRef, clrValue); + } + + [Theory] + [InlineData(Double.PositiveInfinity, ODataVersion.V4, "INF")] + [InlineData(Double.PositiveInfinity, ODataVersion.V401, "INF")] + [InlineData(Double.NegativeInfinity, ODataVersion.V4, "-INF")] + [InlineData(Double.NegativeInfinity, ODataVersion.V401, "-INF")] + [InlineData(Double.NaN, ODataVersion.V4, "NaN")] + [InlineData(Double.NaN, ODataVersion.V401, "NaN")] + public void DoubleRoundtripJsonTest_With_StringValues(double clrValue, ODataVersion version, string expectedValueForNullTypeReference) + { + // Arrange & Act + var typeReference = new EdmPrimitiveTypeReference((IEdmPrimitiveType)this.model.FindType("Edm.Double"), true); + + // roundtrip with type reference + var actualValuewithTypeRef = this.WriteThenReadValue(clrValue, typeReference, version, isIeee754Compatible: true); + + // roundtrip without type reference + var actualValueWithoutTypeRef = this.WriteThenReadValue(clrValue, null, version, isIeee754Compatible: true); + + // Assert + Assert.Equal(clrValue.GetType(), actualValuewithTypeRef.GetType()); + Assert.Equal(actualValuewithTypeRef, clrValue); + + Assert.Equal(typeof(string), actualValueWithoutTypeRef.GetType()); + Assert.Equal(expectedValueForNullTypeReference, actualValueWithoutTypeRef); + } + [Fact] public void GuidRoundtripJsonTest() { @@ -720,6 +858,13 @@ private void VerifyPrimitiveValueDoesNotRoundtrip(object clrValue, IEdmTypeRefer } Assert.True(true); + } + else if (clrValue != null && clrValue is long longClrValue) + { + if(isIeee754Compatible) + { + Assert.NotEqual(actualValue.GetType(), clrValue.GetType()); + } } else {