Skip to content

Commit 6f0d76b

Browse files
authored
Enable instance annotations for null value nested resource or without value nested resource (#3461)
* Fixes #3440: Enable instance annotations for null value nested resource * Fiexes the failing test
1 parent 217020a commit 6f0d76b

14 files changed

+472
-69
lines changed

src/Microsoft.OData.Core/Json/ODataJsonPropertyAndValueDeserializer.cs

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -524,11 +524,13 @@ protected ODataJsonReaderNestedResourceInfo InnerReadUndeclaredProperty(IODataJs
524524
ValidateExpandedNestedResourceInfoPropertyValue(this.JsonReader, isCollection, propertyName, payloadTypeReference);
525525
if (isCollection)
526526
{
527+
// Sam: Unclear that since it's an undeclared property, why call 'ReadExpandedResourceSetNestedResourceInfo' to tread it as navigation property, not call 'ReadNonExpandedResourceSetNestedResourceInfo'?
527528
readerNestedResourceInfo =
528-
ReadExpandedResourceSetNestedResourceInfo(resourceState, null, payloadTypeReference.ToStructuredType(), propertyName, /*isDeltaResourceSet*/ false);
529+
ReadExpandedResourceSetNestedResourceInfo(resourceState, null, payloadTypeReference.ToStructuredType(), propertyName, isDeltaResourceSet: false, this.MessageReaderSettings);
529530
}
530531
else
531532
{
533+
// Sam: Unclear that since it's an undeclared property, why call 'ReadExpandedResourceNestedResourceInfo' to tread it as navigation property, not call 'ReadNonExpandedResourceNestedResourceInfo'?
532534
readerNestedResourceInfo = ReadExpandedResourceNestedResourceInfo(resourceState, null, propertyName, payloadTypeReference.ToStructuredType(), this.MessageReaderSettings);
533535
}
534536

@@ -606,7 +608,7 @@ protected static void ValidateExpandedNestedResourceInfoPropertyValue(
606608
}
607609

608610
/// <summary>
609-
/// Reads non-expanded nested resource set.
611+
/// Reads non-expanded (complex) nested resource set.
610612
/// </summary>
611613
/// <param name="resourceState">The state of the reader for resource to read.</param>
612614
/// <param name="collectionProperty">The collection of complex property for which to read the nested resource info. null for undeclared property.</param>
@@ -629,6 +631,8 @@ protected static ODataJsonReaderNestedResourceInfo ReadNonExpandedResourceSetNes
629631
IsComplex = true
630632
};
631633

634+
AttachPropertyAnnotationsToNestedResourceInfo(resourceState, nestedResourceInfo);
635+
632636
ODataResourceSet expandedResourceSet = CreateCollectionResourceSet(resourceState, propertyName);
633637
return ODataJsonReaderNestedResourceInfo.CreateResourceSetReaderNestedResourceInfo(nestedResourceInfo, collectionProperty, nestedResourceType, expandedResourceSet);
634638
}
@@ -656,13 +660,36 @@ protected static ODataJsonReaderNestedResourceInfo ReadNonExpandedResourceNested
656660
IsComplex = true
657661
};
658662

