Skip to content

Commit

Permalink
Fixes #1301: Enable read the property without value
Browse files Browse the repository at this point in the history
  • Loading branch information
xuzhg committed Aug 24, 2024
1 parent a45702f commit 25d5577
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,21 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
}
}

/// <summary>
/// Deserializes the nested property info from <paramref name="resourceWrapper"/> into <paramref name="resource"/>.
/// Nested property info contains annotations for the property but without the property value.
/// </summary>
/// <param name="resource">The object into which the properties should be read.</param>
/// <param name="resourceWrapper">The resource object containing the structural properties.</param>
/// <param name="structuredType">The type of the resource.</param>
/// <param name="readContext">The deserializer context.</param>
public virtual void ApplyNestedPropertyInfos(object resource, ODataResourceWrapper resourceWrapper,
IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
// Add this method to provide customers an solution to customize the deserializer to handle the properties without value.
// We Will fill this method later when we enable the instance annotation feature.
}

/// <summary>
/// Deserializes the structural properties from <paramref name="resourceWrapper"/> into <paramref name="resource"/>.
/// </summary>
Expand All @@ -489,7 +504,7 @@ public virtual void ApplyStructuralProperties(object resource, ODataResourceWrap
throw new ArgumentNullException(nameof(resourceWrapper));
}

foreach (ODataProperty property in resourceWrapper.Resource.Properties)
foreach (ODataProperty property in resourceWrapper.Resource.Properties.OfType<ODataProperty>())
{
ApplyStructuralProperty(resource, property, structuredType, readContext);
}
Expand Down Expand Up @@ -532,6 +547,7 @@ private void ApplyResourceProperties(object resource, ODataResourceWrapper resou
IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext);
ApplyNestedPropertyInfos(resource, resourceWrapper, structuredType, readContext);
ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ private static void ReadODataItem(ODataReader reader, Stack<ODataItemWrapper> it
resourceSetParentWrapper.Items.Add(new ODataPrimitiveWrapper((ODataPrimitiveValue)reader.Item));
break;

case ODataReaderState.NestedProperty:
Contract.Assert(itemsStack.Count > 0, "The nested property info should be a non-null primitive value within resource wrapper.");
ODataResourceWrapper resourceParentWrapper = (ODataResourceWrapper)itemsStack.Peek();
resourceParentWrapper.NestedPropertyInfos.Add((ODataPropertyInfo)reader.Item);
break;

default:
Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state.");
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public ODataResourceWrapper(ODataResourceBase resource)
IsDeletedResource = resource != null && resource is ODataDeletedResource;

NestedResourceInfos = new List<ODataNestedResourceInfoWrapper>();

NestedPropertyInfos = new List<ODataPropertyInfo>();
}

/// <summary>
Expand All @@ -42,5 +44,11 @@ public ODataResourceWrapper(ODataResourceBase resource)
/// Gets the inner nested resource infos.
/// </summary>
public IList<ODataNestedResourceInfoWrapper> NestedResourceInfos { get; }

