Skip to content

Commit

Permalink
Fixes #3068: Throws ResourceTypeAnnotationNotFirst error when payload…
Browse files Browse the repository at this point in the history
… has both @removed and @odata.type
  • Loading branch information
xuzhg committed Sep 20, 2024
1 parent 95233fc commit a98f2b7
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/Microsoft.OData.Core/Json/BufferingJsonReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,64 @@ internal BufferingJsonReader(IJsonReader innerReader, string inStreamErrorProper
this.currentBufferedNode = null;
}

internal bool TryGetValueFromBuffered(string propertyName, bool removeIfExist, out object value)
{
bool found = false;
BufferedNode currentNode = this.bufferedNodesHead;
if (this.bufferedNodesHead != null)
{
do
{
if (currentNode.NodeType == JsonNodeType.StartObject ||
currentNode.NodeType == JsonNodeType.StartArray)
{
break;
}

if (currentNode.NodeType == JsonNodeType.Property &&
currentNode.Value.ToString() == propertyName &&
currentNode.Next != currentNode) // must have another node
{
found = true;
break;
}

currentNode = currentNode.Next;
}
while (currentNode != this.bufferedNodesHead);
}

value = null;
if (!found)
{
return false;
}

BufferedNode valueNode = currentNode.Next;

if (removeIfExist)
{
currentNode.Previous.Next = valueNode.Next;
valueNode.Next.Previous = currentNode.Previous;

if (this.bufferedNodesHead == currentNode)
{
this.bufferedNodesHead = valueNode.Next;
}

if (this.isBuffering)
{
if (this.currentBufferedNode == currentNode || this.currentBufferedNode == valueNode)
{
this.currentBufferedNode = this.bufferedNodesHead;
}
}
}

value = valueNode.Value;
return true;
}

/// <summary>
/// The type of the last node read.
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ internal void ReadResourceTypeName(IODataJsonReaderResourceState resourceState)
Debug.Assert(resourceState != null, "resourceState != null");
this.AssertJsonCondition(JsonNodeType.Property, JsonNodeType.EndObject);

object value;
if (this.JsonReader.TryGetValueFromBuffered(ODataJsonConstants.PrefixedODataTypePropertyName, true, out value) ||
this.JsonReader.TryGetValueFromBuffered(ODataJsonConstants.SimplifiedODataTypePropertyName, true, out value))
{
resourceState.Resource.TypeName = ReaderUtils.AddEdmPrefixOfTypeName(ReaderUtils.RemovePrefixOfTypeName(value as string));
return;
}

// If the current node is the odata.type property - read it.
if (this.JsonReader.NodeType == JsonNodeType.Property)
{
Expand All @@ -114,6 +122,8 @@ internal void ReadResourceTypeName(IODataJsonReaderResourceState resourceState)

// Read the annotation value.
resourceState.Resource.TypeName = this.ReadODataTypeAnnotationValue();

// resourceState.TypeAnnotationFound = true;
}
}

