diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 32dfa93807..6febfe0511 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Data; +using Volo.Abp.DynamicProxy; using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectMapping; @@ -44,9 +45,13 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider var mapper = ServiceProvider.GetService>(); if (mapper != null) { + var mapExtraPropertiesAttribute = mapper.GetType().GetSingleAttributeOrNull(); mapper.BeforeMap((TSource)source); var destination = mapper.Map((TSource)source); - TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary()); + var destinationExtraProperties = mapExtraPropertiesAttribute == null + ? new ExtraPropertyDictionary() + : GetDestinationExtraPropertiesSeed((TSource)source, destination); + TryMapExtraProperties(mapExtraPropertiesAttribute, (TSource)source, destination, destinationExtraProperties); mapper.AfterMap((TSource)source, destination); return destination; } @@ -54,9 +59,13 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider var reverseMapper = ServiceProvider.GetService>(); if (reverseMapper != null) { + var mapExtraPropertiesAttribute = reverseMapper.GetType().GetSingleAttributeOrNull(); reverseMapper.BeforeReverseMap((TSource)source); var destination = reverseMapper.ReverseMap((TSource)source); - TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary()); + var destinationExtraProperties = mapExtraPropertiesAttribute == null + ? new ExtraPropertyDictionary() + : GetDestinationExtraPropertiesSeed((TSource)source, destination); + TryMapExtraProperties(mapExtraPropertiesAttribute, (TSource)source, destination, destinationExtraProperties); reverseMapper.AfterReverseMap((TSource)source, destination); return destination; } @@ -219,6 +228,38 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return Expression.Lambda>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); } + protected virtual ExtraPropertyDictionary GetDestinationExtraPropertiesSeed(TSource source, TDestination destination) + { + var extraProperties = new ExtraPropertyDictionary(); + if (source is not IHasExtraProperties sourceHasExtraProperties || + destination is not IHasExtraProperties destinationHasExtraProperties || + destinationHasExtraProperties.ExtraProperties is null) + { + return extraProperties; + } + + //Keys that don't exist in the source can only be set by the destination's constructor + foreach (var property in destinationHasExtraProperties.ExtraProperties) + { + if (sourceHasExtraProperties.ExtraProperties == null || !sourceHasExtraProperties.ExtraProperties.ContainsKey(property.Key)) + { + extraProperties[property.Key] = property.Value; + } + } + + //Source keys may be copied by the generated mapper, reset registered ones to their default value and let the filter map the source value + var destinationType = ProxyHelper.UnProxy(destinationHasExtraProperties).GetType(); + foreach (var property in ObjectExtensionManager.Instance.GetProperties(destinationType)) + { + if (!extraProperties.ContainsKey(property.Name) && destinationHasExtraProperties.ExtraProperties.ContainsKey(property.Name)) + { + extraProperties[property.Name] = property.GetDefaultValue(); + } + } + + return extraProperties; + } + protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) { var extraProperties = new ExtraPropertyDictionary(); diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs index 9eca171e4e..7e6d4321a4 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs @@ -53,8 +53,10 @@ public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest(person); personDto.GetProperty("Name").ShouldBe("John"); //Defined in both classes - personDto.GetProperty("ChildCount").ShouldBe(0); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor - personDto.GetProperty("CityName").ShouldBeNull(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.HasProperty("ChildCount").ShouldBeTrue(); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.GetProperty("ChildCount").ShouldBe(0); + personDto.HasProperty("CityName").ShouldBeTrue(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.GetProperty("CityName").ShouldBeNull(); personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes } diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs index 5d166f7dc8..72150477a0 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs @@ -97,6 +97,7 @@ public class AbpReverseMapperly_Tests : AbpIntegratedTest var entity = _objectMapper.Map(dto); entity.GetProperty("Tag").ShouldBe("ok"); - entity.HasProperty("Secret").ShouldBeFalse(); + entity.HasProperty("Secret").ShouldBeTrue(); //Ignored, but was set to the default value by the ExtensibleObject constructor + entity.GetProperty("Secret").ShouldBeNull(); } } diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapExtraPropertiesDefaultSeed_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapExtraPropertiesDefaultSeed_Tests.cs new file mode 100644 index 0000000000..bc3ee341ae --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapExtraPropertiesDefaultSeed_Tests.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class MapExtraPropertiesDefaultSeed_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public MapExtraPropertiesDefaultSeed_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Single_Parameter_Map_Should_Preserve_Constructor_Seeded_Properties() + { + var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() } + .SetProperty("Tag", "ok"); + + var dto = _objectMapper.Map(entity); + + dto.GetProperty("Tag").ShouldBe("ok"); //Defined in both classes + dto.GetProperty("CreatedBy").ShouldBe("system"); //Set by the ExtensibleSeededDto constructor + dto.HasProperty("DtoOnly").ShouldBeTrue(); //Not defined in the source, but was set to the default value by the ExtensibleObject constructor + dto.GetProperty("DtoOnly").ShouldBeNull(); + } + + [Fact] + public void Single_Parameter_Map_Should_Not_Change_The_Source_Extra_Properties() + { + var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() } + .SetProperty("Tag", "ok") + .SetProperty("CreatedBy", "leaked"); + var originalReference = entity.ExtraProperties; + + _objectMapper.Map(entity); + + ReferenceEquals(entity.ExtraProperties, originalReference).ShouldBeTrue(); + entity.GetProperty("Tag").ShouldBe("ok"); + entity.GetProperty("CreatedBy").ShouldBe("leaked"); + } + + [Fact] + public void Single_Parameter_Map_Should_Not_Seed_Defaults_When_Destination_Disables_Them() + { + var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() } + .SetProperty("Tag", "ok"); + + var dto = _objectMapper.Map(entity); + + dto.GetProperty("Tag").ShouldBe("ok"); //Defined in both classes + dto.HasProperty("DtoOnly").ShouldBeFalse(); //ExtensibleNonSeededDto constructor disables the default values seeding + } + + [Fact] + public void Single_Parameter_Map_Should_Not_Leak_Filtered_Source_Values_Into_Registered_Keys() + { + var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() } + .SetProperty("Tag", "ok") + .SetProperty("DtoOnly", "leaked"); + + var dto = _objectMapper.Map(entity); + + dto.GetProperty("Tag").ShouldBe("ok"); //Defined in both classes + dto.GetProperty("DtoOnly").ShouldBeNull(); //Not defined in the source, the source value must not leak + } + + [Fact] + public void Single_Parameter_Map_Should_Not_Invoke_Default_Value_Factories_Without_MapExtraProperties_Attribute() + { + var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() } + .SetProperty("Tag", "ok"); + + var callsBefore = ExtensibleNoAttributeDto.CountedDefaultValueFactoryCalls; + + var dto = _objectMapper.Map(entity); + + (ExtensibleNoAttributeDto.CountedDefaultValueFactoryCalls - callsBefore).ShouldBe(1); //Only the ExtensibleNoAttributeDto constructor seeding + dto.HasProperty("Counted").ShouldBeTrue(); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs index 5153109472..eafce45f89 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs @@ -21,7 +21,20 @@ public class MapperlyTestModule : AbpModule .AddOrUpdateProperty("Tag") .AddOrUpdateProperty("Secret") .AddOrUpdateProperty("Tag") - .AddOrUpdateProperty("Secret"); + .AddOrUpdateProperty("Secret") + .AddOrUpdateProperty("Tag") + .AddOrUpdateProperty("Tag") + .AddOrUpdateProperty("DtoOnly") + .AddOrUpdateProperty("Tag") + .AddOrUpdateProperty("DtoOnly") + .AddOrUpdateProperty("Counted", options => + { + options.DefaultValueFactory = () => + { + ExtensibleNoAttributeDto.CountedDefaultValueFactoryCalls++; + return null!; + }; + }); }); } } diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs index 218ed36e1c..e45ce86d57 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs @@ -105,4 +105,62 @@ public partial class ExtensibleReverseMapper : TwoWayMapperBase +{ + public override partial ExtensibleSeededDto Map(ExtensibleSeededEntity source); + + public override partial void Map(ExtensibleSeededEntity source, ExtensibleSeededDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class ExtensibleNonSeededMapper : MapperBase +{ + public override partial ExtensibleNonSeededDto Map(ExtensibleSeededEntity source); + + public override partial void Map(ExtensibleSeededEntity source, ExtensibleNonSeededDto destination); +} + +[Mapper] +public partial class ExtensibleNoAttributeMapper : MapperBase +{ + public override partial ExtensibleNoAttributeDto Map(ExtensibleSeededEntity source); + + public override partial void Map(ExtensibleSeededEntity source, ExtensibleNoAttributeDto destination); } \ No newline at end of file