/// <summary>
/// Gets the nested property infos.
/// The nested property info is a property without value but could have instance annotations.
/// </summary>
public IList<ODataPropertyInfo> NestedPropertyInfos { get; }
}
}
18 changes: 17 additions & 1 deletion src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3606,6 +3606,16 @@
<param name="structuredType">The type of the resource.</param>
<param name="readContext">The deserializer context.</param>
</member>
<member name="M:Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedPropertyInfos(System.Object,Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper,Microsoft.OData.Edm.IEdmStructuredTypeReference,Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext)">
<summary>
Deserializes the nested property info from <paramref name="resourceWrapper"/> into <paramref name="resource"/>.
Nested property info contains annotations for the property but without the property value.
</summary>
<param name="resource">The object into which the properties should be read.</param>
<param name="resourceWrapper">The resource object containing the structural properties.</param>
<param name="structuredType">The type of the resource.</param>
<param name="readContext">The deserializer context.</param>
</member>
<member name="M:Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperties(System.Object,Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper,Microsoft.OData.Edm.IEdmStructuredTypeReference,Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext)">
<summary>
Deserializes the structural properties from <paramref name="resourceWrapper"/> into <paramref name="resource"/>.
Expand Down Expand Up @@ -6331,6 +6341,12 @@
Gets the inner nested resource infos.
</summary>
</member>
<member name="P:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.NestedPropertyInfos">
<summary>
Gets the nested property infos.
The nested property info is a property without value but could have instance annotations.
</summary>
</member>
<member name="T:Microsoft.AspNetCore.OData.ODataApplicationBuilderExtensions">
<summary>
Provides extension methods for <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder"/> to add OData routes.
Expand Down Expand Up @@ -8668,7 +8684,7 @@
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.GetODataQueryContext(System.Object,System.Linq.IQueryable,Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor,Microsoft.AspNetCore.Http.HttpRequest)">
<summary>
Get the ODaya query context.
Get the OData query context.
</summary>
<param name="responseValue">The response value.</param>
<param name="singleResultCollection">The content as SingleResult.Queryable.</param>
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.AspNetCore.OData/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.ResourceSet
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.IsDeletedResource.get -> bool
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.NestedResourceInfos.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper>
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.NestedPropertyInfos.get -> System.Collections.Generic.IList<Microsoft.OData.ODataPropertyInfo>
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.ODataResourceWrapper(Microsoft.OData.ODataResourceBase resource) -> void
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.Resource.get -> Microsoft.OData.ODataResourceBase
Microsoft.AspNetCore.OData.ODataApplicationBuilderExtensions
Expand Down Expand Up @@ -1817,6 +1818,7 @@ virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataPrimitiveDeser
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyDeletedResource(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedProperties(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedProperty(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper resourceInfoWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedPropertyInfos(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperties(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperty(object resource, Microsoft.OData.ODataProperty structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void
virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.CreateResourceInstance(Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,23 @@ public void ReadResource_Calls_ApplyStructuralProperties()
deserializer.Verify();
}

[Fact]
public void ReadResource_Calls_ApplyNestedPropertyInfos()
{
// Arrange
Mock<ODataResourceDeserializer> deserializer = new Mock<ODataResourceDeserializer>(_deserializerProvider);
ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty<ODataProperty>() });
deserializer.CallBase = true;
deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42);
deserializer.Setup(d => d.ApplyNestedPropertyInfos(42, resourceWrapper, _productEdmType, _readContext)).Verifiable();

// Act
deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext);

// Assert
deserializer.Verify();
}

[Fact]
public void ReadResource_Calls_ApplyNestedProperties()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ static ODataReaderExtensionsTests()
{
Model = new EdmModel();

// Enum type simpleEnum
EdmEnumType colorEnum = new EdmEnumType("NS", "Color");
colorEnum.AddMember(new EdmEnumMember(colorEnum, "Red", new EdmEnumMemberValue(0)));
colorEnum.AddMember(new EdmEnumMember(colorEnum, "Blue", new EdmEnumMemberValue(1)));
colorEnum.AddMember(new EdmEnumMember(colorEnum, "Yellow", new EdmEnumMemberValue(2)));
Model.AddElement(colorEnum);

// Address
EdmComplexType address = new EdmComplexType("NS", "Address");
address.AddStructuralProperty("Street", EdmCoreModel.Instance.GetString(false));
Expand All @@ -36,6 +43,7 @@ static ODataReaderExtensionsTests()
Model.AddElement(customer);
customer.AddKeys(customer.AddStructuralProperty("CustomerID", EdmCoreModel.Instance.GetInt32(false)));
customer.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true));
customer.AddStructuralProperty("Color", new EdmEnumTypeReference(colorEnum, true));
customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, false));

// Order
Expand Down Expand Up @@ -116,6 +124,115 @@ public async Task ReadResourceWorksAsExpected()
Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name));
}

[Fact]
public async Task ReadResourceWithPropertyWithoutValueButWithInstanceAnnotationsWorksAsExpected()
{
// Arrange
// Property 'Name' without value but with instance annotations
const string payload =
"{" +
"\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," +
"\"CustomerID\": 17," +
"\"[email protected]\":123," +
"\"[email protected]\":true," +
"\"Location\": { \"Street\":\"154TH AVE\"}" +
"}";

IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers");
Assert.NotNull(customers); // Guard

// Act
Func<ODataMessageReader, Task<ODataReader>> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType);
ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V4, false, "*");

// Assert
Assert.NotNull(item);
ODataResourceWrapper resource = Assert.IsType<ODataResourceWrapper>(item);
Assert.NotNull(resource.Resource);

// Be noted, the ODL 8 inserts the primitive properties without value into the 'Properties' also.
// So, the Resource.Properties contains "CustomerID" and "Name".
Assert.Equal(2, resource.Resource.Properties.Count());

ODataPropertyInfo customerIdPropInfo = resource.Resource.Properties.First(c => c.Name == "CustomerID");
ODataProperty customerIdProp = Assert.IsType<ODataProperty>(customerIdPropInfo);
Assert.Equal("CustomerID", customerIdProp.Name);
Assert.Equal(17, customerIdProp.Value);

ODataPropertyInfo namePropInfo = resource.Resource.Properties.First(c => c.Name == "Name");
Assert.IsNotType<ODataProperty>(namePropInfo);
Assert.Equal(2, namePropInfo.InstanceAnnotations.Count);

