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
{