659-
// Check the odata.type annotation for the complex property, it should show inside the complex object.
660-
if (ValidateDataPropertyTypeNameAnnotation(resourceState.PropertyAndAnnotationCollector, nestedResourceInfo.Name) != null)
663+
// Here's old logic: If a complex nested resource has `odata.type` property annotation, then throw exception, it says the annotation should be inside complex object.
664+
// It doesn't make sense now since the complex nested resoruce value could be null , in that case the `odata.type` annotation has to be outside the complex object.
665+
AttachPropertyAnnotationsToNestedResourceInfo(resourceState, nestedResourceInfo);
666+
667+
return ODataJsonReaderNestedResourceInfo.CreateResourceReaderNestedResourceInfo(nestedResourceInfo, complexProperty, nestedResourceType);
668+
}
669+
670+
protected static void AttachPropertyAnnotationsToNestedResourceInfo(IODataJsonReaderResourceState resourceState, ODataNestedResourceInfo nestedResourceInfo)
671+
{
672+
Debug.Assert(resourceState != null, "resourceState != null");
673+
Debug.Assert(nestedResourceInfo != null, "nestedResourceInfo != null");
674+
675+
foreach (KeyValuePair<string, object> odataAnnotation
676+
in resourceState.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nestedResourceInfo.Name))
661677
{
662-
throw new ODataException(Error.Format(SRResources.ODataJsonPropertyAndValueDeserializer_ComplexValueWithPropertyTypeAnnotation, ODataAnnotationNames.ODataType));
678+
if (string.Equals(odataAnnotation.Key, "odata.type", StringComparison.Ordinal)
679+
|| string.Equals(odataAnnotation.Key, "type", StringComparison.Ordinal))
680+
{
681+
nestedResourceInfo.TypeAnnotation = new ODataTypeAnnotation((string)odataAnnotation.Value);
682+
}
683+
else
684+
{
685+
nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(odataAnnotation.Key, odataAnnotation.Value.ToODataValue(), true));
686+
}
663687
}
664688

665-
return ODataJsonReaderNestedResourceInfo.CreateResourceReaderNestedResourceInfo(nestedResourceInfo, complexProperty, nestedResourceType);
689+
foreach (KeyValuePair<string, object> instanceAnnotation in resourceState.PropertyAndAnnotationCollector.GetCustomPropertyAnnotations(nestedResourceInfo.Name))
690+
{
691+
nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(instanceAnnotation.Key, instanceAnnotation.Value.ToODataValue()));
692+
}
666693
}
667694

668695
/// <summary>
@@ -714,15 +741,27 @@ in resourceState.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nest
714741
break;
715742

716743
default:
717-
if (messageReaderSettings.ThrowOnUndeclaredPropertyForNonOpenType)
744+
if (messageReaderSettings.ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty)
718745
{
719746
throw new ODataException(Error.Format(SRResources.ODataJsonResourceDeserializer_UnexpectedExpandedSingletonNavigationLinkPropertyAnnotation, nestedResourceInfo.Name, propertyAnnotation.Key));
720747
}
721748

749+
// Let's save the property annotations into the nested resource info, therefore we can support the 'null' value nested resource as:
750+
// {
751+
// "[email protected]" : "anyvalue",
752+
// "NestedResource": null
753+
// }
754+
nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(propertyAnnotation.Key, propertyAnnotation.Value.ToODataValue(), true));
755+
722756
break;
723757
}
724758
}
725759

760+
foreach (KeyValuePair<string, object> instanceAnnotation in resourceState.PropertyAndAnnotationCollector.GetCustomPropertyAnnotations(nestedResourceInfo.Name))
761+
{
762+
nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(instanceAnnotation.Key, instanceAnnotation.Value.ToODataValue()));
763+
}
764+
726765
return ODataJsonReaderNestedResourceInfo.CreateResourceReaderNestedResourceInfo(nestedResourceInfo, navigationProperty, propertyType);
727766
}
728767

