From 9bcd4e58ff7d1d969bb42dd112a55a5df8b8ef0f Mon Sep 17 00:00:00 2001 From: Kennedy Kangethe Date: Thu, 16 Sep 2021 11:51:41 +0300 Subject: [PATCH 1/5] Instance annotations for expanded resource sets POC --- .../JsonLight/ODataJsonLightWriter.cs | 10 ++++++ .../ODataNestedResourceInfo.cs | 20 +++++++++++ ...nstanceAnnotationWriterIntegrationTests.cs | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs index 3c0403f0c5..52608a4ccb 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs @@ -1008,6 +1008,16 @@ protected override void WriteDeferredNestedResourceInfo(ODataNestedResourceInfo // A deferred nested resource info is just the link metadata, no value. this.jsonLightResourceSerializer.WriteNavigationLinkMetadata(nestedResourceInfo, this.DuplicatePropertyNameChecker); + + // Only write count when the ODataNestedResourceInfo represents a collection. + if (!(bool)nestedResourceInfo.IsCollection) + { + // Write the inline count if it's available. + this.WriteResourceSetCount(nestedResourceInfo.Count, nestedResourceInfo.Name); + } + + // Write custom instance annotations + this.instanceAnnotationWriter.WriteInstanceAnnotations(nestedResourceInfo.GetInstanceAnnotations(), nestedResourceInfo.Name); } /// diff --git a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs index 29451a8266..a87f954b36 100644 --- a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs +++ b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs @@ -8,7 +8,9 @@ namespace Microsoft.OData { #region Namespaces using System; + using System.Collections.Generic; using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; using Microsoft.OData.Evaluation; #endregion Namespaces @@ -92,6 +94,24 @@ public Uri AssociationLinkUrl } } + /// Gets or sets the number of items in the resource set. + /// The number of items in the resource set. + public long? Count + { + get; + set; + } + + /// + /// Collection of custom instance annotations. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "We want to allow the same instance annotation collection instance to be shared across ODataLib OM instances.")] + public ICollection InstanceAnnotations + { + get { return this.GetInstanceAnnotations(); } + set { this.SetInstanceAnnotations(value); } + } + /// Gets or sets the context url for this nested resource info. /// The URI representing the context url of the nested resource info. internal Uri ContextUrl { get; set; } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs index 21142c3719..16835289f8 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs @@ -555,6 +555,40 @@ private void WriteAnnotationAtEndExpandedFeedShouldFail(ODataFormat format) testRequestOfSingleton.Throws(Strings.ODataJsonLightWriter_InstanceAnnotationNotSupportedOnExpandedResourceSet); } + [Fact] + public void WriteAnnotationAndCountAtStartExpandedFeedShouldPassInJsonLight() + { + string expectedPayload = + "{" + + "\"@odata.context\":\"http://www.example.com/$metadata#TestEntitySet/$entity\"," + + "\"ID\":1," + + "\"ResourceSetNavigationProperty@odata.navigationLink\":\"http://service/navLink\"," + + "\"ResourceSetNavigationProperty@odata.count\":10," + + "\"ResourceSetNavigationProperty@custom.StartFeedAnnotation\":123" + + "}"; + + this.WriteAnnotationAndCountAtStartExpandedFeedShouldPass(ODataFormat.Json, expectedPayload, EntitySet); + } + + private void WriteAnnotationAndCountAtStartExpandedFeedShouldPass(ODataFormat format, string expectedPayload, IEdmNavigationSource navigationSource) + { + Action action = (odataWriter) => + { + var entryToWrite = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 1 } } }; + odataWriter.WriteStart(entryToWrite); + + ODataNestedResourceInfo navLink = new ODataNestedResourceInfo { Name = "ResourceSetNavigationProperty", Url = new Uri("http://service/navLink", UriKind.RelativeOrAbsolute), IsCollection = true }; + navLink.Count = 10; + navLink.InstanceAnnotations.Add(new ODataInstanceAnnotation("custom.StartFeedAnnotation", PrimitiveValue1)); + odataWriter.WriteStart(navLink); + + odataWriter.WriteEnd(); + odataWriter.WriteEnd(); + }; + + this.WriteAnnotationsAndValidatePayload(action, navigationSource, format, expectedPayload, request: false, createFeedWriter: false); + } + #endregion Writing instance annotations on expanded feeds #region Write Delta Feed From 98bdd0cd8d87a5f0709054cc48a150f071fe5f97 Mon Sep 17 00:00:00 2001 From: Kennedy Kangethe Date: Thu, 16 Sep 2021 14:32:01 +0300 Subject: [PATCH 2/5] Fix failing tests --- src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs index 52608a4ccb..1c7441e92e 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs @@ -1010,7 +1010,7 @@ protected override void WriteDeferredNestedResourceInfo(ODataNestedResourceInfo this.jsonLightResourceSerializer.WriteNavigationLinkMetadata(nestedResourceInfo, this.DuplicatePropertyNameChecker); // Only write count when the ODataNestedResourceInfo represents a collection. - if (!(bool)nestedResourceInfo.IsCollection) + if (nestedResourceInfo.IsCollection != null && nestedResourceInfo.IsCollection == true) { // Write the inline count if it's available. this.WriteResourceSetCount(nestedResourceInfo.Count, nestedResourceInfo.Name); From e8b45464e6f810fe6d7da0420d581e1cc7191bc6 Mon Sep 17 00:00:00 2001 From: Kennedy Kangethe Date: Tue, 21 Sep 2021 10:35:05 +0300 Subject: [PATCH 3/5] Add support for READ --- .../ODataJsonLightResourceDeserializer.cs | 11 ++++ ...CustomInstanceAnnotationAcceptanceTests.cs | 56 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs index 51a6a8dbcf..1e5c251f6e 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs @@ -1103,11 +1103,22 @@ in resourceState.PropertyAndAnnotationCollector.GetODataPropertyAnnotations(nest nestedResourceInfo.TypeAnnotation = new ODataTypeAnnotation((string)propertyAnnotation.Value); break; + case ODataAnnotationNames.ODataCount: + Debug.Assert(propertyAnnotation.Value is long, "The odata.count annotation should have been parsed as a 64 bit integer."); + nestedResourceInfo.Count = (long?)propertyAnnotation.Value; + break; + default: + // TODO: Update Error message throw new ODataException(ODataErrorStrings.ODataJsonLightResourceDeserializer_UnexpectedDeferredLinkPropertyAnnotation(nestedResourceInfo.Name, propertyAnnotation.Key)); } } + foreach (var instanceAnnotation in resourceState.PropertyAndAnnotationCollector.GetCustomPropertyAnnotations(nestedResourceInfo.Name)) + { + nestedResourceInfo.InstanceAnnotations.Add(new ODataInstanceAnnotation(instanceAnnotation.Key, instanceAnnotation.Value.ToODataValue(), /*isCustomAnnotation*/ true)); + } + return ODataJsonLightReaderNestedResourceInfo.CreateDeferredLinkInfo(nestedResourceInfo, navigationProperty); } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/CustomInstanceAnnotationAcceptanceTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/CustomInstanceAnnotationAcceptanceTests.cs index 4ebbfb9cbd..b18f09d783 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/CustomInstanceAnnotationAcceptanceTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/CustomInstanceAnnotationAcceptanceTests.cs @@ -89,6 +89,20 @@ static CustomInstanceAnnotationAcceptanceTests() "\"@Custom.FeedEndAnnotation\":1" + "}"; + const string JsonLightExpandedResourceSetPayloadWithCountAndCustomInstanceAnnotations = + "{" + + "\"@odata.context\":\"http://www.example.com/service.svc/$metadata#TestEntitySet\"," + + "\"value\":[" + + "{" + + "\"ID\":1," + + "\"ResourceSetNavigationProperty@odata.navigationLink\":\"http://example.com/multiple\"," + + "\"ResourceSetNavigationProperty@odata.count\":10," + + "\"ResourceSetNavigationProperty@custom.MyAnnotation\":\"123\"," + + "\"ResourceSetNavigationProperty@custom.StartFeedAnnotation\":123" + + "}" + + "]" + + "}"; + [Fact] public void CustomInstanceAnnotationFromFeedAndEntryInJsonLightShouldBeSkippedByTheReaderByDefault() { @@ -121,6 +135,48 @@ public void CustomInstanceAnnotationFromFeedAndEntryInJsonLightShouldBeSkippedBy } } + [Fact] + public void ShouldBeAbleToReadCountAndCustomInstanceAnnotationsFromExpandedResourceSetsInJsonLight() + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonLightExpandedResourceSetPayloadWithCountAndCustomInstanceAnnotations)); + var readerSettings = new ODataMessageReaderSettings { EnableMessageStreamDisposal = true }; + + IODataResponseMessage messageToRead = new InMemoryMessage { StatusCode = 200, Stream = stream }; + messageToRead.SetHeader("Content-Type", "application/json;odata.streaming=true"); + + // Enable reading custom instance annotations. + //messageToRead.PreferenceAppliedHeader().AnnotationFilter = "Custom.*"; + messageToRead.PreferenceAppliedHeader().AnnotationFilter = "*"; + + using (var messageReader = new ODataMessageReader(messageToRead, readerSettings, Model)) + { + var odataReader = messageReader.CreateODataResourceSetReader(EntitySet, EntityType); + while (odataReader.Read()) + { + switch (odataReader.State) + { + case ODataReaderState.NestedResourceInfoStart: + break; + case ODataReaderState.NestedResourceInfoEnd: + ODataNestedResourceInfo navigationLink = (ODataNestedResourceInfo)odataReader.Item; + + if (navigationLink.Name == "ResourceSetNavigationProperty") + { + Assert.Equal("ResourceSetNavigationProperty", navigationLink.Name); + Assert.Equal(10, navigationLink.Count); + Assert.Equal(2, navigationLink.InstanceAnnotations.Count); + Assert.Equal("custom.MyAnnotation", navigationLink.InstanceAnnotations.First().Name); + Assert.Equal("custom.StartFeedAnnotation", navigationLink.InstanceAnnotations.Last().Name); + Assert.Equal(true, navigationLink.IsCollection); + Assert.Equal(new Uri("http://example.com/multiple"), navigationLink.Url); + Assert.Equal(new Uri("http://www.example.com/service.svc/TestEntitySet(1)/ResourceSetNavigationProperty/$ref"), navigationLink.AssociationLinkUrl); + } + break; + } + } + } + } + [Fact] public void ShouldBeAbleToReadCustomInstanceAnnotationFromFeedAndEntryInJsonLight() { From 08498922a82bae98fb73b0eb537dbaf081c446a4 Mon Sep 17 00:00:00 2001 From: Kennedy Kangethe Date: Wed, 22 Sep 2021 16:53:20 +0300 Subject: [PATCH 4/5] Add validation and Refractor tests --- .../Microsoft.OData.Core.cs | 1 + .../Microsoft.OData.Core.txt | 1 + .../ODataNestedResourceInfo.cs | 19 +++++++++++++++++-- .../Parameterized.Microsoft.OData.Core.cs | 11 +++++++++++ ...nstanceAnnotationWriterIntegrationTests.cs | 8 +------- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs index c1176419e1..3d57394710 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs @@ -54,6 +54,7 @@ internal sealed class TextRes { internal const string ODataWriterCore_StreamNotDisposed = "ODataWriterCore_StreamNotDisposed"; internal const string ODataWriterCore_DeltaResourceWithoutIdOrKeyProperties = "ODataWriterCore_DeltaResourceWithoutIdOrKeyProperties"; internal const string ODataWriterCore_QueryCountInRequest = "ODataWriterCore_QueryCountInRequest"; + internal const string ODataWriterCore_QueryCountInODataNestedResourceInfo = "ODataWriterCore_QueryCountInODataNestedResourceInfo"; internal const string ODataWriterCore_QueryNextLinkInRequest = "ODataWriterCore_QueryNextLinkInRequest"; internal const string ODataWriterCore_QueryDeltaLinkInRequest = "ODataWriterCore_QueryDeltaLinkInRequest"; internal const string ODataWriterCore_CannotWriteDeltaWithResourceSetWriter = "ODataWriterCore_CannotWriteDeltaWithResourceSetWriter"; diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt index 61d00987a5..b415830e15 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt @@ -46,6 +46,7 @@ ODataWriterCore_StreamNotDisposed=ODataWriter.Write or ODataWriter.WriteEnd was ODataWriterCore_DeltaResourceWithoutIdOrKeyProperties=No Id or key properties were found. A resource in a delta payload requires an ID or key properties be specified. ODataWriterCore_QueryCountInRequest=The ODataResourceSet.Count must be null for request payloads. Query counts are only supported in responses. +ODataWriterCore_QueryCountInODataNestedResourceInfo=The ODataNestedResourceInfo.Count must be null when ODataNestedResourceInfo.IsCollection is false. ODataWriterCore_QueryNextLinkInRequest=The NextPageLink must be null for request payloads. Next page links are only supported in responses. ODataWriterCore_QueryDeltaLinkInRequest=The DeltaLink must be null for request payloads. Delta links are only supported in responses. ODataWriterCore_CannotWriteDeltaWithResourceSetWriter=Cannot write a deleted resource, link, deleted link, or nested delta resource set to a non-delta payload. Please use a delta resource set writer, or a request resource writer. diff --git a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs index a87f954b36..dd8b1c7e66 100644 --- a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs +++ b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs @@ -35,6 +35,9 @@ public sealed class ODataNestedResourceInfo : ODataItem /// true if the association link has been set by the user or seen on the wire or computed by the metadata builder, false otherwise. private bool hasAssociationUrl; + /// The number of items in the resource set. + private long count; + /// Gets or sets a value that indicates whether the nested resource info represents a collection or a resource. /// true if the nested resource info represents a collection; false if the navigation represents a resource. public bool? IsCollection @@ -98,8 +101,20 @@ public Uri AssociationLinkUrl /// The number of items in the resource set. public long? Count { - get; - set; + get + { + return this.count; + } + + set + { + if (IsCollection != null && (bool)IsCollection == false) + { + throw new ODataException(Strings.ODataWriterCore_QueryCountInODataNestedResourceInfo); + } + + this.count = (long)value; + } } /// diff --git a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs index acc2b304f7..5baa6bd7d9 100644 --- a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs @@ -304,6 +304,17 @@ internal static string ODataWriterCore_QueryCountInRequest } } + /// + /// A string like "The ODataNestedResourceInfo.Count must be null when ODataNestedResourceInfo.IsCollection is false." + /// + internal static string ODataWriterCore_QueryCountInODataNestedResourceInfo + { + get + { + return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ODataWriterCore_QueryCountInODataNestedResourceInfo); + } + } + /// /// A string like "The NextPageLink must be null for request payloads. Next page links are only supported in responses." /// diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs index 16835289f8..43466e370d 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs @@ -567,11 +567,6 @@ public void WriteAnnotationAndCountAtStartExpandedFeedShouldPassInJsonLight() "\"ResourceSetNavigationProperty@custom.StartFeedAnnotation\":123" + "}"; - this.WriteAnnotationAndCountAtStartExpandedFeedShouldPass(ODataFormat.Json, expectedPayload, EntitySet); - } - - private void WriteAnnotationAndCountAtStartExpandedFeedShouldPass(ODataFormat format, string expectedPayload, IEdmNavigationSource navigationSource) - { Action action = (odataWriter) => { var entryToWrite = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 1 } } }; @@ -586,9 +581,8 @@ private void WriteAnnotationAndCountAtStartExpandedFeedShouldPass(ODataFormat fo odataWriter.WriteEnd(); }; - this.WriteAnnotationsAndValidatePayload(action, navigationSource, format, expectedPayload, request: false, createFeedWriter: false); + this.WriteAnnotationsAndValidatePayload(action, EntitySet, ODataFormat.Json, expectedPayload, request: false, createFeedWriter: false); } - #endregion Writing instance annotations on expanded feeds #region Write Delta Feed From ea6f7f86c02c1acd00a8ef365ff99c063e8d5b4f Mon Sep 17 00:00:00 2001 From: Kennedy Kangethe Date: Wed, 22 Sep 2021 17:20:01 +0300 Subject: [PATCH 5/5] Add test --- .../ODataNestedResourceInfo.cs | 4 ++-- ...nstanceAnnotationWriterIntegrationTests.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs index dd8b1c7e66..bee3eb6e1f 100644 --- a/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs +++ b/src/Microsoft.OData.Core/ODataNestedResourceInfo.cs @@ -36,7 +36,7 @@ public sealed class ODataNestedResourceInfo : ODataItem private bool hasAssociationUrl; /// The number of items in the resource set. - private long count; + private long? count; /// Gets or sets a value that indicates whether the nested resource info represents a collection or a resource. /// true if the nested resource info represents a collection; false if the navigation represents a resource. @@ -113,7 +113,7 @@ public long? Count throw new ODataException(Strings.ODataWriterCore_QueryCountInODataNestedResourceInfo); } - this.count = (long)value; + this.count = value; } } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs index 43466e370d..968dd6d878 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Writer/InstanceAnnotationWriterIntegrationTests.cs @@ -583,6 +583,26 @@ public void WriteAnnotationAndCountAtStartExpandedFeedShouldPassInJsonLight() this.WriteAnnotationsAndValidatePayload(action, EntitySet, ODataFormat.Json, expectedPayload, request: false, createFeedWriter: false); } + + [Fact] + public void WriteCountOnExpandedEntryShouldFail() + { + Action action = (odataWriter) => + { + var entryToWrite = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 1 } } }; + odataWriter.WriteStart(entryToWrite); + + ODataNestedResourceInfo navLink = new ODataNestedResourceInfo { Name = "ResourceNavigationProperty", IsCollection = false }; + navLink.Count = 10; + + odataWriter.WriteStart(navLink); + odataWriter.WriteEnd(); + odataWriter.WriteEnd(); + }; + + Action testResponse = () => this.WriteAnnotationsAndValidatePayload(action, EntitySet, ODataFormat.Json, null, request: false, createFeedWriter: false); + testResponse.Throws(Strings.ODataWriterCore_QueryCountInODataNestedResourceInfo); + } #endregion Writing instance annotations on expanded feeds #region Write Delta Feed