Skip to content

Commit

Permalink
Fix reading untyped collections containing values with @type specifie…
Browse files Browse the repository at this point in the history
…d. (#1985)

* Fix reading untyped collections containing values with @type specified.

* Update code, tests
  • Loading branch information
mikepizzo authored Feb 12, 2021
1 parent 68e2bf5 commit 82eb2b2
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,7 @@ private object ReadNonEntityValueImplementation(
}
}

expectedTypeReference = expectedTypeReference != null && expectedTypeReference.IsUntyped() ? null : expectedTypeReference;
ODataTypeAnnotation typeAnnotation;
EdmTypeKind targetTypeKind;
IEdmTypeReference targetTypeReference = this.ReaderValidator.ResolvePayloadTypeNameAndComputeTargetType(
Expand Down Expand Up @@ -1907,7 +1908,7 @@ private object ReadNonEntityValueImplementation(
}

// If we have no expected type make sure the collection items are of the same kind and specify the same name.
if (collectionValidator != null)
if (collectionValidator != null && targetTypeKind != EdmTypeKind.None)
{
string payloadTypeNameFromResult = ODataJsonLightReaderUtils.GetPayloadTypeName(result);
Debug.Assert(expectedTypeReference == null, "If a collection validator is specified there must not be an expected value type reference.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,7 @@ private ODataJsonLightReaderNestedInfo TryReadAsStream(IODataJsonLightReaderReso
Func<IEdmPrimitiveType, bool, string, IEdmProperty, bool> readAsStream = this.MessageReaderSettings.ReadAsStreamFunc;

// is the property a stream or a stream collection,
// untyped collection,
// or a binary or binary collection the client wants to read as a stream...
if (
(primitiveType != null &&
Expand All @@ -1328,7 +1329,10 @@ private ODataJsonLightReaderNestedInfo TryReadAsStream(IODataJsonLightReaderReso
&& (property == null || !property.IsKey()) // don't stream key properties
&& (primitiveType.IsBinary() || primitiveType.IsString() || isCollection))
&& readAsStream(primitiveType, isCollection, propertyName, property))) ||
((propertyType == null || propertyType.Definition.AsElementType().IsUntyped())
(propertyType != null &&
isCollection &&
propertyType.Definition.AsElementType().IsUntyped()) ||
(propertyType == null
&& (isCollection || this.JsonReader.CanStream())
&& readAsStream != null
&& readAsStream(null, isCollection, propertyName, property)))
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OData.Core/Metadata/EdmLibraryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1572,7 +1572,7 @@ internal static IEdmTypeReference ToTypeReference(this IEdmType type, bool nulla
IEdmStructuredType untypedType = type as IEdmStructuredType;
if (untypedType != null)
{
return new EdmUntypedStructuredTypeReference(untypedType);
return new EdmUntypedStructuredTypeReference(untypedType, nullable);
}

return new EdmUntypedTypeReference((IEdmUntypedType)type);
Expand Down
10 changes: 8 additions & 2 deletions src/Microsoft.OData.Core/ODataReaderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ protected void ApplyResourceTypeNameFromPayload(string resourceTypeNameFromPaylo
ODataTypeAnnotation typeAnnotation;
EdmTypeKind targetTypeKind;
IEdmStructuredTypeReference targetResourceTypeReference =
(IEdmStructuredTypeReference)this.inputContext.MessageReaderSettings.Validator.ResolvePayloadTypeNameAndComputeTargetType(
this.inputContext.MessageReaderSettings.Validator.ResolvePayloadTypeNameAndComputeTargetType(
EdmTypeKind.None,
/*expectStructuredType*/ true,
/*defaultPrimitivePayloadType*/ null,
Expand All @@ -748,7 +748,7 @@ protected void ApplyResourceTypeNameFromPayload(string resourceTypeNameFromPaylo
this.inputContext.Model,
() => EdmTypeKind.Entity,
out targetTypeKind,
out typeAnnotation);
out typeAnnotation) as IEdmStructuredTypeReference;

IEdmStructuredType targetResourceType = null;
ODataResourceBase resource = this.Item as ODataResourceBase;
Expand All @@ -766,6 +766,12 @@ protected void ApplyResourceTypeNameFromPayload(string resourceTypeNameFromPaylo
{
resource.TypeName = resourceTypeNameFromPayload;
}
else if (this.CurrentResourceTypeReference.IsUntyped())
{
targetResourceTypeReference = this.CurrentResourceTypeReference.IsNullable ?
EdmUntypedStructuredTypeReference.NullableTypeReference :
EdmUntypedStructuredTypeReference.NonNullableTypeReference;
}

// Set the current resource type since the type might be derived from the expected one.
this.CurrentResourceTypeReference = targetResourceTypeReference;
Expand Down
9 changes: 6 additions & 3 deletions src/Microsoft.OData.Core/ReaderValidationUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ internal static IEdmTypeReference ResolveAndValidateNonPrimitiveTargetType(
payloadTypeKind == EdmTypeKind.TypeDefinition || payloadTypeKind == EdmTypeKind.Untyped,
"The payload type kind must be one of None, Primitive, Enum, Untyped, Complex, Entity, Collection or TypeDefinition.");
Debug.Assert(
expectedTypeReference == null || expectedTypeReference.TypeKind() == expectedTypeKind,
expectedTypeReference == null || expectedTypeReference.TypeKind() == EdmTypeKind.Untyped || expectedTypeReference.TypeKind() == expectedTypeKind,
"The expected type kind must match the expected type reference if that is available.");
Debug.Assert(
payloadType == null || payloadType.TypeKind == payloadTypeKind,
Expand Down Expand Up @@ -823,7 +823,10 @@ private static IEdmTypeReference ResolveAndValidateTargetTypeStrictValidationEna
{
// The payload type must be assignable to the expected type.
IEdmTypeReference payloadTypeReference = payloadType.ToTypeReference(/*nullable*/ true);
ValidationUtils.ValidateEntityTypeIsAssignable((IEdmEntityTypeReference)expectedTypeReference, (IEdmEntityTypeReference)payloadTypeReference);
if (!expectedTypeReference.IsUntyped())
{
ValidationUtils.ValidateEntityTypeIsAssignable((IEdmEntityTypeReference)expectedTypeReference, (IEdmEntityTypeReference)payloadTypeReference);
}

// Use the payload type
return payloadTypeReference;
Expand Down Expand Up @@ -1003,7 +1006,7 @@ private static EdmTypeKind ComputeTargetTypeKind(
}

EdmTypeKind targetTypeKind;
if (expectedTypeKind != EdmTypeKind.None)
if (expectedTypeKind != EdmTypeKind.None && expectedTypeKind != EdmTypeKind.Untyped)
{
// If we have an expected type, use that.
targetTypeKind = expectedTypeKind;
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OData.Core/ValidationUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ internal static void ValidateTypeKind(EdmTypeKind actualTypeKind, EdmTypeKind ex
return;
}

if (expectedTypeKind != actualTypeKind)
if (expectedTypeKind != EdmTypeKind.Untyped && expectedTypeKind != actualTypeKind)
{
if (typeName == null)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.OData.Edm/Schema/EdmUntypedStructuredType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public sealed class EdmUntypedStructuredType : EdmStructuredType, IEdmStructured
private readonly string name;
private readonly string fullName;

/// <summary>
/// The core Edm.Untyped singleton.
/// </summary>
public static readonly EdmUntypedStructuredType Instance = new EdmUntypedStructuredType();

/// <summary>
/// Initializes a new instance of the <see cref="EdmStructuredType"/> class.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,32 @@ namespace Microsoft.OData.Edm
/// </summary>
public class EdmUntypedStructuredTypeReference : EdmTypeReference, IEdmUntypedTypeReference, IEdmStructuredTypeReference
{
/// <summary>
/// Returns a static instance of a nullable untyped structured type reference.
/// </summary>
public static readonly IEdmStructuredTypeReference NullableTypeReference = new EdmUntypedStructuredTypeReference(EdmUntypedStructuredType.Instance, true);

/// <summary>
/// Returns a static instance of a non-nullable untyped structured type reference.
/// </summary>
public static readonly IEdmStructuredTypeReference NonNullableTypeReference = new EdmUntypedStructuredTypeReference(EdmUntypedStructuredType.Instance, false);

/// <summary>
/// Constructor
/// </summary>
/// <param name="definition">IEdmStructuredType definition.</param>
public EdmUntypedStructuredTypeReference(IEdmStructuredType definition)
: base(definition, true)
: this(definition, true)
{
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="definition">IEdmStructuredType definition.</param>
/// <param name="isNullable">Denotes whether the type can be nullable.</param>
public EdmUntypedStructuredTypeReference(IEdmStructuredType definition, bool isNullable)
: base(definition, isNullable)
{
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ public void CanReadUntypedCollection()
"\"" + binaryValue + "\"" +
",\"" + stringValue + "\"" +
",\"" + stringValue + "\"" +
", { \"name\": \"Betty\",\"age@type\":\"#Int32\",\"age\": 18}" +
", { \"@type\": \"#test.customer\",\"name\": \"Betty\",\"age\": 18}" +
",null" +
",true" +
",false" +
Expand All @@ -186,63 +188,120 @@ public void CanReadUntypedCollection()
return true;
};

foreach (Func<IEdmPrimitiveType, bool, string, IEdmProperty, bool> ShouldStream in new Func<IEdmPrimitiveType, bool, string, IEdmProperty, bool>[] { StreamCollection, StreamAll })
Func<IEdmPrimitiveType, bool, string, IEdmProperty, bool> StreamNone =
(IEdmPrimitiveType type, bool isCollection, string propertyName, IEdmProperty property) =>
{
return false;
};

foreach (Func<IEdmPrimitiveType, bool, string, IEdmProperty, bool> ShouldStream in new Func<IEdmPrimitiveType, bool, string, IEdmProperty, bool>[] { StreamCollection, StreamAll, StreamNone })
{
foreach (Variant variant in GetVariants(ShouldStream))
{
int expectedPropertyCount = variant.IsRequest ? 1 : 2;
ODataResource resource = null;
List<object> propertyValues = null;

List<object> collectionValues = null;
ODataResource currentResource = null;
Stack<ODataResource> resources = new Stack<ODataResource>();
ODataPropertyInfo currentProperty = null;
Stack<List<ODataProperty>> nestedProperties = new Stack<List<ODataProperty>>();
ODataReader reader = CreateODataReader(payload, variant);

while (reader.Read())
{
switch (reader.State)
{
case ODataReaderState.ResourceStart:
if (resource == null)
currentResource = reader.Item as ODataResource;
if (resources.Count == 1)
{
resource = reader.Item as ODataResource;
collectionValues.Add(reader.Item);
}
else
resources.Push(currentResource);
nestedProperties.Push(new List<ODataProperty>());

break;

case ODataReaderState.ResourceEnd:
currentResource = resources.Pop();
List<ODataProperty> properties = nestedProperties.Pop();
if (currentResource != null)
{
propertyValues.Add(reader.Item);
properties.AddRange(currentResource.NonComputedProperties);
currentResource.Properties = properties;
}
break;

case ODataReaderState.Primitive:
ODataPrimitiveValue primitive = reader.Item as ODataPrimitiveValue;
propertyValues.Add(primitive.Value);
if (resources.Count == 1)
{
ODataPrimitiveValue primitive = reader.Item as ODataPrimitiveValue;
collectionValues.Add(primitive.Value);
}

break;

case ODataReaderState.Stream:
propertyValues.Add(ReadStream(reader));
string streamValue = ReadStream(reader);
if (resources.Count == 1)
{
collectionValues.Add(streamValue);
}
else if (currentProperty != null)
{
nestedProperties.Peek().Add(new ODataProperty { Name = currentProperty.Name, Value = streamValue });
}
currentProperty = null;

break;

case ODataReaderState.NestedProperty:
currentProperty = reader.Item as ODataPropertyInfo;
break;

case ODataReaderState.NestedResourceInfoStart:
ODataNestedResourceInfo info = reader.Item as ODataNestedResourceInfo;
Assert.Equal("untypedCollection", info.Name);
Assert.Null(propertyValues);
propertyValues = new List<object>();
Assert.Null(collectionValues);
collectionValues = new List<object>();
break;
}
}

Assert.NotNull(resource);
Assert.Equal(expectedPropertyCount, resource.Properties.Count());
Assert.NotNull(resource.Properties.FirstOrDefault(p => p.Name == "id"));
Assert.Equal(7, propertyValues.Count);
Assert.Equal(binaryValue, propertyValues[0]);
Assert.Equal(stringValue, propertyValues[1]);
Assert.Equal(stringValue, propertyValues[2]);
Assert.Null(propertyValues[3]);
Assert.Equal(true, propertyValues[4]);
Assert.Equal(false, propertyValues[5]);
Assert.Equal(-10.5m, ((Decimal)propertyValues[6]));
Assert.NotNull(currentResource);
Assert.Equal(expectedPropertyCount, currentResource.Properties.Count());
Assert.NotNull(currentResource.Properties.FirstOrDefault(p => p.Name == "id"));
Assert.Equal(9, collectionValues.Count);
Assert.Equal(binaryValue, collectionValues[0]);
Assert.Equal(stringValue, collectionValues[1]);
Assert.Equal(stringValue, collectionValues[2]);
ValidateResource(collectionValues[3]);
ValidateResource(collectionValues[4]);
Assert.Equal("test.customer", ((ODataResource)collectionValues[4]).TypeName);
Assert.Null(collectionValues[5]);
Assert.Equal(true, collectionValues[6]);
Assert.Equal(false, collectionValues[7]);
Assert.Equal(-10.5m, (Decimal)collectionValues[8]);
}
}
}

private void ValidateResource(object value)
{
ODataResource resource = value as ODataResource;
Assert.NotNull(resource);
ValidateProperty(resource, "name", "Betty");
ValidateProperty(resource, "age", 18);
}

private void ValidateProperty(ODataResource resource, string propertyName, object expectedValue)
{
ODataProperty property = resource.Properties.FirstOrDefault(p => p.Name == propertyName);
if (property != null)
{
Assert.Equal(expectedValue, property.Value);
}
}

[Fact]
public void NotStreamingCollectionsWorks()
{
Expand Down

0 comments on commit 82eb2b2

Please sign in to comment.