diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs index 8e53425613..e5d87b4052 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs @@ -374,7 +374,7 @@ private void WriteNullProperty( // TODO: Enable updating top-level properties to null #645 throw new ODataException("A null top-level property is not allowed to be serialized."); } - else + else if (!this.MessageWriterSettings.IgnoreNullValues) { this.JsonWriter.WriteName(property.Name); this.JsonLightValueSerializer.WriteNullValue(); diff --git a/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs b/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs index 731e19399d..51332a093b 100644 --- a/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs +++ b/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs @@ -177,6 +177,14 @@ public ODataUri ODataUri /// internal bool ThrowOnDuplicatePropertyNames { get; private set; } + /// + /// Don't serialize null values + /// + /// + /// Default valus is false, that means serialize null values. + /// + public bool IgnoreNullValues { get; set; } + /// /// Returns whether ThrowOnUndeclaredPropertyForNonOpenType validation setting is enabled. /// @@ -398,6 +406,7 @@ private void CopyFrom(ODataMessageWriterSettings other) this.shouldIncludeAnnotation = other.shouldIncludeAnnotation; this.useFormat = other.useFormat; this.Version = other.Version; + this.IgnoreNullValues = other.IgnoreNullValues; this.validations = other.validations; this.ThrowIfTypeConflictsWithMetadata = other.ThrowIfTypeConflictsWithMetadata; diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl index e10cb0b6c9..a0964e9b11 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl @@ -4927,6 +4927,7 @@ public sealed class Microsoft.OData.ODataMessageWriterSettings { System.Uri BaseUri { public get; public set; } bool EnableCharactersCheck { public get; public set; } bool EnableMessageStreamDisposal { public get; public set; } + bool IgnoreNullValues { public get; public set; } string JsonPCallback { public get; public set; } Microsoft.OData.ODataMessageQuotas MessageQuotas { public get; public set; } Microsoft.OData.ODataUri ODataUri { public get; public set; } diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.IgnoreNullPropertiesInEntryTest.approved.txt b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.IgnoreNullPropertiesInEntryTest.approved.txt new file mode 100644 index 0000000000..3578c7bcee --- /dev/null +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.IgnoreNullPropertiesInEntryTest.approved.txt @@ -0,0 +1,32 @@ +Combination: 1; TestConfiguration = Format: JsonLight, Request: True, Synchronous: True +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"} + +Combination: 2; TestConfiguration = Format: JsonLight, Request: False, Synchronous: True +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"} + +Combination: 3; TestConfiguration = Format: JsonLight, Request: True, Synchronous: False +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"} + +Combination: 4; TestConfiguration = Format: JsonLight, Request: False, Synchronous: False +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"} + +Combination: 5; TestConfiguration = Format: JsonLight, Request: True, Synchronous: True +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44} + +Combination: 6; TestConfiguration = Format: JsonLight, Request: False, Synchronous: True +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44} + +Combination: 7; TestConfiguration = Format: JsonLight, Request: True, Synchronous: False +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44} + +Combination: 8; TestConfiguration = Format: JsonLight, Request: False, Synchronous: False +Model Present: true +{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44} + diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs index f7491a77e3..3056e96869 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs @@ -48,7 +48,7 @@ public void PayloadOrderTest() var nonMLEBaseType = new EdmEntityType("TestModel", "NonMLEBaseType"); nonMLEBaseType.AddKeys(nonMLEBaseType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false))); model.AddElement(nonMLEBaseType); - var nonMLESet = container.AddEntitySet("NonMLESet", nonMLEBaseType); + var nonMLESet = container.AddEntitySet("NonMLESet", nonMLEBaseType); var nonMLEType = new EdmEntityType("TestModel", "NonMLEType", nonMLEBaseType); nonMLEType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); @@ -66,13 +66,13 @@ public void PayloadOrderTest() var mleType = new EdmEntityType("TestModel", "MLEType", mleBaseType, false, false, true); mleType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); mleType.AddStructuralProperty("Description", EdmCoreModel.Instance.GetString(true)); - mleType.AddStructuralProperty("StreamProperty", EdmPrimitiveTypeKind.Stream, isNullable:false); + mleType.AddStructuralProperty("StreamProperty", EdmPrimitiveTypeKind.Stream, isNullable: false); var mleNav = mleType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "NavProp", Target = otherType, TargetMultiplicity = EdmMultiplicity.Many }); mleSet.AddNavigationTarget(mleNav, otherset); model.AddElement(mleType); IEnumerable testCases = new[] - { + { new EntryPayloadTestCase { DebugDescription = "TypeName at the beginning, nothing else", @@ -96,7 +96,7 @@ public void PayloadOrderTest() new ODataProperty { Name = "Name", Value = "test" }, } } - .WithAnnotation(new WriteEntryCallbacksAnnotation + .WithAnnotation(new WriteEntryCallbacksAnnotation { BeforeWriteStartCallback = (entry) => { entry.TypeName = "TestModel.MLEType"; }, BeforeWriteEndCallback = (entry) => { entry.TypeName = "NonExistingType"; } @@ -197,16 +197,16 @@ public void PayloadOrderTest() new ODataProperty { Name = "ID", Value = (int)42 }, new ODataProperty { Name = "Name", Value = "test" }, } - }.WithAnnotation(new WriteEntryCallbacksAnnotation + }.WithAnnotation(new WriteEntryCallbacksAnnotation { BeforeWriteStartCallback = (entry) => - { + { entry.EditLink = null; entry.MediaResource.EditLink = null; entry.MediaResource.ETag = null; entry.MediaResource.ContentType = null; }, - BeforeWriteEndCallback = (entry) => + BeforeWriteEndCallback = (entry) => { entry.EditLink = new Uri("http://odata.org/editlink"); entry.MediaResource.EditLink = new Uri("http://odata.org/mediaeditlink"); @@ -240,7 +240,7 @@ public void PayloadOrderTest() testCase.Items, tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings) { - Json = string.Format(CultureInfo.InvariantCulture, testCase.Json, + Json = string.Format(CultureInfo.InvariantCulture, testCase.Json, string.Empty, JsonLightWriterUtils.GetMetadataUrlPropertyForEntry(testCase.EntitySet.Name) + ","), FragmentExtractor = (result) => result.RemoveAllAnnotations(true) @@ -338,7 +338,7 @@ public void ActionAndFunctionPayloadOrderTest() model.AddElement(nonMLEType); container.AddEntitySet("NonMLEType", nonMLEType); - ODataAction action = new ODataAction + ODataAction action = new ODataAction { Metadata = new Uri("http://odata.org/test/$metadata#defaultAction"), Title = "Default Action", @@ -432,6 +432,96 @@ public void ActionAndFunctionPayloadOrderTest() }); } + [TestMethod, Variation(Description = "Test skipping null values when writing JSON Lite entries with IgnoreNullValues = true.")] + public void IgnoreNullPropertiesInEntryTest() + { + EdmModel model = new EdmModel(); + var container = new EdmEntityContainer("TestModel", "TestContainer"); + model.AddElement(container); + + var customerType = new EdmEntityType("TestModel", "CustomerType", null, false, isOpen: false); + customerType.AddKeys(customerType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false))); + customerType.AddKeys(customerType.AddStructuralProperty("Hobby", EdmCoreModel.Instance.GetString(true))); + model.AddElement(customerType); + var customerSet = container.AddEntitySet("CustomerSet", customerType); + + var addressType = new EdmComplexType("TestModel", "AddressType"); + addressType.AddStructuralProperty("Street", EdmCoreModel.Instance.GetStream(true)); + model.AddElement(addressType); + + IEnumerable testCases = new[] + { + new EntryPayloadTestCase + { + DebugDescription = "Customer instance with all properties set.", + Items = new[] { new ODataResource() + { + TypeName = "TestModel.CustomerType", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "ID", Value = (int)42 }, + new ODataProperty { Name = "Hobby", Value = "Hiking" }, + } + } }, + Model = model, + EntitySet = customerSet, + Json = string.Join("$(NL)", + "{{", + "{0}\"ID\":\"42\", \"Hobby\":\"Hiking\"", + "}}") + }, + new EntryPayloadTestCase + { + DebugDescription = "Customer instance without Hobby.", + Items = new[] {new ODataResource() + { + TypeName = "TestModel.CustomerType", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "ID", Value = (int)44 }, + new ODataProperty { Name = "Hobby", Value = null }, + } + } }, + Model = model, + EntitySet = customerSet, + Json = string.Join("$(NL)", + "{{", + "{0}\"ID\":\"44\"", + "}}") + }, + + }; + + IEnumerable> testDescriptors = testCases.Select(testCase => + new PayloadWriterTestDescriptor( + this.Settings, + testCase.Items, + tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings) + { + Json = string.Format( + CultureInfo.InvariantCulture, + testCase.Json, + JsonLightWriterUtils.GetMetadataUrlPropertyForEntry(testCase.EntitySet.Name) + ","), + FragmentExtractor = (result) => result.RemoveAllAnnotations(true) + }) + { + DebugDescription = testCase.DebugDescription, + Model = testCase.Model, + PayloadEdmElementContainer = testCase.EntitySet, + PayloadEdmElementType = testCase.EntityType, + SkipTestConfiguration = testCase.SkipTestConfiguration + }); + + this.CombinatorialEngineProvider.RunCombinations( + testDescriptors, + this.WriterTestConfigurationProvider.JsonLightFormatConfigurationsWithIndent, + (testDescriptor, testConfiguration) => + { + testConfiguration.MessageWriterSettings.IgnoreNullValues = true; + TestWriterUtils.WriteAndVerifyODataEdmPayload(testDescriptor, testConfiguration, this.Assert, this.Logger); + }); + } + [TestMethod, Variation(Description = "Test correct serialization format when writing JSON Lite entries with open properties.")] public void OpenPropertiesInEntryTest() { @@ -439,7 +529,7 @@ public void OpenPropertiesInEntryTest() var container = new EdmEntityContainer("TestModel", "TestContainer"); model.AddElement(container); - var openCustomerType = new EdmEntityType("TestModel", "OpenCustomerType", null, false, isOpen:true); + var openCustomerType = new EdmEntityType("TestModel", "OpenCustomerType", null, false, isOpen: true); openCustomerType.AddKeys(openCustomerType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false))); model.AddElement(openCustomerType); var customerSet = container.AddEntitySet("CustomerSet", openCustomerType); @@ -494,7 +584,7 @@ public void OpenPropertiesInEntryTest() "\"type\":\"name\",\"properties\":{{", "\"name\":\"EPSG:4326\"", "}}", - "}}", + "}}", "}}", "}}") }, @@ -526,7 +616,7 @@ public void OpenPropertiesInEntryTest() tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings) { Json = string.Format( - CultureInfo.InvariantCulture, + CultureInfo.InvariantCulture, testCase.Json, JsonLightWriterUtils.GetMetadataUrlPropertyForEntry(testCase.EntitySet.Name) + ","), FragmentExtractor = (result) => result.RemoveAllAnnotations(true) @@ -561,7 +651,7 @@ public void SpatialPropertiesInEntryTest() customerType.AddStructuralProperty("Location2", EdmCoreModel.Instance.GetSpatial(EdmPrimitiveTypeKind.GeographyPoint, false)); model.AddElement(customerType); var customerSet = container.AddEntitySet("CustomerSet", customerType); - + ISpatial pointValue = GeographyFactory.Point(32.0, -100.0).Build(); IEnumerable testCases = new[] @@ -590,7 +680,7 @@ public void SpatialPropertiesInEntryTest() "\"type\":\"name\",\"properties\":{{", "\"name\":\"EPSG:4326\"", "}}", - "}}", + "}}", "}}", "}}") }, @@ -617,7 +707,7 @@ public void SpatialPropertiesInEntryTest() "\"type\":\"name\",\"properties\":{{", "\"name\":\"EPSG:4326\"", "}}", - "}}", + "}}", "}}", "}}") }, diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj index d9feb236ef..d1d7ea011e 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj @@ -182,6 +182,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest