diff --git a/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs b/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs index c6eaace36a..420268b3a3 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonResourceDeserializer.cs @@ -1111,6 +1111,17 @@ in resourceState.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nest nestedResourceInfo.TypeAnnotation = new ODataTypeAnnotation((string)propertyAnnotation.Value); break; + case ODataAnnotationNames.ODataCount: + Debug.Assert(propertyAnnotation.Value is long && propertyAnnotation.Value != null, "The odata.count annotation should have been parsed as a non-null long."); + nestedResourceInfo.Count = (long?)propertyAnnotation.Value; + break; + + // TODO: do we support odata.context uri here? why? + case ODataAnnotationNames.ODataContext: + Debug.Assert(propertyAnnotation.Value is Uri && propertyAnnotation.Value != null, "The odata.context annotation should have been parsed as a non-null Uri."); + nestedResourceInfo.ContextUrl = (Uri)propertyAnnotation.Value; + break; + default: throw new ODataException(Error.Format(SRResources.ODataJsonResourceDeserializer_UnexpectedDeferredLinkPropertyAnnotation, nestedResourceInfo.Name, propertyAnnotation.Key)); } diff --git a/src/Microsoft.OData.Core/Json/ODataJsonResourceSerializer.cs b/src/Microsoft.OData.Core/Json/ODataJsonResourceSerializer.cs index 50ac30406d..813d42487c 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonResourceSerializer.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonResourceSerializer.cs @@ -208,7 +208,7 @@ internal void WriteResourceEndMetadataProperties(IODataJsonWriterResourceState r Debug.Assert(resource.MetadataBuilder != null, "resource.MetadataBuilder != null"); navigationLinkInfo.NestedResourceInfo.MetadataBuilder = resource.MetadataBuilder; - this.WriteNavigationLinkMetadata(navigationLinkInfo.NestedResourceInfo, duplicatePropertyNameChecker); + this.WriteNavigationLinkMetadata(navigationLinkInfo.NestedResourceInfo, duplicatePropertyNameChecker, count: false); navigationLinkInfo = resource.MetadataBuilder.GetNextUnprocessedNavigationLink(); } @@ -240,7 +240,8 @@ internal void WriteResourceEndMetadataProperties(IODataJsonWriterResourceState r /// /// The navigation link to write the metadata for. /// The DuplicatePropertyNameChecker to use. - internal void WriteNavigationLinkMetadata(ODataNestedResourceInfo nestedResourceInfo, IDuplicatePropertyNameChecker duplicatePropertyNameChecker) + /// The boolean value indicating to write the count value if has. + internal void WriteNavigationLinkMetadata(ODataNestedResourceInfo nestedResourceInfo, IDuplicatePropertyNameChecker duplicatePropertyNameChecker, bool count = false) { Debug.Assert(nestedResourceInfo != null, "nestedResourceInfo != null"); Debug.Assert(!string.IsNullOrEmpty(nestedResourceInfo.Name), "The nested resource info Name should have been validated by now."); @@ -261,6 +262,12 @@ internal void WriteNavigationLinkMetadata(ODataNestedResourceInfo nestedResource this.ODataAnnotationWriter.WritePropertyAnnotationName(navigationLinkName, ODataAnnotationNames.ODataNavigationLinkUrl); this.JsonWriter.WriteValue(this.UriToString(navigationLinkUrl)); } + + if (count && nestedResourceInfo.Count != null) + { + this.ODataAnnotationWriter.WritePropertyAnnotationName(navigationLinkName, ODataAnnotationNames.ODataCount); + this.JsonWriter.WriteValue(nestedResourceInfo.Count.Value); + } } /// @@ -567,8 +574,9 @@ await this.WriteOperationsAsync(functions.Cast(), /*isAction*/ f /// /// The navigation link to write the metadata for. /// The DuplicatePropertyNameChecker to use. + /// he boolean value indicating to write the count value if has. /// A task that represents the asynchronous write operation. - internal async Task WriteNavigationLinkMetadataAsync(ODataNestedResourceInfo nestedResourceInfo, IDuplicatePropertyNameChecker duplicatePropertyNameChecker) + internal async Task WriteNavigationLinkMetadataAsync(ODataNestedResourceInfo nestedResourceInfo, IDuplicatePropertyNameChecker duplicatePropertyNameChecker, bool count = false) { Debug.Assert(nestedResourceInfo != null, "nestedResourceInfo != null"); Debug.Assert(!string.IsNullOrEmpty(nestedResourceInfo.Name), "The nested resource info Name should have been validated by now."); @@ -592,6 +600,12 @@ await this.ODataAnnotationWriter.WritePropertyAnnotationNameAsync(navigationLink await this.JsonWriter.WriteValueAsync(this.UriToString(navigationLinkUrl)) .ConfigureAwait(false); } + + if (count && nestedResourceInfo.Count.HasValue) + { + await this.ODataAnnotationWriter.WritePropertyAnnotationNameAsync(navigationLinkName, ODataAnnotationNames.ODataCount).ConfigureAwait(false); + await this.JsonWriter.WriteValueAsync(nestedResourceInfo.Count.Value).ConfigureAwait(false); + } } /// diff --git a/src/Microsoft.OData.Core/Json/ODataJsonWriter.cs b/src/Microsoft.OData.Core/Json/ODataJsonWriter.cs index 671ae24808..6498e05157 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonWriter.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonWriter.cs @@ -968,7 +968,7 @@ protected override void WriteDeferredNestedResourceInfo(ODataNestedResourceInfo Debug.Assert(this.writingResponse, "Deferred links are only supported in response, we should have verified this already."); // A deferred nested resource info is just the link metadata, no value. - this.jsonResourceSerializer.WriteNavigationLinkMetadata(nestedResourceInfo, this.DuplicatePropertyNameChecker); + this.jsonResourceSerializer.WriteNavigationLinkMetadata(nestedResourceInfo, this.DuplicatePropertyNameChecker, count: true); } /// @@ -999,7 +999,7 @@ protected override void StartNestedResourceInfoWithContent(ODataNestedResourceIn } // Write the nested resource info metadata first. The rest is written by the content resource or resource set. - this.jsonResourceSerializer.WriteNavigationLinkMetadata(nestedResourceInfo, this.DuplicatePropertyNameChecker); + this.jsonResourceSerializer.WriteNavigationLinkMetadata(nestedResourceInfo, this.DuplicatePropertyNameChecker, count: false); } else { @@ -2038,7 +2038,7 @@ protected override Task WriteDeferredNestedResourceInfoAsync(ODataNestedResource // A deferred nested resource info is just the link metadata, no value. return this.jsonResourceSerializer.WriteNavigationLinkMetadataAsync( nestedResourceInfo, - this.DuplicatePropertyNameChecker); + this.DuplicatePropertyNameChecker, count: true); } /// @@ -2078,7 +2078,7 @@ await this.jsonResourceSerializer.WriteNestedResourceInfoContextUrlAsync(innerNe // Write the nested resource info metadata first. The rest is written by the content resource or resource set. await this.jsonResourceSerializer.WriteNavigationLinkMetadataAsync( innerNestedResourceInfo, - this.DuplicatePropertyNameChecker).ConfigureAwait(false); + this.DuplicatePropertyNameChecker, count: false).ConfigureAwait(false); } } else diff --git a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs index 29451a8266..2d1247e5a2 100644 --- a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs +++ b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs @@ -49,6 +49,17 @@ public string Name set; } + /// Gets or sets the number of items for this nested resource info. + /// Be noted, this count property is for nested resource info without content. + /// For nested resource info with content, please specify the count on ODataResourceSetBase.Count. + /// + /// The number of items in the resource set. + public long? Count + { + get; + set; + } + /// Gets or sets the URI representing the Unified Resource Locator (URL) of the link. /// The URI representing the Unified Resource Locator (URL) of the link. public Uri Url diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedSerializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedSerializerTests.cs index 45777b0d96..2be751e3cb 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedSerializerTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonEntryAndFeedSerializerTests.cs @@ -51,6 +51,21 @@ public void SerializedNavigationPropertyShouldIncludeNavigationLinkUrl() Assert.Contains("NavigationProperty@odata.navigationLink\":\"http://example.com/navigation", jsonResult); } + [Fact] + public void SerializedNavigationPropertyShouldIncludeCountIfApply() + { + var jsonResult = this.SerializeJsonFragment(serializer => + serializer.WriteNavigationLinkMetadata( + new ODataNestedResourceInfo + { + Name = "NavigationProperty", + Count = 42 + }, + new DuplicatePropertyNameChecker(), true)); + + Assert.Contains("NavigationProperty@odata.count\":42", jsonResult); + } + [Fact] public void WriteOperationsOnRequestsShouldThrow() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonResourceSerializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonResourceSerializerTests.cs index 5a546d1476..0a52530c12 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonResourceSerializerTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonResourceSerializerTests.cs @@ -311,6 +311,27 @@ public async Task WriteNavigationLinkMetadataAsync_WritesNavigationLinkMetadata( "\"BestSeller@odata.navigationLink\":\"http://tempuri.org/Categories(1)/BestSeller\"", result); } + [Fact] + public async Task WriteNavigationLinkMetadataAsync_WritesCountMetadata() + { + var nestedResourceInfo = new ODataNestedResourceInfo + { + Name = "BestSeller", + IsCollection = true, + Count = 42 + }; + + var result = await SetupJsonResourceSerializerAndRunTestAsync( + (jsonResourceSerializer) => + { + return jsonResourceSerializer.WriteNavigationLinkMetadataAsync( + nestedResourceInfo, + new NullDuplicatePropertyNameChecker(), true); + }); + + Assert.Equal("{\"BestSeller@odata.count\":42", result); + } + [Fact] public async Task WriteNestedResourceInfoContextUrlAsync_WritesNestedResourceInfoContextUrl() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/Json/FullPayloadValidateTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/Json/FullPayloadValidateTests.cs index 8979616178..64b6ff16a2 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/Json/FullPayloadValidateTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/Json/FullPayloadValidateTests.cs @@ -842,6 +842,33 @@ public void WritingNestedInlinecountTest() "\"ContainedCollectionNavProp@odata.navigationLink\":\"http://example.org/odata.svc/navigation\"," + "\"ContainedCollectionNavProp@odata.count\":1," + "\"ContainedCollectionNavProp\":[]" + + "}" + + "]" + + "}"; + Assert.Equal(expectedPayload, result); + } + + [Fact] + public void WritingNestedInlinecountWithoutContentTest() + { + this.containedCollectionNavLink.Count = 42; + ODataItem[] itemsToWrite = new ODataItem[] + { + new ODataResourceSet(), + this.entryWithOnlyData1, + this.containedCollectionNavLink + }; + + string resourcePath = "EntitySet"; + string result = this.GetWriterOutputForContentTypeAndKnobValue("application/json;odata.metadata=minimal", true, itemsToWrite, Model, EntitySet, EntityType, null, null, resourcePath); + + string expectedPayload = "{" + + "\"@odata.context\":\"http://example.org/odata.svc/$metadata#EntitySet\"," + + "\"value\":[" + + "{" + + "\"ID\":101,\"Name\":\"Alice\"," + + "\"ContainedCollectionNavProp@odata.navigationLink\":\"http://example.org/odata.svc/navigation\"," + + "\"ContainedCollectionNavProp@odata.count\":42" + "}" + "]" + "}"; @@ -922,6 +949,50 @@ public void ReadingNestedInlinecountTest() ODataResourceSet topFeed = feedList[1]; Assert.Null(topFeed.Count); } + + [Fact] + public void ReadingNestedInlinecountWithoutContentTest() + { + string payload = "{" + + "\"@odata.context\":\"http://example.org/odata.svc/$metadata#EntitySet\"," + + "\"value\":[" + + "{" + + "\"ID\":101,\"Name\":\"Alice\"," + + "\"ContainedCollectionNavProp@odata.context\":\"http://example.org/odata.svc/$metadata#EntitySet(101)/ContainedCollectionNavProp\"," + + "\"ContainedCollectionNavProp@odata.navigationLink\":\"http://example.org/odata.svc/navigation\"," + + "\"ContainedCollectionNavProp@odata.count\":51" + + "}" + + "]" + + "}"; + InMemoryMessage message = new InMemoryMessage(); + message.SetHeader("Content-Type", "application/json;odata.metadata=minimal"); + message.Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)); + List feedList = new List(); + + ODataNestedResourceInfo nestedResourceInfo = null; + using (var messageReader = new ODataMessageReader((IODataResponseMessage)message, null, Model)) + { + var reader = messageReader.CreateODataResourceSetReader(); + while (reader.Read()) + { + switch (reader.State) + { + case ODataReaderState.ResourceSetEnd: + feedList.Add(reader.Item as ODataResourceSet); + break; + + case ODataReaderState.NestedResourceInfoStart: + nestedResourceInfo = reader.Item as ODataNestedResourceInfo; + break; + + } + } + } + + Assert.Single(feedList); // only contains the toplevel + Assert.NotNull(nestedResourceInfo); + Assert.Equal(51, nestedResourceInfo.Count); + } #endregion Inlinecount Tests [Fact]