ODataPropertyInfo nameProp = Assert.Single(resource.NestedPropertyInfos);
Assert.Equal("Name", nameProp.Name);
Assert.Equal(2, nameProp.InstanceAnnotations.Count);

ODataInstanceAnnotation primitiveAnnotation = nameProp.InstanceAnnotations.First(i => i.Name == "Custom.PrimitiveAnnotation");
ODataPrimitiveValue primitiveValue = Assert.IsType<ODataPrimitiveValue>(primitiveAnnotation.Value);
Assert.Equal(123, primitiveValue.Value);

ODataInstanceAnnotation booleanAnnotation = nameProp.InstanceAnnotations.First(i => i.Name == "Custom.BooleanAnnotation");
ODataPrimitiveValue booleanValue = Assert.IsType<ODataPrimitiveValue>(booleanAnnotation.Value);
Assert.True((bool)booleanValue.Value);

ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(resource.NestedResourceInfos);
Assert.Equal("Location", nestedInfoWrapper.NestedResourceInfo.Name);
}

[Fact]
public async Task ReadResourceWithNonPrimitivePropertyWithoutValueButWithInstanceAnnotationsWorksAsExpected()
{
// Arrange
const string payload =
"{" +
"\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," +
"\"[email protected]\":42," +
"\"[email protected]\":123," + // Put 'Name' after 'Color' property is by design
"\"[email protected]\":9" +
"}";

IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers");
Assert.NotNull(customers); // Guard

// Act
Func<ODataMessageReader, Task<ODataReader>> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType);
ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V4, false, "*");

// Assert
Assert.NotNull(item);
ODataResourceWrapper resource = Assert.IsType<ODataResourceWrapper>(item);
Assert.NotNull(resource.Resource);

ODataPropertyInfo namePropInfo = Assert.Single(resource.Resource.Properties);
Assert.IsNotType<ODataProperty>(namePropInfo);
Assert.Single(namePropInfo.InstanceAnnotations);

Assert.Equal(3, resource.NestedPropertyInfos.Count);

// -- Color
ODataPropertyInfo colorProp = resource.NestedPropertyInfos.Single(c => c.Name == "Color");
ODataInstanceAnnotation colorAnnotation = Assert.Single(colorProp.InstanceAnnotations);
Assert.Equal("Custom.PrimitiveAnnotation", colorAnnotation.Name);
ODataPrimitiveValue primitiveValue = Assert.IsType<ODataPrimitiveValue>(colorAnnotation.Value);
Assert.Equal(42, primitiveValue.Value);

// -- Name
ODataPropertyInfo nameProp = resource.NestedPropertyInfos.Single(c => c.Name == "Name");
ODataInstanceAnnotation primitiveAnnotation = Assert.Single(nameProp.InstanceAnnotations);
Assert.Equal("Custom.PrimitiveAnnotation", primitiveAnnotation.Name);
primitiveValue = Assert.IsType<ODataPrimitiveValue>(primitiveAnnotation.Value);
Assert.Equal(123, primitiveValue.Value);

// -- Location
ODataPropertyInfo locationProp = resource.NestedPropertyInfos.Single(c => c.Name == "Location");
ODataInstanceAnnotation locationAnnotation = Assert.Single(locationProp.InstanceAnnotations);
Assert.Equal("Custom.PrimitiveAnnotation", locationAnnotation.Name);
primitiveValue = Assert.IsType<ODataPrimitiveValue>(locationAnnotation.Value);
Assert.Equal(9, primitiveValue.Value);

Assert.Empty(resource.NestedResourceInfos);
}

[Fact]
public async Task ReadResourceSetWorksAsExpected()
{
Expand Down Expand Up @@ -575,7 +692,8 @@ public async Task ReadEntityReferenceLinksSetWorksAsExpected_V401()

private async Task<ODataItemWrapper> ReadPayloadAsync(string payload,
IEdmModel edmModel, Func<ODataMessageReader, Task<ODataReader>> createReader, ODataVersion version = ODataVersion.V4,
bool readUntypedAsString = false)
bool readUntypedAsString = false,
string annotationFilter = null)
{
var message = new InMemoryMessage()
{
Expand All @@ -591,6 +709,11 @@ private async Task<ODataItemWrapper> ReadPayloadAsync(string payload,
Version = version,
};

if (annotationFilter != null)
{
readerSettings.ShouldIncludeAnnotation = ODataUtils.CreateAnnotationFilter(annotationFilter);
}

using (var msgReader = new ODataMessageReader((IODataRequestMessageAsync)message, readerSettings, edmModel))
{
ODataReader reader = await createReader(msgReader);
Expand Down
Loading

0 comments on commit 25d5577

Please sign in to comment.