From 7c1dba3e77aca91a32e15a954184afc807047fdc Mon Sep 17 00:00:00 2001 From: Biao Li Date: Thu, 8 Mar 2018 13:17:47 -0800 Subject: [PATCH] Various updates including: - Rebase to tip of ODL master branch. - Add e2e test ExpandedCustomerEntryOmitNullValuesTest with various cases exercising null values. - Updates restore null value processing related to base-type properties navigation properties by odata.navigationlink annotations. - Added output for comparison of omittedNullValues of true & false in same test cases. - Added test cases for unknonwn(dynamic) properties to verify that no content are restored. - Added test cases for expanded navigation link to verify that omitted properties are restored for non-null entity, and that no content are restored for null entity. --- .../ODataJsonLightPropertySerializer.cs | 19 +- .../JsonLight/ODataJsonLightReader.cs | 28 +- .../ODataMessageWriterSettings.cs | 1 + .../SelectedPropertiesNode.cs | 15 +- .../WriteJsonWithoutModelTests.cs | 139 ++++- .../WritePayloadHelper.cs | 31 +- ...AndValueJsonLightReaderIntegrationTests.cs | 482 ++++++++++++++---- .../ODataJsonLightDeserializerTests.cs | 55 +- .../SelectedPropertiesNodeTests.cs | 2 +- 9 files changed, 618 insertions(+), 154 deletions(-) diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs index 2ccc2cbbcd..c4c8e64fdc 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs @@ -181,6 +181,8 @@ private void WriteProperty( { WriterValidationUtils.ValidatePropertyNotNull(property); + ODataValue value = property.ODataValue; + string propertyName = property.Name; if (this.JsonLightOutputContext.MessageWriterSettings.Validations != ValidationKinds.None) @@ -197,6 +199,19 @@ private void WriteProperty( this.currentPropertyInfo = this.JsonLightOutputContext.PropertyCacheHandler.GetProperty(propertyName, owningType); } + // Optimization for null values: + // If no validation is required, we don't need property serialization info and could try to skip writing null property right away + // If this property is top-level, we cannot optimize here due to backward-compatibility requirement for OData-6.x. + // For very wide and sparse outputs it allows to avoid a lot of dictionary lookups + bool isNullValue = (value == null || value is ODataNullValue); + if (isNullValue && omitNullValues) + { + if (!this.currentPropertyInfo.IsTopLevel && !this.MessageWriterSettings.ThrowIfTypeConflictsWithMetadata) + { + return; + } + } + WriterValidationUtils.ValidatePropertyDefined(this.currentPropertyInfo, this.MessageWriterSettings.ThrowOnUndeclaredPropertyForNonOpenType); duplicatePropertyNameChecker.ValidatePropertyUniqueness(property); @@ -208,8 +223,6 @@ private void WriteProperty( WriteInstanceAnnotation(property, isTopLevel, currentPropertyInfo.MetadataType.IsUndeclaredProperty); - ODataValue value = property.ODataValue; - // handle ODataUntypedValue ODataUntypedValue untypedValue = value as ODataUntypedValue; if (untypedValue != null) @@ -233,7 +246,7 @@ private void WriteProperty( return; } - if (value is ODataNullValue || value == null) + if (isNullValue) { this.WriteNullProperty(property, omitNullValues); return; diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs index a8cd92527c..077cff9ba3 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs @@ -2284,26 +2284,29 @@ private void RestoreOmittedNullValues() ODataResourceBase resource = resourceState.Resource; IEnumerable selectedProperties; + if (resourceState.SelectedProperties == SelectedPropertiesNode.EntireSubtree) { - selectedProperties = edmStructuredType.DeclaredProperties; + selectedProperties = GetSelfAndBaseTypeProperties(edmStructuredType); } else - { // Partial subtree. Combine navigation properties and selected properties at the node with distinct. + { + // Partial subtree. Combine navigation properties and selected properties at the node with distinct. selectedProperties = resourceState.SelectedProperties.GetSelectedNavigationProperties(edmStructuredType) as IEnumerable; selectedProperties = selectedProperties.Concat( - resourceState.SelectedProperties.GetSelectedProperties(edmStructuredType).Values) + resourceState.SelectedProperties.GetSelectedProperties(edmStructuredType)) .Distinct(); } foreach (IEdmProperty currentProperty in selectedProperties) { Debug.Assert(currentProperty.Type != null, "currentProperty.Type != null"); - if (!currentProperty.Type.IsNullable) + if (!currentProperty.Type.IsNullable || currentProperty.PropertyKind == EdmPropertyKind.Navigation) { - // Skip declared properties that are not null-able types. + // Skip declared properties that are not null-able types, and declared navigation properties + // which are specified using odata.navigationlink annotations whose absence stand for null values. continue; } @@ -2344,6 +2347,21 @@ private void RestoreOmittedNullValues() } } + /// + /// Get all properties defined by the EDM structural type and its base types. + /// + /// The EDM structural type. + /// All the properties of this type. + private static IEnumerable GetSelfAndBaseTypeProperties(IEdmStructuredType edmStructuredType) + { + if (edmStructuredType == null) + { + return Enumerable.Empty(); + } + + return edmStructuredType.DeclaredProperties.Concat(GetSelfAndBaseTypeProperties(edmStructuredType.BaseType)); + } + /// /// Add info resolved from context url to current scope. /// diff --git a/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs b/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs index acefee6af5..a72122da25 100644 --- a/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs +++ b/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs @@ -414,6 +414,7 @@ private void CopyFrom(ODataMessageWriterSettings other) this.Version = other.Version; this.OmitNullValues = other.OmitNullValues; this.LibraryCompatibility = other.LibraryCompatibility; + this.validations = other.validations; this.ThrowIfTypeConflictsWithMetadata = other.ThrowIfTypeConflictsWithMetadata; this.ThrowOnDuplicatePropertyNames = other.ThrowOnDuplicatePropertyNames; diff --git a/src/Microsoft.OData.Core/SelectedPropertiesNode.cs b/src/Microsoft.OData.Core/SelectedPropertiesNode.cs index dbc37b072c..38878706d8 100644 --- a/src/Microsoft.OData.Core/SelectedPropertiesNode.cs +++ b/src/Microsoft.OData.Core/SelectedPropertiesNode.cs @@ -38,8 +38,8 @@ internal sealed class SelectedPropertiesNode /// An empty set of navigation properties to return when nothing is selected. private static readonly IEnumerable EmptyNavigationProperties = Enumerable.Empty(); - /// An empty dictionary of EDM properties to return when nothing is selected. - private static readonly Dictionary EmptyEdmProperties = new Dictionary(StringComparer.Ordinal); + /// An empty enumeration of EDM properties to return when nothing is selected. + private static readonly IEnumerable EmptyEdmProperties = Enumerable.Empty(); /// The type of the current node. private readonly SelectionType selectionType; @@ -343,7 +343,7 @@ internal IEnumerable GetSelectedNavigationProperties(IEd /// /// The current structured type. /// The selected properties at this node level. - internal IDictionary GetSelectedProperties(IEdmStructuredType structuredType) + internal IEnumerable GetSelectedProperties(IEdmStructuredType structuredType) { if (this.selectionType == SelectionType.Empty) { @@ -358,16 +358,13 @@ internal IDictionary GetSelectedProperties(IEdmStructuredT if (this.selectionType == SelectionType.EntireSubtree || this.hasWildcard) { - return structuredType.DeclaredProperties.ToDictionary(sp => sp.Name, StringComparer.Ordinal); + return structuredType.DeclaredProperties; } Debug.Assert(this.selectedProperties != null, "selectedProperties != null"); - IDictionary selectedEdmProperties = this.selectedProperties - .Select(structuredType.FindProperty) - .ToDictionary(p => p.Name, StringComparer.Ordinal); - - return selectedEdmProperties; + // Get declared properties selected, and filter out unrecognized properties. + return this.selectedProperties.Select(structuredType.FindProperty).OfType(); } /// diff --git a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs index 72cc1e0fbc..edb127832d 100644 --- a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs +++ b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs @@ -4,6 +4,8 @@ // //--------------------------------------------------------------------- +using System.Runtime.CompilerServices; + namespace Microsoft.Test.OData.Tests.Client.WriteJsonPayloadTests { using System; @@ -145,6 +147,36 @@ public void ExpandedCustomerEntryTest() } } + /// + /// Write and read an expanded customer entry containing primitive, complex, collection of primitive/complex properties with null values + /// using omit-value=nulls Preference header. + /// + [TestMethod] + public void ExpandedCustomerEntryOmitNullValuesTest() + { + // Repeat with full and minimal metadata. + foreach (string mimeType in this.mimeTypes.GetRange(0, 2)) + { + var settings = new ODataMessageWriterSettings(); + settings.ODataUri = new ODataUri() {ServiceRoot = this.ServiceUri}; + string rspRecvd = null; + + var responseMessageWithModel = new StreamResponseMessage(new MemoryStream()); + responseMessageWithModel.SetHeader("Content-Type", mimeType); + responseMessageWithModel.PreferenceAppliedHeader().OmitValues = ODataConstants.OmitValuesNulls; + + using (var messageWriter = new ODataMessageWriter(responseMessageWithModel, settings, + WritePayloadHelper.Model)) + { + var odataWriter = messageWriter.CreateODataResourceWriter(WritePayloadHelper.CustomerSet, + WritePayloadHelper.CustomerType); + rspRecvd = this.WriteAndVerifyExpandedCustomerEntryWithSomeNullValues( + responseMessageWithModel, odataWriter, /* hasModel */false); + Assert.IsNotNull(rspRecvd); + } + } + } + /// /// Write an entry containing stream, named stream /// @@ -519,10 +551,9 @@ private string WriteAndVerifyOrderFeed(StreamResponseMessage responseMessage, OD return WritePayloadHelper.ReadStreamContent(stream); } - private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage responseMessage, - ODataWriter odataWriter, bool hasModel, string mimeType) + private void WriteCustomerEntry( ODataWriter odataWriter, bool hasModel, bool withSomeNullValues) { - ODataResourceWrapper customerEntry = WritePayloadHelper.CreateCustomerEntry(hasModel); + ODataResourceWrapper customerEntry = WritePayloadHelper.CreateCustomerEntry(hasModel, withSomeNullValues); var loginFeed = new ODataResourceSet() { Id = new Uri(this.ServiceUri + "Customer(-9)/Logins") }; if (!hasModel) @@ -533,30 +564,41 @@ private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage respons var loginEntry = WritePayloadHelper.CreateLoginEntry(hasModel); - customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat(WritePayloadHelper.CreateCustomerNavigationLinks()); - customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat(new[]{ new ODataNestedResourceInfoWrapper() - { - NestedResourceInfo = new ODataNestedResourceInfo() - { - Name = "Logins", - IsCollection = true, - Url = new Uri(this.ServiceUri + "Customer(-9)/Logins") - }, - NestedResourceOrResourceSet = new ODataResourceSetWrapper() - { - ResourceSet = loginFeed, - Resources = new List() - { - new ODataResourceWrapper() + // Setting null values for some navigation links, those null links won't be serialized + // as odata.navigationlink annotations. + customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat( + WritePayloadHelper.CreateCustomerNavigationLinks(withSomeNullValues)); + customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat( + new[]{ new ODataNestedResourceInfoWrapper() { - Resource = loginEntry, - NestedResourceInfoWrappers = WritePayloadHelper.CreateLoginNavigationLinksWrapper().ToList() + NestedResourceInfo = new ODataNestedResourceInfo() + { + Name = "Logins", + IsCollection = true, + Url = new Uri(this.ServiceUri + "Customer(-9)/Logins") + }, + NestedResourceOrResourceSet = new ODataResourceSetWrapper() + { + ResourceSet = loginFeed, + Resources = new List() + { + new ODataResourceWrapper() + { + Resource = loginEntry, + NestedResourceInfoWrappers = WritePayloadHelper.CreateLoginNavigationLinksWrapper(withSomeNullValues).ToList() + } + } + } } - } - } - }}); + }); ODataWriterHelper.WriteResource(odataWriter, customerEntry); + } + + private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage responseMessage, + ODataWriter odataWriter, bool hasModel, string mimeType) + { + WriteCustomerEntry(odataWriter, hasModel, false); // Some very basic verification for the payload. bool verifyFeedCalled = false; @@ -601,6 +643,57 @@ private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage respons return WritePayloadHelper.ReadStreamContent(stream); } + private string WriteAndVerifyExpandedCustomerEntryWithSomeNullValues(StreamResponseMessage responseMessage, + ODataWriter odataWriter, bool hasModel) + { + WriteCustomerEntry(odataWriter, hasModel, /* withSomeNullValues */true); + + // Some very basic verification for the payload. + bool verifyFeedCalled = false; + int verifyEntryCalled = 0; + int verifyNullValuesCount = 0; + bool verifyNavigationCalled = false; + Action verifyFeed = (feed) => + { + verifyFeedCalled = true; + }; + + Action verifyEntry = (entry) => + { + if (entry.TypeName.Contains("Customer")) + { + Assert.AreEqual(4, entry.Properties.Count()); + verifyEntryCalled++; + } + + if (entry.TypeName.Contains("Login")) + { + Assert.AreEqual(2, entry.Properties.Count()); + verifyEntryCalled++; + } + + // Counting restored null property value during response de-serialization. + verifyNullValuesCount += entry.Properties.Count(p => p.Value == null); + }; + + Action verifyNavigation = (navigation) => + { + Assert.IsNotNull(navigation.Name); + verifyNavigationCalled = true; + }; + + Stream stream = responseMessage.GetStream(); + stream.Seek(0, SeekOrigin.Begin); + WritePayloadHelper.ReadAndVerifyFeedEntryMessage(false, responseMessage, WritePayloadHelper.CustomerSet, WritePayloadHelper.CustomerType, + verifyFeed, verifyEntry, verifyNavigation); + Assert.IsTrue(verifyFeedCalled); + Assert.IsTrue(verifyEntryCalled == 2); + Assert.IsTrue(verifyNullValuesCount == 4); + Assert.IsTrue(verifyNavigationCalled); + + return WritePayloadHelper.ReadStreamContent(stream); + } + private string WriteAndVerifyCarEntry(StreamResponseMessage responseMessage, ODataWriter odataWriter, bool hasModel, string mimeType) { diff --git a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs index b6aeb4bce0..e832e50405 100644 --- a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs +++ b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs @@ -4,6 +4,8 @@ // //--------------------------------------------------------------------- +using System.Diagnostics; + namespace Microsoft.Test.OData.Tests.Client.WriteJsonPayloadTests { using System; @@ -594,9 +596,9 @@ public static ODataResourceWrapper CreateOrderEntry2(bool hasModel) #region ExpandedCustomerEntryTestHelper - public static ODataResourceWrapper CreateCustomerEntry(bool hasModel) + public static ODataResourceWrapper CreateCustomerEntry(bool hasModel, bool withSomeNullValues = false) { - var customerEntryWrapper = CreateCustomerResourceWrapperNoMetadata(hasModel); + var customerEntryWrapper = CreateCustomerResourceWrapperNoMetadata(hasModel, withSomeNullValues); var customerEntry = customerEntryWrapper.Resource; customerEntry.Id = new Uri(ServiceUri + "Customer(-9)"); @@ -1079,7 +1081,7 @@ public static ODataResourceWrapper CreateAuditInforWrapper() }; } - public static IEnumerable CreateCustomerNavigationLinks() + public static IEnumerable CreateCustomerNavigationLinks(bool withSomeNullValues) { return new List() { @@ -1098,7 +1100,9 @@ public static IEnumerable CreateCustomerNavigati { Name = "Husband", IsCollection = false, - Url = new Uri(ServiceUri + "Customer(-9)/Husband") + Url = withSomeNullValues + ? null + : new Uri(ServiceUri + "Customer(-9)/Husband") } }, new ODataNestedResourceInfoWrapper() @@ -1107,7 +1111,9 @@ public static IEnumerable CreateCustomerNavigati { Name = "Wife", IsCollection = false, - Url = new Uri(ServiceUri + "Customer(-9)/Wife") + Url = withSomeNullValues + ? null + : new Uri(ServiceUri + "Customer(-9)/Wife") } }, new ODataNestedResourceInfoWrapper() @@ -1131,7 +1137,7 @@ public static ODataResource CreateLoginEntry(bool hasModel) return loginEntry; } - public static IEnumerable CreateLoginNavigationLinksWrapper() + public static IEnumerable CreateLoginNavigationLinksWrapper(bool withSomeNullValues) { return new List() { @@ -1150,7 +1156,9 @@ public static IEnumerable CreateLoginNavigationL { Name = "LastLogin", IsCollection = false, - Url = new Uri(ServiceUri + "Login('2')/LastLogin") + Url = withSomeNullValues + ? null + : new Uri(ServiceUri + "Login('2')/LastLogin") } }, new ODataNestedResourceInfoWrapper() @@ -1159,7 +1167,9 @@ public static IEnumerable CreateLoginNavigationL { Name = "SentMessages", IsCollection = true, - Url = new Uri(ServiceUri + "Login('2')/SentMessages") + Url = withSomeNullValues + ? null + : new Uri(ServiceUri + "Login('2')/SentMessages") } }, new ODataNestedResourceInfoWrapper() @@ -1386,7 +1396,7 @@ public static ODataNestedResourceInfo AddOrderEntryCustomNavigation(ODataResourc return orderEntry2Navigation; } - public static ODataResourceWrapper CreateCustomerResourceWrapperNoMetadata(bool hasModel) + public static ODataResourceWrapper CreateCustomerResourceWrapperNoMetadata(bool hasModel, bool withSomeNullValues = false) { var customerEntry = new ODataResource() { @@ -1394,7 +1404,8 @@ public static ODataResourceWrapper CreateCustomerResourceWrapperNoMetadata(bool }; var customerEntryP1 = new ODataProperty { Name = "CustomerId", Value = -9 }; - var customerEntryP2 = new ODataProperty { Name = "Name", Value = "CustomerName" }; + string aCustomerName = withSomeNullValues ? null : "CustomerName"; + var customerEntryP2 = new ODataProperty {Name = "Name", Value = aCustomerName }; var primaryContactInfo_nestedInfoWrapper = new ODataNestedResourceInfoWrapper() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs index d6f934ff14..1fa92ed8cc 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using FluentAssertions; using Microsoft.OData.Tests.JsonLight; @@ -367,24 +368,26 @@ public void ReadingTypeDefinitionPayloadJsonLight() address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.OrdinalIgnoreCase)).Value.Should().Be("China"); } - [Fact] - public void ReadingTypeDefinitionPayloadJsonLightWithOmittedNullValues() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingTypeDefinitionPayloadJsonLightWithOmittedNullValues(bool bNullValuesOmitted) { EdmEntityType entityType; EdmEntitySet entitySet; EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet); const string payload = @" -{ + { ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People/$entity"", ""@odata.id"":""http://mytest"", ""Id"":0, ""Weight"":60.5, ""Address"":{""CountryRegion"":""US"", ""City"":""Redmond""} -}"; + }"; List entries = new List(); - ODataNestedResourceInfo navigationLink = null; + List nestedResourceInfos = new List(); this.ReadEntryPayload(model, payload, entitySet, entityType, reader => { @@ -394,13 +397,13 @@ public void ReadingTypeDefinitionPayloadJsonLightWithOmittedNullValues() entries.Add(reader.Item as ODataResource); break; case ODataReaderState.NestedResourceInfoStart: - navigationLink = (ODataNestedResourceInfo)reader.Item; + nestedResourceInfos.Add((ODataNestedResourceInfo)reader.Item); break; default: break; } }, - nullValuesOmitted: true); + nullValuesOmitted: bNullValuesOmitted); Assert.Equal(2, entries.Count); @@ -409,20 +412,38 @@ public void ReadingTypeDefinitionPayloadJsonLightWithOmittedNullValues() .Value.Should().Be(0); person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Weight", StringComparison.Ordinal)) .Value.Should().Be(60.5); - // omitted value should be restored to null. - person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) - .Value.Should().BeNull(); + if (bNullValuesOmitted) + { + // Omitted value should be restored to null. + person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + person.Properties.Any(s => string.Equals(s.Name, "Education",StringComparison.Ordinal)).Should().BeFalse(); + } ODataResource address = entries[1]; address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.Ordinal)) .Value.Should().Be("US"); address.Properties.FirstOrDefault(s => string.Equals(s.Name, "City", StringComparison.Ordinal)) .Value.Should().Be("Redmond"); - // Omitted value should be restored to null. - address.Properties.FirstOrDefault(s => string.Equals(s.Name, "ZipCode", StringComparison.Ordinal)) - .Value.Should().BeNull(); - navigationLink.Name.Should().Be("Address"); + if (bNullValuesOmitted) + { + // Omitted value should be restored to null. + address.Properties.FirstOrDefault(s => string.Equals(s.Name, "ZipCode", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + address.Properties.Any(s => string.Equals(s.Name, "ZipCode", StringComparison.Ordinal)).Should().BeFalse(); + } + + nestedResourceInfos.Count().Should().Be(2); + nestedResourceInfos[0].Name.Should().Be("Address"); + // Navigation link missing from payload. + nestedResourceInfos[1].Name.Should().Be("Company"); } [Fact] @@ -845,8 +866,11 @@ public void ReadingNullValueForDeclaredCollectionPropertyInComplexTypeShouldFail read.ShouldThrow().WithMessage("A null value was found for the property named 'CountriesOrRegions', which has the expected type 'Collection(Edm.String)[Nullable=False]'. The expected type 'Collection(Edm.String)[Nullable=False]' does not allow null values."); } - [Fact] - public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValues() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValues( + bool bNullValuesOmitted) { EdmEntityType entityType; EdmEntitySet entitySet; @@ -872,29 +896,55 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu break; } }, - nullValuesOmitted: true); + nullValuesOmitted: bNullValuesOmitted); Assert.Equal(2, entries.Count); ODataResource edu = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal)); edu.Should().NotBeNull(); edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)).Value.Should().Be(1); - // null-able collection should be restored. - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Value.Should().BeNull(); - // null-able property value should be restored. - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Value.Should().BeNull(); + + if (bNullValuesOmitted) + { + // null-able collection should be restored. + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Value.Should().BeNull(); + // null-able property value should be restored. + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Value.Should().BeNull(); + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)).Value.Should().BeNull(); + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgCategory", StringComparison.Ordinal)).Value.Should().BeNull(); + + } + else + { + edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); + edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse(); + edu.Properties.Any(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)).Should().BeFalse(); + edu.Properties.Any(s => string.Equals(s.Name, "OrgCategory", StringComparison.Ordinal)).Should().BeFalse(); + } ODataResource person = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal)); person.Should().NotBeNull(); person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)).Value.Should().Be(0); - // omitted null-able property should be restored as null. - person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Value.Should().BeNull(); + + if (bNullValuesOmitted) + { + // omitted null-able property should be restored as null. + person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Value.Should().BeNull(); + } + else + { + person.Properties.Any(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Should().BeFalse(); + } + // missing non-null-able property should not be restored. person.Properties.Any(s => string.Equals(s.Name, "Weight", StringComparison.Ordinal)).Should().BeFalse(); } - [Fact] - public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesEntireSubTree() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesEntireSubTree( + bool bNullValuesOmitted) { EdmEntityType entityType; EdmEntitySet entitySet; @@ -904,13 +954,13 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu // null-able property Address is selected, thus should be restored. // Property Education is null-able. const string payloadWithQueryOption = @"{ - ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education,Address)/$entity"", + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,UnknownPropX,Education,Address)/$entity"", ""@odata.id"":""http://mytest"", ""Id"":0, ""Education"":{""Id"":1} }"; const string payloadWithWildcardInQueryOption = @"{ - ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/*,Address)/$entity"", + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,UnknownPropX,Education/*,Address)/$entity"", ""@odata.id"":""http://mytest"", ""Id"":0, ""Education"":{""Id"":1} @@ -919,7 +969,7 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu foreach (string payload in new string[] {payloadWithQueryOption, payloadWithWildcardInQueryOption}) { List entries = new List(); - List navigationLinks = new List(); + List nestedResourceInfos = new List(); this.ReadEntryPayload(model, payload, entitySet, entityType, reader => { @@ -929,17 +979,17 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu entries.Add(reader.Item as ODataResource); break; case ODataReaderState.NestedResourceInfoStart: - navigationLinks.Add(reader.Item as ODataNestedResourceInfo); + nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo); break; default: break; } }, - nullValuesOmitted: true); + nullValuesOmitted: bNullValuesOmitted); entries.Count.Should().Be(2); - navigationLinks.Count.Should().Be(1); - navigationLinks.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) + nestedResourceInfos.Count.Should().Be(1); + nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) .Should().NotBeNull(); // Education @@ -948,30 +998,54 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu edu.Should().NotBeNull(); edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) .Value.Should().Be(1); - // null-able collection should be restored. - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)) - .Value.Should().BeNull(); - // null-able property value should be restored. - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)) - .Value.Should().BeNull(); + if (bNullValuesOmitted) + { + // null-able collection should be restored. + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)) + .Value.Should().BeNull(); + // null-able property value should be restored. + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); + edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse(); + } // Person ODataResource person = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal)); person.Should().NotBeNull(); + + // Verify that unknown property doesn't cause anomaly and is not restored. + person.Properties.Any(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal)).Should().BeFalse(); + person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) .Value.Should().Be(0); - // selected null-able property/navigationLink should be restored as null. - person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Address", StringComparison.Ordinal)) - .Value.Should().BeNull(); + + if (bNullValuesOmitted) + { + // selected null-able property should be restored as null. + person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Address", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + person.Properties.Any(s => string.Equals(s.Name, "Address", StringComparison.Ordinal)).Should().BeFalse(); + } + // null-able but not selected properties and not-null-able properties should not be restored. person.Properties.Any(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Should().BeFalse(); person.Properties.Any(s => string.Equals(s.Name, "Weight", StringComparison.Ordinal)).Should().BeFalse(); } } - [Fact] - public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTree() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTree( + bool bNullValuesOmitted) { EdmEntityType entityType; EdmEntitySet entitySet; @@ -980,74 +1054,260 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu // null-able property Height is not selected, thus should not be restored. // null-able property Address is selected, thus should be restored. // Property Education is null-able. - const string payloadWithWildcardInQueryOption = @"{ - ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/SchoolName,%20Address)/$entity"", + const string payloadWithSelectedPropertiesPartialSubTreeInQueryOption = @"{ + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/SchoolName,%20Education/UnknownPropX,%20Address)/$entity"", ""@odata.id"":""http://mytest"", ""Id"":0, ""Education"":{""Id"":1} }"; - foreach (string payload in new string[] { payloadWithWildcardInQueryOption }) - { - List entries = new List(); - List navigationLinks = new List(); - this.ReadEntryPayload(model, payload, entitySet, entityType, - reader => + List entries = new List(); + List nestedResourceInfos = new List(); + this.ReadEntryPayload(model, payloadWithSelectedPropertiesPartialSubTreeInQueryOption, entitySet, entityType, + reader => + { + switch (reader.State) { - switch (reader.State) - { - case ODataReaderState.ResourceStart: - entries.Add(reader.Item as ODataResource); - break; - case ODataReaderState.NestedResourceInfoStart: - navigationLinks.Add(reader.Item as ODataNestedResourceInfo); - break; - default: - break; - } - }, - nullValuesOmitted: true); + case ODataReaderState.ResourceStart: + entries.Add(reader.Item as ODataResource); + break; + case ODataReaderState.NestedResourceInfoStart: + nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo); + break; + default: + break; + } + }, + nullValuesOmitted: bNullValuesOmitted); - entries.Count.Should().Be(2); - navigationLinks.Count.Should().Be(1); - navigationLinks.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) - .Should().NotBeNull(); + entries.Count.Should().Be(2); + nestedResourceInfos.Count.Should().Be(1); + nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) + .Should().NotBeNull(); - // Education - ODataResource edu = - entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal)); - edu.Should().NotBeNull(); - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) - .Value.Should().Be(1); + // Education + ODataResource edu = + entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal)); + edu.Should().NotBeNull(); + edu.Properties.Any(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal)).Should().BeFalse(); + + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) + .Value.Should().Be(1); + if (bNullValuesOmitted) + { // null-able property value should be restored. edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)) .Value.Should().BeNull(); - // not selected property should not be restored. - edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); } + else + { + edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse(); + } + + // not selected property should not be restored. + edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); } - [Fact] - public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTreeAndBaseTypeProperty() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTreeAndBaseTypeProperty( + bool bNullValuesOmitted) { EdmEntityType entityType; EdmEntitySet entitySet; EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet); - // null-able property Height is not selected, thus should not be restored. // null-able property Address is selected, thus should be restored. // Property Education is null-able. const string payloadWithWildcardInQueryOption = @"{ - ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/OrgId,%20Address)/$entity"", - ""@odata.id"":""http://mytest"", + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/OrgId,%20Address)/$entity"", + ""@odata.id"":""http://mytest"", + ""Id"":0, + ""Education"":{""Id"":1} + }"; + + List entries = new List(); + List nestedResourceInfos = new List(); + this.ReadEntryPayload(model, payloadWithWildcardInQueryOption, entitySet, entityType, + reader => + { + switch (reader.State) + { + case ODataReaderState.ResourceStart: + entries.Add(reader.Item as ODataResource); + break; + case ODataReaderState.NestedResourceInfoStart: + nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo); + break; + default: + break; + } + }, + nullValuesOmitted: bNullValuesOmitted); + + entries.Count.Should().Be(2); + nestedResourceInfos.Count.Should().Be(1); + nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) + .Should().NotBeNull(); + + // Education + ODataResource edu = + entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal)); + edu.Should().NotBeNull(); + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) + .Value.Should().Be(1); + + // selected null-able property from base type should be restored to null if omitted. + if (bNullValuesOmitted) + { + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + edu.Properties.Any(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)).Should().BeFalse(); + } + + // not selected property should not be restored. + edu.Properties.Any(s => string.Equals(s.Name, "OrgCategory", StringComparison.Ordinal)).Should().BeFalse(); + edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse(); + edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingWithSelectExpandClauseShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTree( + bool bNullValuesOmitted) + { + EdmEntityType entityType; + EdmEntitySet entitySet; + EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet); + + // null-able property Height is not selected, thus should not be restored. + // Property Education is null-able. + const string payloadWithSelectExpandClauseInQueryOption = @"{ + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/Id,Education/SchoolName,Company/Id,Company/Name)/$entity"", + ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(0)"", + ""Id"":0, + ""Education"":{""Id"":1}, + ""Company"":{ + ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.Companies(11)"", + ""Id"":11} + }"; + + List entries = new List(); + List nestedResourceInfos = new List(); + this.ReadEntryPayload(model, payloadWithSelectExpandClauseInQueryOption, entitySet, entityType, + reader => + { + switch (reader.State) + { + case ODataReaderState.ResourceStart: + entries.Add(reader.Item as ODataResource); + break; + case ODataReaderState.NestedResourceInfoStart: + nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo); + break; + default: + break; + } + }, + nullValuesOmitted: bNullValuesOmitted); + + nestedResourceInfos.Count.Should().Be(2); + ODataNestedResourceInfo eduNestedResouce = + nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)); + + // Edu is complex type, its Url should be null. + eduNestedResouce.Should().NotBeNull(); + eduNestedResouce.Url.Should().BeNull(); + // Navigation link for Company entity should be non-null. + ODataNestedResourceInfo companyLink = + nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Company", StringComparison.Ordinal)); + companyLink.Should().NotBeNull(); + companyLink.Url.Should().NotBeNull(); + + entries.Count.Should().Be(3); + // Education + ODataResource edu = + entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal)); + edu.Should().NotBeNull(); + + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) + .Value.Should().Be(1); + if (bNullValuesOmitted) + { + // null-able property value should be restored. + edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse(); + } + // not selected property should not be restored. + edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); + + // Company + ODataResource company = + entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Cmpny", StringComparison.Ordinal)); + company.Should().NotBeNull(); + company.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) + .Value.Should().Be(11); + if (bNullValuesOmitted) + { + company.Properties.FirstOrDefault(s => string.Equals(s.Name, "Name", StringComparison.Ordinal)) + .Value.Should().BeNull(); + } + else + { + company.Properties.Any(s => string.Equals(s.Name, "Name", StringComparison.Ordinal)).Should().BeFalse(); + } + + // Person + ODataResource person = + entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal)); + person.Should().NotBeNull(); + person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) + .Value.Should().Be(0); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadingWithSelectExpandClauseShouldHaveNoContentForNullNavigationProperty( + bool bNullValuesOmitted) + { + EdmEntityType entityType; + EdmEntitySet entitySet; + EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet); + + // Context URL with subtree of navigation entity selected. + const string payloadWithSelectExpandClauseInQueryOption1 = @"{ + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/Id,Education/SchoolName,Company/Id,Company/Name)/$entity"", + ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(0)"", + ""Id"":0, + ""Education"":{""Id"":1} + }"; + + // Context URL with entire navigation entity selected. + const string payloadWithSelectExpandClauseInQueryOption2 = @"{ + ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/Id,Education/SchoolName,Company)/$entity"", + ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(0)"", ""Id"":0, ""Education"":{""Id"":1} }"; - foreach (string payload in new string[] { payloadWithWildcardInQueryOption }) + foreach (string payload in new string[] + { + payloadWithSelectExpandClauseInQueryOption1, + payloadWithSelectExpandClauseInQueryOption2 + }) { List entries = new List(); - List navigationLinks = new List(); + List nestedResourceInfos = new List(); this.ReadEntryPayload(model, payload, entitySet, entityType, reader => { @@ -1057,31 +1317,27 @@ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValu entries.Add(reader.Item as ODataResource); break; case ODataReaderState.NestedResourceInfoStart: - navigationLinks.Add(reader.Item as ODataNestedResourceInfo); + nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo); break; default: break; } }, - nullValuesOmitted: true); + nullValuesOmitted: bNullValuesOmitted); entries.Count.Should().Be(2); - navigationLinks.Count.Should().Be(1); - navigationLinks.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal)) - .Should().NotBeNull(); - - // Education - ODataResource edu = - entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal)); - edu.Should().NotBeNull(); - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)) - .Value.Should().Be(1); - // selected null-able property from base type should be restored to null if omitted. - edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)) - .Value.Should().BeNull(); - // not selected property should not be restored. - edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse(); - edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse(); + // Expanded Company entity via null navigation link is not restored. + // see example: http://services.odata.org/V4/TripPinService/People('ronaldmundy')?$expand=Photo + entries.Any(s => string.Equals(s.TypeName, "NS.Cmpny", StringComparison.Ordinal)).Should().BeFalse(); + + nestedResourceInfos.Count.Should().Be(2); + // There should be a navigation link reported as missing from payload. + ODataNestedResourceInfo companyLink = + nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Company", StringComparison.Ordinal)); + companyLink.Should().NotBeNull(); + // Reported missing/un-processed navigation link has URL constructed from metadata and + // @odata.id annotation of navigation source. + companyLink.Url.Should().NotBeNull(); } } @@ -1186,7 +1442,7 @@ private EdmModel BuildEdmModelForOmittedNullValuesTestCases(out EdmEntityType en EdmModel model = new EdmModel(); entityType = new EdmEntityType("NS", "Person"); - entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32); + entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); EdmTypeDefinition weightType = new EdmTypeDefinition("NS", "Wgt", EdmPrimitiveTypeKind.Double); EdmTypeDefinitionReference weightTypeRef = new EdmTypeDefinitionReference(weightType, false); @@ -1230,13 +1486,35 @@ private EdmModel BuildEdmModelForOmittedNullValuesTestCases(out EdmEntityType en entityType.AddStructuralProperty("Education", educationTypeRef); + // Company entity type. + EdmEntityType companyEntityType = new EdmEntityType("NS", "Cmpny"); + entityType.AddKeys(companyEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + companyEntityType.AddStructuralProperty("Name", + new EdmTypeDefinitionReference( + new EdmTypeDefinition("NS", "CmpyName", EdmPrimitiveTypeKind.String), + true)); + companyEntityType.AddStructuralProperty("Address", addressTypeRef); + + EdmNavigationPropertyInfo companyNav = new EdmNavigationPropertyInfo() + { + Name = "Company", + ContainsTarget = true, + Target = companyEntityType, + TargetMultiplicity = EdmMultiplicity.ZeroOrOne + }; + + // Add navigation link from Person to Company. + entityType.AddUnidirectionalNavigation(companyNav); + model.AddElement(weightType); model.AddElement(heightType); model.AddElement(addressType); model.AddElement(entityType); + model.AddElement(companyEntityType); EdmEntityContainer container = new EdmEntityContainer("EntityNs", "MyContainer"); entitySet = container.AddEntitySet("People", entityType); + container.AddEntitySet("Companies", companyEntityType); model.AddElement(container); return model; diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs index b99f2cf566..f8947eed50 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs @@ -13,6 +13,7 @@ using Microsoft.OData.Json; using Microsoft.OData.JsonLight; using Microsoft.OData.Edm; +using Microsoft.Test.OData.Utils.ODataLibTest; using Xunit; using ErrorStrings = Microsoft.OData.Strings; @@ -867,6 +868,58 @@ public void ParsingInstanceAnnotationsInTopLevelPropertyShouldSkipBaseOnSettings property.InstanceAnnotations.Count.Should().Be(0); } + [Fact] + public void ParsingPrimitiveTypePropertyInsideComplextPropertyShouldSucceed() + { + EdmModel model = this.CreateEdmModelWithEntity(); + + // Add complex type to model. + EdmComplexType complexType = new EdmComplexType("TestNamespace", "Addr"); + complexType.AddStructuralProperty("CountryRegion", EdmPrimitiveTypeKind.String); + IEdmComplexTypeReference complexTypeReference = new EdmComplexTypeReference(complexType, true); + + EdmEntityType entityType = model.GetEntityType("TestNamespace.Customer"); + entityType.AddStructuralProperty("Address", complexTypeReference); + + IEdmTypeReference primitiveTypeRef = + ((IEdmComplexTypeReference) (entityType.GetProperty("Address").Type)).FindProperty( + "CountryRegion").Type; + + string payload = @"{ + ""@odata.context"":""http://odata.org/test/$metadata#Customers(1)/Address/CountryRegion"", + ""value"":""US"" + }"; + + ODataJsonLightPropertyAndValueDeserializer deserializer = new ODataJsonLightPropertyAndValueDeserializer(this.CreateJsonLightInputContext(payload, model)); + ODataProperty property = deserializer.ReadTopLevelProperty(primitiveTypeRef); + property.Value.Should().Be("US"); + } + + [Fact] + public void ParsingTopLevelComplextPropertyShouldFail() + { + EdmModel model = this.CreateEdmModelWithEntity(); + + // Add complex type to model. + EdmComplexType complexType = new EdmComplexType("TestNamespace", "Addr"); + complexType.AddStructuralProperty("CountryRegion", EdmPrimitiveTypeKind.String); + IEdmComplexTypeReference complexTypeReference = new EdmComplexTypeReference(complexType, true); + + EdmEntityType entityType = model.GetEntityType("TestNamespace.Customer"); + entityType.AddStructuralProperty("Address", complexTypeReference); + + string payload = @"{ + ""@odata.context"":""http://odata.org/test/$metadata#Customers(1)/Address"", + ""value"":{""CountryRegion"":""US""} + }"; + + ODataJsonLightPropertyAndValueDeserializer deserializer = new ODataJsonLightPropertyAndValueDeserializer(this.CreateJsonLightInputContext(payload, model)); + + // Currently using de-serializer to read top-level complex property directly is not supported. Need to use ODataReader.Read API instead. + Action action = () => deserializer.ReadTopLevelProperty(complexTypeReference); + action.ShouldThrow().WithMessage( + "An internal error 'ODataJsonLightPropertyAndValueDeserializer_ReadPropertyValue' occurred."); + } #endregion #region Complex properties instance annotation @@ -1078,7 +1131,7 @@ public void ParsingExpectedComplexPropertyActualNotShouldThrow() var complexTypeRef = new EdmComplexTypeReference(complexType, false); var odataReader = this.CreateJsonLightInputContext("\"CountryRegion\":\"China\"", model, false) .CreateResourceReader(null, complexType); - Action action = () => + Action action = () => { while (odataReader.Read()) { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs index 74cd149ff1..86f38c4953 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs @@ -561,7 +561,7 @@ internal SelectedPropertiesNodeAssertions(SelectedPropertiesNode node) : base(no internal AndConstraint HaveProperties(IEdmEntityType entityType, params string[] propertyNames) { - this.Subject.As().GetSelectedProperties(entityType).Keys.Should().BeEquivalentTo(propertyNames); + this.Subject.As().GetSelectedProperties(entityType).Select(p => p.Name).Should().BeEquivalentTo(propertyNames); return new AndConstraint(this); }