@@ -734,11 +773,13 @@ in resourceState.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nest
734773
/// <param name="propertyType">The type of the collection.</param>
735774
/// <param name="propertyName">The property name.</param>
736775
/// <param name="isDeltaResourceSet">The property being read represents a nested delta resource set.</param>
776+
/// <param name="messageReaderSettings">The ODataMessageReaderSettings.</param>
737777
/// <returns>The nested resource info for the expanded link read.</returns>
738778
/// <remarks>
739779
/// This method doesn't move the reader.
740780
/// </remarks>
741-
protected static ODataJsonReaderNestedResourceInfo ReadExpandedResourceSetNestedResourceInfo(IODataJsonReaderResourceState resourceState, IEdmNavigationProperty navigationProperty, IEdmStructuredType propertyType, string propertyName, bool isDeltaResourceSet)
781+
protected static ODataJsonReaderNestedResourceInfo ReadExpandedResourceSetNestedResourceInfo(IODataJsonReaderResourceState resourceState,
782+
IEdmNavigationProperty navigationProperty, IEdmStructuredType propertyType, string propertyName, bool isDeltaResourceSet, ODataMessageReaderSettings messageReaderSettings)
742783
{
743784
Debug.Assert(resourceState != null, "resourceState != null");
744785
Debug.Assert(navigationProperty != null || propertyName != null, "navigationProperty != null || propertyName != null");
@@ -796,10 +837,21 @@ in resourceState.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nest
796837

797838
case ODataAnnotationNames.ODataDeltaLink: // Delta links are not supported on expanded resource sets.
798839
default:
799-
throw new ODataException(Error.Format(SRResources.ODataJsonResourceDeserializer_UnexpectedExpandedCollectionNavigationLinkPropertyAnnotation, nestedResourceInfo.Name, propertyAnnotation.Key));
840+
if (messageReaderSettings.ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty)
841+
{
842+
throw new ODataException(Error.Format(SRResources.ODataJsonResourceDeserializer_UnexpectedExpandedCollectionNavigationLinkPropertyAnnotation, nestedResourceInfo.Name, propertyAnnotation.Key));
843+
}
844+
845+
nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(propertyAnnotation.Key, propertyAnnotation.Value.ToODataValue(), true));
846+
break;
800847
}
801848
}
802849

850+
foreach (KeyValuePair<string, object> instanceAnnotation in resourceState.PropertyAndAnnotationCollector.GetCustomPropertyAnnotations(nestedResourceInfo.Name))
851+
{
852+
nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(instanceAnnotation.Key, instanceAnnotation.Value.ToODataValue()));
853+
}
854+
803855
return ODataJsonReaderNestedResourceInfo.CreateResourceSetReaderNestedResourceInfo(nestedResourceInfo, navigationProperty, propertyType, expandedResourceSet);
804856
}
805857

