Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a4d1901
Rebase to 9.x
WanjohiSammy Jul 2, 2025
9c927d9
Add more tests
WanjohiSammy Jul 3, 2025
7d677d3
refactor tests
WanjohiSammy Jul 8, 2025
ecd4583
Add number conversition to Int64 before converting to decimal
WanjohiSammy Jul 16, 2025
9151ef6
nit
WanjohiSammy Jul 17, 2025
715b1ed
Add more tests
WanjohiSammy Aug 19, 2025
772cbba
refactor number parser
WanjohiSammy Aug 20, 2025
8654f30
remove unused variables
WanjohiSammy Aug 21, 2025
64fef94
Add a feature flag
WanjohiSammy Aug 21, 2025
c388681
revert changes in ParseNumberPrimitiveValue() of JsonReader
WanjohiSammy Aug 21, 2025
63a9ce5
refactor tests
WanjohiSammy Aug 21, 2025
7c9e24d
Add untypedtype tests
WanjohiSammy Aug 22, 2025
0673be8
Added E2E tests
WanjohiSammy Aug 22, 2025
4b7f410
some tests
WanjohiSammy Sep 2, 2025
1b607d0
Simplify E2E tests
WanjohiSammy Sep 3, 2025
89c9693
Use LibraryCompatibility instead of having to add another setting for…
WanjohiSammy Sep 4, 2025
a28eb6e
sort out failed tests
WanjohiSammy Sep 26, 2025
c2ef32c
Rebase to 9.x
WanjohiSammy Jul 2, 2025
3d715fe
Add more tests
WanjohiSammy Jul 3, 2025
4e597ce
refactor tests
WanjohiSammy Jul 8, 2025
7c41655
Add number conversition to Int64 before converting to decimal
WanjohiSammy Jul 16, 2025
814e0a3
nit
WanjohiSammy Jul 17, 2025
7fd89f3
Add more tests
WanjohiSammy Aug 19, 2025
9036ebd
refactor number parser
WanjohiSammy Aug 20, 2025
ede9038
remove unused variables
WanjohiSammy Aug 21, 2025
7c6473e
Add a feature flag
WanjohiSammy Aug 21, 2025
57c661c
revert changes in ParseNumberPrimitiveValue() of JsonReader
WanjohiSammy Aug 21, 2025
3b67cea
refactor tests
WanjohiSammy Aug 21, 2025
27ba0cc
Add untypedtype tests
WanjohiSammy Aug 22, 2025
d1c28f0
Added E2E tests
WanjohiSammy Aug 22, 2025
bb16e0e
some tests
WanjohiSammy Sep 2, 2025
ae7b7b7
Simplify E2E tests
WanjohiSammy Sep 3, 2025
4546387
Use LibraryCompatibility instead of having to add another setting for…
WanjohiSammy Sep 4, 2025
e522cef
sort out failed tests
WanjohiSammy Sep 26, 2025
2b9017b
Merge branch 'fix/resolve-untype-values' of https://github.com/OData/…
WanjohiSammy Sep 26, 2025
1f65eb1
default is set to disable the behavior to read untyped numeric as dec…
WanjohiSammy Sep 26, 2025
0d38347
resolve missing Public API
WanjohiSammy Oct 9, 2025
9e4b8c2
Merge branch 'dev-9.x' into fix/resolve-untype-values
WanjohiSammy Oct 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ internal object ReadODataOrCustomInstanceAnnotationValue(string annotationName,
/// <param name="primitiveTypeResolver">Function that takes a primitive value and returns an <see cref="IEdmTypeReference"/>.</param>
/// <param name="readUntypedAsString">Whether unknown properties should be read as a raw string value.</param>
/// <param name="generateTypeIfMissing">Whether to generate a type if not already part of the model.</param>
/// <param name="readUntypedNumericAsDecimal">Whether untyped numeric values should be preserved as decimals. Default is <see langword="true"/></param>
/// <returns>The <see cref="IEdmTypeReference"/> of the current value to be read.</returns>
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Each code path casts to bool at most one time, and only if needed.")]
internal static IEdmTypeReference ResolveUntypedType(
Expand All @@ -217,7 +218,8 @@ internal static IEdmTypeReference ResolveUntypedType(
IEdmTypeReference payloadTypeReference,
Func<object, string, IEdmTypeReference> primitiveTypeResolver,
bool readUntypedAsString,
bool generateTypeIfMissing)
bool generateTypeIfMissing,
bool readUntypedNumericAsDecimal)
{
if (payloadTypeReference != null && (payloadTypeReference.TypeKind() != EdmTypeKind.Untyped || readUntypedAsString))
{
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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();
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -3824,7 +3825,8 @@ await this.JsonReader.GetValueAsync().ConfigureAwait(false),
payloadTypeReference,
this.MessageReaderSettings.PrimitiveTypeResolver,
this.MessageReaderSettings.ReadUntypedAsString,
!this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's for 9.x, do we need to keep back compatible?

!this.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata,
this.MessageReaderSettings.LibraryCompatibility.HasFlag(ODataLibraryCompatibility.ReadUntypedNumericAsDecimal));

bool isCollection = payloadTypeReference.IsCollection();
IEdmStructuredType payloadTypeOrItemType = payloadTypeReference.ToStructuredType();
Expand Down
14 changes: 12 additions & 2 deletions src/Microsoft.OData.Core/ODataLibraryCompatibility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,24 @@ public enum ODataLibraryCompatibility
/// </summary>
UseLegacyODataInnerErrorSerialization = 1 << 4,

/// <summary>
/// When enabled, untyped numeric values are read as decimal.
/// </summary>
ReadUntypedNumericAsDecimal = 1 << 5,

/// <summary>
/// Version 6.x
/// </summary>
Version6 = UseLegacyVariableCasing | WriteTopLevelODataNullAnnotation | WriteODataContextAnnotationForNavProperty | DoNotThrowExceptionForTopLevelNullProperty | UseLegacyODataInnerErrorSerialization,
Version6 = UseLegacyVariableCasing | WriteTopLevelODataNullAnnotation | WriteODataContextAnnotationForNavProperty | DoNotThrowExceptionForTopLevelNullProperty | UseLegacyODataInnerErrorSerialization | ReadUntypedNumericAsDecimal,

/// <summary>
/// Version 7.x
/// </summary>
Version7 = UseLegacyVariableCasing | UseLegacyODataInnerErrorSerialization
Version7 = UseLegacyVariableCasing | UseLegacyODataInnerErrorSerialization | ReadUntypedNumericAsDecimal,

/// <summary>
/// Version 8.x
/// </summary>
Version8 = ReadUntypedNumericAsDecimal,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Customer">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="UntypedProperty" Type="Edm.Untyped" />
<Property Name="UntypedList" Type="Collection(Edm.Untyped)" />
<NavigationProperty Name="Orders" Type="Collection(Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes.Order)" />
</EntityType>
<EntityType Name="Order">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Amount" Type="Edm.Decimal" Nullable="false" Scale="variable" />
</EntityType>
<EntityContainer Name="Container">
<EntitySet Name="Customers" EntityType="Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes.Customer">
<NavigationPropertyBinding Path="Orders" Target="Orders" />
</EntitySet>
<EntitySet Name="Orders" EntityType="Microsoft.OData.E2E.TestCommon.Common.Server.UntypedTypes.Order" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//-----------------------------------------------------------------------------
// <copyright file="UntypedTypesController.cs" company=".NET Foundation">
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// See License.txt in the project root for license information.
// </copyright>
//------------------------------------------------------------------------------

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<Customer> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//---------------------------------------------------------------------
// <copyright file="UntypedTypesDataModel.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

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<object>? UntypedList { get; set; } = null;
public List<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
public int Id { get; set; }
public decimal Amount { get; set; }
}
Loading