From 85dce730b6d6b5cb6ef7219a471f750e1c672025 Mon Sep 17 00:00:00 2001 From: "DECIDEWARE\\mperdeck" Date: Thu, 27 Oct 2022 12:56:00 +1100 Subject: [PATCH 1/4] OData#277 change contract of IPropertyMapper.MapProperty, so it can now return null if the field should not be serialized at all (as in, ignored) --- .../Microsoft.AspNetCore.OData.xml | 5 ++++- .../Properties/SRResources.Designer.cs | 2 +- .../Properties/SRResources.resx | 2 +- .../Query/Container/IPropertyMapper.cs | 3 +++ .../Query/Container/NamedPropertyOfT.cs | 11 +++++++---- .../Query/Wrapper/SelectExpandWrapper.cs | 11 +++++++---- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 4d0e931a0..18808b539 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -6822,7 +6822,7 @@ - Looks up a localized string similar to The key mapping for the property '{0}' can't be null or empty.. + Looks up a localized string similar to The key mapping for the property '{0}' can't be empty.. @@ -7792,6 +7792,9 @@ properties in the that will be used during the serialization of the $select and $expand projection by a given formatter. For example, to support custom serialization attributes of a particular formatter. + + It also allows you to ignore a field (ensure that the returned + does not have a key for that field), by mapping the property name to null. diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs index 31011acb1..38e667b70 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs @@ -880,7 +880,7 @@ internal static string InvalidPropertyMapper { } /// - /// Looks up a localized string similar to The key mapping for the property '{0}' can't be null or empty.. + /// Looks up a localized string similar to The key mapping for the property '{0}' can't be empty.. /// internal static string InvalidPropertyMapping { get { diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx index e43617960..91a0110b8 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx @@ -496,7 +496,7 @@ A binary operator with incompatible types was detected. Found operand types '{0}' and '{1}' for operator kind '{2}'. - The key mapping for the property '{0}' can't be null or empty. + The key mapping for the property '{0}' can't be empty. Unknown function '{0}'. diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs b/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs index 2f349f28d..2ad722ac7 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs @@ -23,6 +23,9 @@ namespace Microsoft.AspNetCore.OData.Query.Container /// properties in the that will be used during the serialization of the $select /// and $expand projection by a given formatter. For example, to support custom serialization attributes of a /// particular formatter. + /// + /// It also allows you to ignore a field (ensure that the returned + /// does not have a key for that field), by mapping the property name to null. /// public interface IPropertyMapper { diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs index 1fc96e4f0..a8de825cd 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs @@ -27,12 +27,15 @@ public override void ToDictionaryCore(Dictionary dictionary, IPr if (Name != null && (includeAutoSelected || !AutoSelected)) { string mappedName = propertyMapper.MapProperty(Name); - if (String.IsNullOrEmpty(mappedName)) + if (mappedName != null) { - throw Error.InvalidOperation(SRResources.InvalidPropertyMapping, Name); - } + if (String.IsNullOrEmpty(mappedName)) + { + throw Error.InvalidOperation(SRResources.InvalidPropertyMapping, Name); + } - dictionary.Add(mappedName, GetValue()); + dictionary.Add(mappedName, GetValue()); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs index d74171d1a..194155a62 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs @@ -144,12 +144,15 @@ public IDictionary ToDictionary(Func Date: Thu, 27 Oct 2022 12:57:54 +1100 Subject: [PATCH 2/4] OData#277 update implementation of both default MapProperty methods (Newtonsoft.Json and System.Text), so they return null if the field has the JsonIgnore attribute --- .../JsonPropertyNameMapper.cs | 13 +++++++++++++ .../Query/Container/JsonPropertyNameMapper.cs | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs index 285a130e1..cc7e0ded2 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs @@ -49,6 +49,13 @@ public string MapProperty(string propertyName) IEdmProperty property = _type.Properties().Single(s => s.Name == propertyName); PropertyInfo info = GetPropertyInfo(property); + + JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); + if (jsonIgnore != null) + { + return null; + } + JsonPropertyAttribute jsonProperty = GetJsonProperty(info); if (jsonProperty != null && !string.IsNullOrWhiteSpace(jsonProperty.PropertyName)) { @@ -82,5 +89,11 @@ private static JsonPropertyAttribute GetJsonProperty(PropertyInfo property) return property.GetCustomAttributes(typeof(JsonPropertyAttribute), inherit: false) .OfType().SingleOrDefault(); } + + private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) + .OfType().SingleOrDefault(); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs b/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs index 1bf9c404c..695f4ee4c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs @@ -30,6 +30,13 @@ public string MapProperty(string propertyName) { IEdmProperty property = _type.Properties().Single(s => s.Name == propertyName); PropertyInfo info = GetPropertyInfo(property); + + JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); + if (jsonIgnore != null) + { + return null; + } + JsonPropertyNameAttribute jsonProperty = GetJsonProperty(info); if (jsonProperty != null && !String.IsNullOrWhiteSpace(jsonProperty.Name)) { @@ -63,5 +70,11 @@ private static JsonPropertyNameAttribute GetJsonProperty(PropertyInfo property) return property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), inherit: false) .OfType().SingleOrDefault(); } + + private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) + .OfType().SingleOrDefault(); + } } } From d0a0296aa43b29f91d2253c97f6067929c982834 Mon Sep 17 00:00:00 2001 From: "DECIDEWARE\\mperdeck" Date: Thu, 27 Oct 2022 13:00:12 +1100 Subject: [PATCH 3/4] OData#277 update unit tests to reflect that MapProperty now returns null if the field has the JsonIgnore attribute --- .../Container/JsonPropertyNameMapperTest.cs | 10 ++++ .../Query/Container/PropertyContainerTest.cs | 10 ++-- .../Query/Wrapper/SelectExpandWrapperTest.cs | 58 ++++++++++++++----- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs index 26fa73eef..5aa1e110a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs @@ -27,6 +27,9 @@ public void MapProperty_Maps_PropertyName() // Act & Assert Assert.Equal("City", mapper.MapProperty("City")); + + // Act & Assert + Assert.Null(mapper.MapProperty("IgnoreThis")); } private static (IEdmModel, IEdmStructuredType) GetOData() @@ -35,6 +38,7 @@ private static (IEdmModel, IEdmStructuredType) GetOData() EdmComplexType address = new EdmComplexType("NS", "Address"); address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("IgnoreThis", EdmPrimitiveTypeKind.String); model.AddElement(address); model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(JAddress))); @@ -42,6 +46,9 @@ private static (IEdmModel, IEdmStructuredType) GetOData() model.SetAnnotationValue(address.FindProperty("Street"), new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("Street"))); + model.SetAnnotationValue(address.FindProperty("IgnoreThis"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("IgnoreThis"))); + return (model, address); } @@ -51,6 +58,9 @@ private class JAddress [JsonPropertyName("Road")] public string Street { get; set; } + + [JsonIgnore] + public string IgnoreThis { get; set; } } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs index 0aa4ad7d8..899093ee8 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs @@ -203,7 +203,8 @@ public void ToDictionary_AppliesMappingToAllProperties() IList properties = new NamedPropertyExpression[] { new NamedPropertyExpression(name: Expression.Constant("PropA"), value: Expression.Constant(3)), - new NamedPropertyExpression(name: Expression.Constant("PropB"), value: Expression.Constant(6)) + new NamedPropertyExpression(name: Expression.Constant("PropB"), value: Expression.Constant(6)), + new NamedPropertyExpression(name: Expression.Constant("PropC"), value: Expression.Constant(9)) }; Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); PropertyContainer container = ToContainer(containerExpression); @@ -211,6 +212,7 @@ public void ToDictionary_AppliesMappingToAllProperties() Mock mapperMock = new Mock(); mapperMock.Setup(m => m.MapProperty("PropA")).Returns("PropertyA"); mapperMock.Setup(m => m.MapProperty("PropB")).Returns("PropB"); + mapperMock.Setup(m => m.MapProperty("PropC")).Returns((string)null); //Act IDictionary result = container.ToDictionary(mapperMock.Object); @@ -219,12 +221,12 @@ public void ToDictionary_AppliesMappingToAllProperties() Assert.NotNull(result); Assert.True(result.ContainsKey("PropertyA")); Assert.True(result.ContainsKey("PropB")); + Assert.False(result.ContainsKey("PropC")); } [Theory] - [InlineData(null)] [InlineData("")] - public void ToDictionary_Throws_IfMappingFunctionReturns_NullOrEmpty(string mappedName) + public void ToDictionary_Throws_IfMappingFunctionReturns_Empty(string mappedName) { // Arrange IList properties = new NamedPropertyExpression[] @@ -239,7 +241,7 @@ public void ToDictionary_Throws_IfMappingFunctionReturns_NullOrEmpty(string mapp // Act & Assert ExceptionAssert.Throws(() => - container.ToDictionary(mapperMock.Object), "The key mapping for the property 'PropA' can't be null or empty."); + container.ToDictionary(mapperMock.Object), "The key mapping for the property 'PropA' can't be empty."); } private static PropertyContainer ToContainer(Expression containerCreationExpression) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs index 3b7e9b5a2..3d23261ea 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs @@ -254,9 +254,8 @@ public void ToDictionary_Throws_IfMapperProvider_ReturnsNullPropertyMapper() } [Theory] - [InlineData(null)] [InlineData("")] - public void ToDictionary_Throws_IfMappingIsNullOrEmpty_ForAGivenProperty(string propertyMapping) + public void ToDictionary_Throws_IfMappingIsEmpty_ForAGivenProperty(string propertyMapping) { // Arrange EdmEntityType entityType = new EdmEntityType("NS", "Name"); @@ -282,38 +281,65 @@ public void ToDictionary_Throws_IfMappingIsNullOrEmpty_ForAGivenProperty(string // Act & Assert ExceptionAssert.Throws(() => testWrapper.ToDictionary(mapperProvider), - "The key mapping for the property 'SampleProperty' can't be null or empty."); + "The key mapping for the property 'SampleProperty' can't be empty."); } [Fact] - public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull() + public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull_IfPropertyIsRenamed() { // Arrange + var testWrapper = GetSamplePropertyTestWrapper(); + var mapperProvider = GetMapperProvider("SampleProperty", "Sample"); + + // Act + var result = testWrapper.ToDictionary(mapperProvider); + + // Assert + Assert.Equal(42, result["Sample"]); + } + + [Fact] + public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull_IfPropertyIsIgnored() + { + // Arrange + var testWrapper = GetSamplePropertyTestWrapper(); + var mapperProvider = GetMapperProvider("SampleProperty", null); + + // Act + var result = testWrapper.ToDictionary(mapperProvider); + + // Assert + Assert.False(result.ContainsKey("SampleProperty")); + } + + private Func GetMapperProvider(string mapFrom, string mapTo) + { + Mock mapperMock = new Mock(); + mapperMock.Setup(m => m.MapProperty(mapFrom)).Returns(mapTo); + Func mapperProvider = + (IEdmModel m, IEdmStructuredType t) => mapperMock.Object; + + return mapperProvider; + } + + private SelectExpandWrapper GetSamplePropertyTestWrapper() + { EdmEntityType entityType = new EdmEntityType("NS", "Name"); entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); - + EdmModel model = new EdmModel(); model.AddElement(entityType); model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); - + SelectExpandWrapper testWrapper = new SelectExpandWrapper { Instance = new TestEntity { SampleProperty = 42 }, Model = model, UseInstanceForProperties = true, }; - - Mock mapperMock = new Mock(); - mapperMock.Setup(m => m.MapProperty("SampleProperty")).Returns("Sample"); - Func mapperProvider = - (IEdmModel m, IEdmStructuredType t) => mapperMock.Object; - // Act - var result = testWrapper.ToDictionary(mapperProvider); - - // Assert - Assert.Equal(42, result["Sample"]); + return testWrapper; } private class TestEntity From 0d9356bcdc7ef4a01d6e1017dc124be7dff75fcf Mon Sep 17 00:00:00 2001 From: "DECIDEWARE\\mperdeck" Date: Thu, 27 Oct 2022 13:01:08 +1100 Subject: [PATCH 4/4] OData#277 add unit tests for Newtonsoft Json version of MapProperty --- .../JsonPropertyNameMapperTest.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs new file mode 100644 index 000000000..0168411ca --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Newtonsoft.Json; +using Microsoft.AspNetCore.OData.NewtonsoftJson; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Query.Container +{ + public class JsonPropertyNameMapperTests + { + [Fact] + public void MapProperty_Maps_PropertyName() + { + // Arrange + (IEdmModel model, IEdmStructuredType address) = GetOData(); + JsonPropertyNameMapper mapper = new JsonPropertyNameMapper(model, address); + + // Act & Assert + Assert.Equal("Road", mapper.MapProperty("Street")); + + // Act & Assert + Assert.Equal("City", mapper.MapProperty("City")); + + // Act & Assert + Assert.Null(mapper.MapProperty("IgnoreThis")); + } + + private static (IEdmModel, IEdmStructuredType) GetOData() + { + EdmModel model = new EdmModel(); + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("IgnoreThis", EdmPrimitiveTypeKind.String); + model.AddElement(address); + + model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(JAddress))); + + model.SetAnnotationValue(address.FindProperty("Street"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("Street"))); + + model.SetAnnotationValue(address.FindProperty("IgnoreThis"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("IgnoreThis"))); + + return (model, address); + } + + private class JAddress + { + public string City { get; set; } + + [JsonProperty("Road")] + public string Street { get; set; } + + [JsonIgnore] + public string IgnoreThis { get; set; } + } + } +}