@@ -2338,7 +2390,7 @@ await ValidateExpandedNestedResourceInfoPropertyValueAsync(
23382390
if (isCollection)
23392391
{
23402392
readerNestedResourceInfo =
2341-
ReadExpandedResourceSetNestedResourceInfo(resourceState, null, payloadTypeReference.ToStructuredType(), propertyName, isDeltaResourceSet: false);
2393+
ReadExpandedResourceSetNestedResourceInfo(resourceState, null, payloadTypeReference.ToStructuredType(), propertyName, isDeltaResourceSet: false, this.MessageReaderSettings);
23422394
}
23432395
else
23442396
{

src/Microsoft.OData.Core/Json/ODataJsonReader.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,16 +1826,30 @@ private void ReadExpandedNestedResourceInfoStart(ODataNestedResourceInfo nestedR
18261826

18271827
// Expanded null resource
18281828
// The expected type and expected navigation source for an expanded resource are the same as for the nested resource info around it.
1829-
this.EnterScope(new JsonResourceScope(ODataReaderState.ResourceStart, /*resource*/ null,
1830-
this.CurrentNavigationSource, this.CurrentResourceTypeReference, /*propertyAndAnnotationCollector*/null,
1831-
/*projectedProperties*/null, this.CurrentScope.ODataUri));
1829+
this.EnterScope(new JsonResourceScope(ODataReaderState.ResourceStart, resource: null,
1830+
navigationSource: this.CurrentNavigationSource, expectedResourceTypeReference: this.CurrentResourceTypeReference, propertyAndAnnotationCollector: null,
1831+
selectedProperties: null, odataUri: this.CurrentScope.ODataUri));
18321832
}
18331833
else
18341834
{
1835-
// Expanded resource
1835+
// Expanded resource, a JSON object
18361836
// The expected type for an expanded resource is the same as for the nested resource info around it.
18371837
JsonResourceBaseScope parentScope = (JsonResourceBaseScope)this.ParentScope;
18381838
SelectedPropertiesNode parentSelectedProperties = parentScope.SelectedProperties;
1839+
1840+
// OData spec says: When annotating a name/value pair for which the value is represented as a JSON object, each annotation is placed within the object and represented as a single name/value pair.
1841+
// So, do we need to verify that the property annotation should be specified as an instance annotation in the nested resource value?
1842+
// Or, just let it go and let the upper reader to handle it?
1843+
// Sam descided to comment out the verification for now. 12/2025, reasons:
1844+
// 1) This would be a breaking change for existing clients.
1845+
// 2) The upper reader will handle invalid annotations anyway.
1846+
1847+
//if (parentScope.PropertyAndAnnotationCollector.GetCustomPropertyAnnotations(nestedResourceInfo.Name).Any() ||
1848+
// parentScope.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nestedResourceInfo.Name).Count > 0)
1849+
//{
1850+
// throw new ODataException(Error.Format(SRResources.ODataJsonPropertyAndValueDeserializer_NestedResourceValueWithPropertyTypeAnnotation, nestedResourceInfo.Name));
1851+
//}
1852+
18391853
Debug.Assert(parentSelectedProperties != null, "parentProjectedProperties != null");
18401854
this.ReadResourceSetItemStart(/*propertyAndAnnotationCollector*/ null, parentSelectedProperties.GetSelectedPropertiesForNavigationProperty(parentScope.ResourceType, nestedResourceInfo.Name));
18411855
}

src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ internal ODataJsonReaderNestedInfo ReadPropertyWithoutValue(IODataJsonReaderReso
989989
if (edmProperty == null || edmProperty.Type.IsUntyped())
990990
{
991991
// Undeclared property - we need to run detection algorithm here.
992-
readerNestedInfo = this.ReadUndeclaredProperty(resourceState, propertyName, propertyWithValue: false);
992+
readerNestedInfo = this.ReadUndeclaredProperty(resourceState, propertyName, propertyWithValue: false);
993993

994994
this.AssertJsonCondition(JsonNodeType.Property, JsonNodeType.EndObject);
995995
return readerNestedInfo;
@@ -1274,7 +1274,7 @@ private ODataJsonReaderNestedInfo ReadPropertyWithValue(IODataJsonReaderResource
12741274
if (isCollection)
12751275
{
12761276
readerNestedResourceInfo = this.ReadingResponse || isDeltaResourceSet
1277-
? ReadExpandedResourceSetNestedResourceInfo(resourceState, navigationProperty, navigationProperty.Type.ToStructuredType(), propertyName, /*isDeltaResourceSet*/ isDeltaResourceSet)
1277+
? ReadExpandedResourceSetNestedResourceInfo(resourceState, navigationProperty, navigationProperty.Type.ToStructuredType(), propertyName, isDeltaResourceSet: isDeltaResourceSet, this.MessageReaderSettings)
12781278
: ReadEntityReferenceLinksForCollectionNavigationLinkInRequest(resourceState, navigationProperty, propertyName, /*isExpanded*/ true);
12791279
}
12801280
else
@@ -3502,7 +3502,7 @@ await ValidateExpandedNestedResourceInfoPropertyValueAsync(
35023502
if (isCollection)
35033503
{
35043504
readerNestedResourceInfo = this.ReadingResponse || isDeltaResourceSet
3505-
? ReadExpandedResourceSetNestedResourceInfo(resourceState, navigationProperty, navigationProperty.Type.ToStructuredType(), propertyName, isDeltaResourceSet: isDeltaResourceSet)
3505+
? ReadExpandedResourceSetNestedResourceInfo(resourceState, navigationProperty, navigationProperty.Type.ToStructuredType(), propertyName, isDeltaResourceSet: isDeltaResourceSet, this.MessageReaderSettings)
35063506
: ReadEntityReferenceLinksForCollectionNavigationLinkInRequest(resourceState, navigationProperty, propertyName, isExpanded: true);
35073507
}
35083508
else

src/Microsoft.OData.Core/Json/ODataJsonWriter.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -364,22 +364,22 @@ protected override void EndProperty(ODataPropertyInfo property)
364364
/// <param name="resource">The resource to write.</param>
365365
protected override void StartResource(ODataResource resource)
366366
{
367-
ODataNestedResourceInfo parentNavLink = this.ParentNestedResourceInfo;
368-
if (parentNavLink != null)
367+
ODataNestedResourceInfo nestedResourceInfo = this.ParentNestedResourceInfo;
368+
if (nestedResourceInfo != null)
369369
{
370370
// For a null value, write the type as a property annotation
371371
if (resource == null)
372372
{
373-
if (parentNavLink.TypeAnnotation != null && parentNavLink.TypeAnnotation.TypeName != null)
373+
if (nestedResourceInfo.TypeAnnotation != null && nestedResourceInfo.TypeAnnotation.TypeName != null)
374374
{
375-
this.odataAnnotationWriter.WriteODataTypePropertyAnnotation(parentNavLink.Name, parentNavLink.TypeAnnotation.TypeName);
375+
this.odataAnnotationWriter.WriteODataTypePropertyAnnotation(nestedResourceInfo.Name, nestedResourceInfo.TypeAnnotation.TypeName);
376376
}
377377

378-
this.instanceAnnotationWriter.WriteInstanceAnnotations(parentNavLink.GetInstanceAnnotations(), parentNavLink.Name);
378+
this.instanceAnnotationWriter.WriteInstanceAnnotations(nestedResourceInfo.GetInstanceAnnotations(), nestedResourceInfo.Name);
379379
}
380380

381381
// Write the property name of an expanded navigation property to start the value.
382-
this.jsonWriter.WriteName(parentNavLink.Name);
382+
this.jsonWriter.WriteName(nestedResourceInfo.Name);
383383
}
384384

385385
if (resource == null)

src/Microsoft.OData.Core/ODataMessageReaderSettings.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public ValidationKinds Validations
9494
ThrowOnDuplicatePropertyNames = (validations & ValidationKinds.ThrowOnDuplicatePropertyNames) != 0;
9595
ThrowIfTypeConflictsWithMetadata = (validations & ValidationKinds.ThrowIfTypeConflictsWithMetadata) != 0;
9696
ThrowOnUndeclaredPropertyForNonOpenType = (validations & ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType) != 0;
97+
ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty = (validations & ValidationKinds.ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty) != 0;
9798
}
9899
}
99100

@@ -224,6 +225,11 @@ public ODataMessageQuotas MessageQuotas
224225
/// </summary>
225226
internal bool ThrowOnUndeclaredPropertyForNonOpenType { get; private set; }
226227

228+
/// <summary>
229+
/// Returns whether ThwoOnUnexpectedODataPropertyAnnotationsOnNavigationProperty validation setting is enabled.
230+
/// </summary>
231+
internal bool ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty { get; private set; }
232+
227233
/// <summary>
228234
/// Gets or sets a value that indicates whether the reader should put key values in their own URI segment when automatically building URIs.
229235
/// If this value is false, automatically-generated URLs will take the form "../EntitySet('KeyValue')/..".
@@ -299,6 +305,7 @@ private void CopyFrom(ODataMessageReaderSettings other)
299305
this.ThrowOnDuplicatePropertyNames = other.ThrowOnDuplicatePropertyNames;
300306
this.ThrowIfTypeConflictsWithMetadata = other.ThrowIfTypeConflictsWithMetadata;
301307
this.ThrowOnUndeclaredPropertyForNonOpenType = other.ThrowOnUndeclaredPropertyForNonOpenType;
308+
this.ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty = other.ThrowOnUnexpectedODataPropertyAnnotationOnNavigationProperty;
302309
this.LibraryCompatibility = other.LibraryCompatibility;
303310
this.Version = other.Version;
304311
this.ReadAsStreamFunc = other.ReadAsStreamFunc;

0 commit comments

Comments
 (0)