Expand Down Expand Up @@ -821,10 +831,16 @@ internal object ReadEntryInstanceAnnotation(string annotationName, bool anyPrope
return this.ReadAndValidateAnnotationStringValue(annotationName);

case ODataAnnotationNames.ODataRemoved: // 'odata.removed'
// If the value of 'odata.removed' is an object, let's throw exception since it should be not read here.
if (this.JsonReader.NodeType == JsonNodeType.StartObject)
{
throw new ODataException(ODataErrorStrings.ODataJsonResourceDeserializer_UnexpectedDeletedEntryInResponsePayload);
}

// for others, let's skip it
this.JsonReader.SkipValue();
return null;

default:
ODataAnnotationNames.ValidateIsCustomAnnotationName(annotationName);
Debug.Assert(
Expand Down Expand Up @@ -905,6 +921,9 @@ internal void ApplyEntryInstanceAnnotation(IODataJsonReaderResourceState resourc
mediaResource.ETag = (string)annotationValue;
break;

case ODataAnnotationNames.ODataRemoved: // 'odata.removed'
break;

default:
ODataAnnotationNames.ValidateIsCustomAnnotationName(annotationName);
Debug.Assert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,159 @@ private static IEdmModel GetModelWithAbstractType(out EdmEntitySet entitySet)
return model;
}

[Fact]
public void ReadingResourceWithInstanceAnnotationAndODataTypeWorks1()
{
const string payload =
"{\"@odata.context\":\"http://svc/$metadata#EntitySet/$entity\"," +
"\"@odata.type\":\"#Namespace.EntityType\"," +
"\"Name\":\"SampleName\"," +
"\"@removed\":false," +
"\"ID\":89}";

EdmEntitySet entitySet = EntitySet;
IEdmModel model = Model;
IEdmEntityType entityType = EntitySet.EntityType;

InMemoryMessage message = new InMemoryMessage();
message.SetHeader("Content-Type", "application/json;odata.metadata=minimal");
message.Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload));

ODataResource topLevelResource = null;
ODataMessageReaderSettings settings = new ODataMessageReaderSettings(ODataVersion.V401)
{
};

using (var messageReader = new ODataMessageReader((IODataResponseMessage)message, settings, model))
{
var reader = messageReader.CreateODataResourceReader(entitySet, entityType);
while (reader.Read())
{
switch (reader.State)
{
case ODataReaderState.ResourceEnd:
topLevelResource = (ODataResource)reader.Item;
break;
}
}
}

Assert.NotNull(topLevelResource);
Assert.Equal(new Uri("http://svc/EntitySet(89)"), topLevelResource.Id);

Assert.Equal("Namespace.EntityType", topLevelResource.TypeName);
Assert.Equal("SampleName", Assert.IsType<ODataProperty>(topLevelResource.Properties.First(p => p.Name == "Name")).Value);
Assert.Equal(89, Assert.IsType<ODataProperty>(topLevelResource.Properties.First(p => p.Name == "ID")).Value);
}

[Fact]
public void ReadingResourceWithInstanceAnnotationAndODataTypeWorks()
{
const string payload =
"{\"@odata.context\":\"http://svc/$metadata#EntitySet/$entity\"," +
"\"@odata.type\":\"#Namespace.EntityType\"," +
"\"Name\":\"SampleName\"," +
"\"@NS.removed\":false," +
"\"ID\":89}";

EdmEntitySet entitySet = EntitySet;
IEdmModel model = Model;
IEdmEntityType entityType = EntitySet.EntityType;

InMemoryMessage message = new InMemoryMessage();
message.SetHeader("Content-Type", "application/json;odata.metadata=minimal");
message.Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload));

ODataResource topLevelResource = null;
ODataMessageReaderSettings settings = new ODataMessageReaderSettings(ODataVersion.V401)
{
ShouldIncludeAnnotation = (annotation) => true
};

using (var messageReader = new ODataMessageReader((IODataResponseMessage)message, settings, model))
{
var reader = messageReader.CreateODataResourceReader(entitySet, entityType);
while (reader.Read())
{
switch (reader.State)
{
case ODataReaderState.ResourceEnd:
topLevelResource = (ODataResource)reader.Item;
break;
}
}
}

Assert.NotNull(topLevelResource);
Assert.Equal(new Uri("http://svc/EntitySet(89)"), topLevelResource.Id);

Assert.Equal("Namespace.EntityType", topLevelResource.TypeName);
Assert.Equal("SampleName", Assert.IsType<ODataProperty>(topLevelResource.Properties.First(p => p.Name == "Name")).Value);
Assert.Equal(89, Assert.IsType<ODataProperty>(topLevelResource.Properties.First(p => p.Name == "ID")).Value);

ODataInstanceAnnotation annotation = Assert.Single(topLevelResource.InstanceAnnotations);
Assert.Equal("NS.removed", annotation.Name);
Assert.Equal(false, annotation.Value.FromODataValue());
}

[Fact]
public void ReadingDeletedResourceContainsODataTypeWorks()
{
const string payload =
"{\"@odata.context\":\"http://svc/$metadata#EntitySet/$delta\"," +
"\"value\":[" +
"{" +
"\"@odata.type\":\"#Namespace.EntityType\"," +
"\"Name\":\"SampleName\"," +
"\"@removed\":{\"reason\":\"deleted\"}," +
"\"ID\":89" +
"}" +
"]" +
"}";

EdmEntitySet entitySet = EntitySet;
IEdmModel model = Model;
IEdmEntityType entityType = EntitySet.EntityType;

InMemoryMessage message = new InMemoryMessage();
message.SetHeader("Content-Type", "application/json;odata.metadata=minimal");
message.Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload));

ODataMessageReaderSettings settings = new ODataMessageReaderSettings(ODataVersion.V401);

ODataDeletedResource deletedResource = null;
using (var messageReader = new ODataMessageReader((IODataResponseMessage)message, settings, model))
{
var reader = messageReader.CreateODataDeltaResourceSetReader(entitySet, entityType);
while (reader.Read())
{
switch (reader.State)
{
case ODataReaderState.DeltaResourceSetStart:
break;

case ODataReaderState.DeltaResourceSetEnd:
break;

case ODataReaderState.DeletedResourceStart:
deletedResource = (ODataDeletedResource)reader.Item;
break;
case ODataReaderState.DeletedResourceEnd:
break;
}
}
}

Assert.NotNull(deletedResource);
Assert.Equal(new Uri("http://svc/EntitySet(89)"), deletedResource.Id);

Assert.Equal("Namespace.EntityType", deletedResource.TypeName);

Assert.Equal(2, deletedResource.Properties.Count());
Assert.Equal("SampleName", Assert.IsType<ODataProperty>(deletedResource.Properties.First(p => p.Name == "Name")).Value);
Assert.Equal(89, Assert.IsType<ODataProperty>(deletedResource.Properties.First(p => p.Name == "ID")).Value);
}

[Theory]
[InlineData("minimal")]
[InlineData("full")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ await SetupReorderingJsonReaderAndRunTestAsync(
await reorderingReader.ReadPrimitiveValueAsync());
});
}

[Fact]
public async Task ReadReorderedPayloadContainingSimplifiedODataAnnotationsAsync()
{
Expand Down Expand Up @@ -345,6 +346,86 @@ await SetupReorderingJsonReaderAndRunTestAsync(
});
}

[Fact]
public async Task ReadReorderedPayload_OrderingContextRemovedTypeIdAsync()
{
var payload = "{" +
"\"Id\":1," +
"\"@odata.id\":\"http://tempuri.org/Customers(1)\"," +
"\"Name\":\"Sue\"," +
"\"@odata.removed\":{\"reason\":\"deleted\"}," +
"\"@odata.type\":\"SomeEntityType\"," +
"\"@odata.context\":\"any\"" +
"}";

await SetupReorderingJsonReaderAndRunTestAsync(
payload,
async (reorderingReader) =>
{
// 1. should be '@odata.context'
Assert.Equal("@odata.context", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip context value

// 2. should be '@odata.removed'
Assert.Equal("@odata.removed", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value

// 3. should be '@odata.type'
Assert.Equal("@odata.type", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value

// 4. should be '@odata.id'
Assert.Equal("@odata.id", await reorderingReader.ReadPropertyNameAsync());
Assert.Equal("http://tempuri.org/Customers(1)", await reorderingReader.ReadPrimitiveValueAsync());

Assert.Equal("Id", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value

Assert.Equal("Name", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value
});
}

[Fact]
public async Task ReadReorderedPayload_OrderingContextRemovedTypeId_ContainingSimplifiedAnnotationAsync()
{
var payload = "{" +
"\"Id\":1," +
"\"@id\":\"http://tempuri.org/Customers(1)\"," +
"\"Name\":\"Sue\"," +
"\"@removed\":{\"reason\":\"deleted\"}," +
"\"@context\":\"any\"," +
"\"@type\":\"SomeEntityType\""+
"}";

await SetupReorderingJsonReaderAndRunTestAsync(
payload,
async (reorderingReader) =>
{
// 1. should be '@context'
Assert.Equal("@context", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip context value

// 2. should be '@removed'
Assert.Equal("@removed", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value

// 3. should be '@type'
Assert.Equal("@type", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value

// 4. should be '@id'
Assert.Equal("@id", await reorderingReader.ReadPropertyNameAsync());
Assert.Equal("http://tempuri.org/Customers(1)", await reorderingReader.ReadPrimitiveValueAsync());

Assert.Equal("Id", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value

Assert.Equal("Name", await reorderingReader.ReadPropertyNameAsync());
await reorderingReader.SkipValueAsync(); // Skip object value
});
}

[Fact]
public async Task ReadBinaryValueAsync()
{
Expand Down

0 comments on commit a98f2b7

Please sign in to comment.