From d1df05b6dbe4ed2c29597026193c0178dc2fd2e1 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Mon, 26 Apr 2021 16:33:22 +0100 Subject: [PATCH 1/5] Adding data source -> target member matcher selection --- .../WhenConfiguringConstructorDataSources.cs | 2 +- .../WhenConfiguringDataSources.cs | 2 +- .../WhenConfiguringDataSourcesByFilter.cs | 147 ++++++++++++++ ...nfiguringDataSourcesByFilterIncorrectly.cs | 79 ++++++++ .../WhenConfiguringDataSourcesIncorrectly.cs | 4 +- .../WhenConfiguringReverseDataSources.cs | 2 +- ...onfiguringReverseDataSourcesIncorrectly.cs | 2 +- .../WhenConfiguringSequentialDataSources.cs | 92 +-------- ...iguringSequentialDataSourcesIncorrectly.cs | 106 ++++++++++ .../WhenConfiguringToTargetDataSources.cs | 2 +- ...nfiguringToTargetDataSourcesIncorrectly.cs | 2 +- ...enConfiguringToTargetInsteadDataSources.cs | 12 +- ...gnoringSourceMembersByValueFilterInline.cs | 21 +- .../WhenIgnoringMembersIncorrectly.cs | 10 +- AgileMapper/AgileMapper.csproj | 2 +- .../ConfigInfoDataSourceExtensions.cs | 47 +++++ .../CustomDataSourceTargetMemberSpecifier.cs | 125 +++++++----- ...mDictionaryMappingTargetMemberSpecifier.cs | 2 +- .../IConditionalMappingConfigurator.cs | 20 ++ .../IConfiguredDataSourceFactoryFactory.cs | 2 +- .../ICustomDataSourceTargetMemberSpecifier.cs | 23 ++- .../Api/Configuration/IFullMappingSettings.cs | 4 +- .../ISequencedDataSourceFactory.cs | 2 +- .../MappingConfigContinuation.cs | 13 +- .../MappingConfigStartingPoint.cs | 23 ++- .../Api/Configuration/MappingConfigurator.cs | 36 ++-- .../ConfiguredDataSourceFactory.cs | 157 ++------------- .../ConfiguredDataSourceFactoryBase.cs | 179 +++++++++++++++++ .../ConfiguredFilterDataSourceFactory.cs | 91 +++++++++ .../DataSourceReversalSetting.cs | 2 +- .../IReversibleConfiguredDataSourceFactory.cs | 9 + .../Dictionaries/CustomDictionaryKey.cs | 2 +- .../Configuration/MappingConfigInfo.cs | 2 +- .../MemberIgnores/ConfiguredMemberFilter.cs | 4 +- .../MemberIgnores/ConfiguredMemberIgnore.cs | 10 +- .../ConfiguredMemberIgnoreBase.cs | 2 +- .../ConfiguredSourceMemberFilter.cs | 11 +- .../ConfiguredSourceMemberIgnore.cs | 2 +- .../ConfiguredSourceMemberIgnoreBase.cs | 2 +- .../MemberIgnores/IMemberFilterIgnore.cs | 4 - .../Configuration/UserConfigurationSet.cs | 46 +++-- .../Configuration/UserConfiguredItemBase.cs | 2 +- .../Factories/DataSourceFindContext.cs | 30 +-- .../Factories/MetaMemberDataSourcesFactory.cs | 3 +- .../Internal/ExpressionExtensions.cs | 8 +- AgileMapper/Members/MemberExtensionMethods.cs | 2 +- .../Members/MemberMapperDataExtensions.cs | 2 +- .../ConfiguredMappingFactory.cs | 2 +- .../MappingExpressionFactoryBase.cs | 181 ++++++++++++------ AgileMapper/ObjectPopulation/ObjectMapper.cs | 2 +- 50 files changed, 1044 insertions(+), 493 deletions(-) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringConstructorDataSources.cs (98%) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringDataSources.cs (99%) create mode 100644 AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs create mode 100644 AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringDataSourcesIncorrectly.cs (99%) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringReverseDataSources.cs (99%) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringReverseDataSourcesIncorrectly.cs (99%) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringSequentialDataSources.cs (72%) create mode 100644 AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSourcesIncorrectly.cs rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringToTargetDataSources.cs (99%) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringToTargetDataSourcesIncorrectly.cs (98%) rename AgileMapper.UnitTests/Configuration/{ => DataSources}/WhenConfiguringToTargetInsteadDataSources.cs (96%) create mode 100644 AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs create mode 100644 AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs rename AgileMapper/Configuration/{ => DataSources}/DataSourceReversalSetting.cs (97%) create mode 100644 AgileMapper/Configuration/DataSources/IReversibleConfiguredDataSourceFactory.cs diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringConstructorDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringConstructorDataSources.cs similarity index 98% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringConstructorDataSources.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringConstructorDataSources.cs index 28a151d86..3567c2332 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringConstructorDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringConstructorDataSources.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using AgileMapper.Extensions; diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSources.cs similarity index 99% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSources.cs index 918864a21..b163b2e38 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSources.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using System.Collections; diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs new file mode 100644 index 000000000..c39c01a8d --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs @@ -0,0 +1,147 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources +{ + using System; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + // See https://github.com/agileobjects/AgileMapper/issues/208 + public class WhenConfiguringDataSourcesByFilter + { + [Fact] + public void ShouldApplyAConstantByTargetMemberTypeAndMemberType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .To() + .IfTargetMembersMatch(member => member.IsField) + .Map(123) + .ToTarget(); + + var source = new { Value = 456 }; + + var matchingResult = mapper.Map(source).ToANew>(); + matchingResult.Value.ShouldBe(123); + + var nonMatchingResult = mapper.Map(source).ToANew>(); + nonMatchingResult.Value.ShouldBe(456); + } + } + + [Fact] + public void ShouldApplyAnAlternateConstantByTargetTypeAndTargetMemberType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .To>() + .IfTargetMembersMatch(member => member.HasType()) + .Map("Hurrah!") + .ToTargetInstead(); + + var source = new PublicTwoFields + { + Value1 = 123, + Value2 = new Address { Line1 = "One", Line2 = "Two" } + }; + + var matchingResult = mapper.Map(source).ToANew>(); + matchingResult.Value1.ShouldBe(123); + matchingResult.Value2.Line1.ShouldBe("Hurrah!"); + matchingResult.Value2.Line2.ShouldBe("Hurrah!"); + + var nonMatchingResult = mapper.Map(source).ToANew>(); + nonMatchingResult.Value1.Line1.ShouldBe("One"); + nonMatchingResult.Value1.Line2.ShouldBe("Two"); + nonMatchingResult.Value2.ShouldBe(123); + } + } + + [Fact] + public void ShouldApplyAnExpressionBySourceTypeTargetTypeAndTargetMemberAttribute() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To() + .IfTargetMembersMatch(member => member.HasAttribute()) + .Map((b, s) => b ? "Y" : "N") + .ToTarget(); + + var source = new { AttributeValue = true, NoAttributeValue = false }; + + var matchingResult = mapper.Map(source).ToANew(); + matchingResult.AttributeValue.ShouldBe("Y"); + matchingResult.NoAttributeValue.ShouldBe("false"); + } + } + + [Fact] + public void ShouldApplyASourceMemberByTargetTypeAndTargetMemberNameConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew
() + .Map(ctx => ctx.Source.Value1.Line1) + .To(addr => addr.Line1) + .And + .IfTargetMembersMatch(member => member.Name.StartsWith(nameof(Address.Line2))) + .If(ctx => !string.IsNullOrEmpty(ctx.Source.Value2)) + .Map(ctx => ctx.Source.Value2) + .ToTarget(); + + var matchingSource = new PublicTwoFields + { + Value1 = new Address { Line1 = "Value1.Line1", Line2 = "Value1.Line2" }, + Value2 = "Value2" + }; + + var matchingResult = mapper.Map(matchingSource).ToANew
(); + matchingResult.Line1.ShouldBe("Value1.Line1"); + matchingResult.Line2.ShouldBe("Value2"); + + var nonMatchingSource = new PublicTwoFields + { + Value1 = new Address { Line1 = "Value1.Line1", Line2 = "Value1.Line2" }, + Value2 = string.Empty + }; + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew
(); + nonMatchingResult.Line1.ShouldBe("Value1.Line1"); + nonMatchingResult.Line2.ShouldBeNull(); + } + } + + #region Helper Classes + + private static class Issue208 + { + public sealed class YesNoBoolAttribute : Attribute + { + } + + // ReSharper disable ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + public class YesNoBoolValue + { + [YesNoBool] + public string AttributeValue { get; set; } + + public string NoAttributeValue { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore ClassNeverInstantiated.Local + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs new file mode 100644 index 000000000..9d90ca7eb --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs @@ -0,0 +1,79 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources +{ + using AgileMapper.Configuration; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + // See https://github.com/agileobjects/AgileMapper/issues/208 + public class WhenConfiguringDataSourcesByFilterIncorrectly + { + [Fact] + public void ShouldErrorIfMemberIgnoreSpecified() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ToANew>() + .IfTargetMembersMatch(member => member.IsField) + .Ignore(ptf => ptf.Value1, ptf => ptf.Value2); + } + }); + + configEx.Message.ShouldContain("target member filter"); + configEx.Message.ShouldContain("member.IsField"); + configEx.Message.ShouldContain("PublicTwoFields.Value1,"); + configEx.Message.ShouldContain("PublicTwoFields.Value2"); + } + + [Fact] + public void ShouldErrorIfDataSourceTargetMemberSpecified() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ToANew>() + .IfTargetMembersMatch(member => member.IsProperty) + .Map("Yippee!") + .To(ptf => ptf.Value1); + } + }); + + configEx.Message.ShouldContain("target member filter"); + configEx.Message.ShouldContain("member.IsProperty"); + configEx.Message.ShouldContain("data source mapping '\"Yippee!\"' -> "); + configEx.Message.ShouldContain("PublicTwoFields.Value1"); + } + + [Fact] + public void ShouldErrorIfConflictingMemberFilterConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ToANew>() + .IfTargetMembersMatch(member => member.HasType()) + .Map("Yippee!") + .ToTarget(); + + mapper.WhenMapping + .ToANew>() + .IgnoreTargetMembersOfType(); + } + }); + + configEx.Message.ShouldContain("target member filter"); + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesIncorrectly.cs similarity index 99% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesIncorrectly.cs index 81b1f9cf5..318a7061f 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesIncorrectly.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using System.Collections.Generic; @@ -26,7 +26,7 @@ public void ShouldErrorIfUnconvertibleConstantSpecified() .From>() .To>() .Map(new byte[] { 2, 4, 6, 8 }) - .To(x => x.Value); + .To(pf => pf.Value); } }); } diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringReverseDataSources.cs similarity index 99% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringReverseDataSources.cs index 1df1ff1bf..1092b49a1 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringReverseDataSources.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using Common; diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringReverseDataSourcesIncorrectly.cs similarity index 99% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringReverseDataSourcesIncorrectly.cs index 02fdecd06..31a8be12f 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringReverseDataSourcesIncorrectly.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using AgileMapper.Configuration; diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSources.cs similarity index 72% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSources.cs index b6e37dfe8..ea27be4de 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSources.cs @@ -1,7 +1,5 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { - using System; - using AgileMapper.Configuration; using AgileMapper.Extensions.Internal; using Common; using TestClasses; @@ -233,94 +231,6 @@ public void ShouldHandleANullSequentialDataSource() } } - [Fact] - public void ShouldErrorIfDuplicateSequentialDataSourceConfigured() - { - var configEx = Should.Throw(() => - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .From() - .ToANew() - .Map((src, _) => src.TheCat) - .Then.Map((src, _) => src.TheCat) - .To(tp => tp.PetNames); - } - }); - - configEx.Message.ShouldContain("already has configured data source"); - configEx.Message.ShouldContain("TheCat"); - } - - [Fact] - public void ShouldErrorIfSequentialDataSourceMemberDuplicated() - { - var configEx = Should.Throw(() => - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .From() - .ToANew() - .Map((src, _) => src.TheCat) - .Then.Map((src, _) => src.TheDog) - .To(tp => tp.PetNames) - .And - .Map((src, _) => src.TheCat) - .To(tp => tp.PetNames); - } - }); - - configEx.Message.ShouldContain("already has configured data source"); - configEx.Message.ShouldContain("TheCat"); - } - - [Fact] - public void ShouldErrorIfSimpleTypeMemberSpecified() - { - var configEx = Should.Throw(() => - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .From>() - .To>() - .Map(ctx => ctx.Source.Value1) - .Then.Map(ctx => ctx.Source.Value2) - .To(pp => pp.Value); - } - }); - - configEx.Message.ShouldContain("PublicTwoFields.Value2"); - configEx.Message.ShouldContain("cannot be sequentially applied"); - configEx.Message.ShouldContain("PublicProperty.Value"); - configEx.Message.ShouldContain("cannot have sequential data sources"); - } - - [Fact] - public void ShouldErrorIfIgnoredSourceMemberSpecified() - { - var configEx = Should.Throw(() => - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .From>() - .ToANew>() - .IgnoreSource(ptf => ptf.Value2) - .And - .Map((ptf, _) => ptf.Value1) - .Then.Map((ptf, _) => ptf.Value2) - .To(pp => pp.Value); - } - }); - - configEx.Message.ShouldContain("PublicTwoFields.Value2"); - configEx.Message.ShouldContain("PublicProperty
.Value"); - configEx.Message.ShouldContain("conflicts with an ignored source member"); - } - #region Helper Members public static class Issue184 diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSourcesIncorrectly.cs new file mode 100644 index 000000000..a39f17774 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringSequentialDataSourcesIncorrectly.cs @@ -0,0 +1,106 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources +{ + using System; + using AgileMapper.Configuration; + using Common; + using TestClasses; +#if !NET35 + using Xunit; + using static WhenConfiguringSequentialDataSources; +#else + using Fact = NUnit.Framework.TestAttribute; + using static WhenConfiguringSequentialDataSources; + + [NUnit.Framework.TestFixture] +#endif + public class WhenConfiguringSequentialDataSourcesIncorrectly + { + [Fact] + public void ShouldErrorIfDuplicateSequentialDataSourceConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .Map((src, _) => src.TheCat) + .Then.Map((src, _) => src.TheCat) + .To(tp => tp.PetNames); + } + }); + + configEx.Message.ShouldContain("already has configured data source"); + configEx.Message.ShouldContain("TheCat"); + } + + [Fact] + public void ShouldErrorIfSequentialDataSourceMemberDuplicated() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .Map((src, _) => src.TheCat) + .Then.Map((src, _) => src.TheDog) + .To(tp => tp.PetNames) + .And + .Map((src, _) => src.TheCat) + .To(tp => tp.PetNames); + } + }); + + configEx.Message.ShouldContain("already has configured data source"); + configEx.Message.ShouldContain("TheCat"); + } + + [Fact] + public void ShouldErrorIfSimpleTypeMemberSpecified() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(ctx => ctx.Source.Value1) + .Then.Map(ctx => ctx.Source.Value2) + .To(pp => pp.Value); + } + }); + + configEx.Message.ShouldContain("PublicTwoFields.Value2"); + configEx.Message.ShouldContain("cannot be sequentially applied"); + configEx.Message.ShouldContain("PublicProperty.Value"); + configEx.Message.ShouldContain("cannot have sequential data sources"); + } + + [Fact] + public void ShouldErrorIfIgnoredSourceMemberSpecified() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew>() + .IgnoreSource(ptf => ptf.Value2) + .And + .Map((ptf, _) => ptf.Value1) + .Then.Map((ptf, _) => ptf.Value2) + .To(pp => pp.Value); + } + }); + + configEx.Message.ShouldContain("PublicTwoFields.Value2"); + configEx.Message.ShouldContain("PublicProperty
.Value"); + configEx.Message.ShouldContain("conflicts with an ignored source member"); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetDataSources.cs similarity index 99% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetDataSources.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetDataSources.cs index da63c9e38..046e32fea 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetDataSources.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System.Collections.Generic; using System.Collections.ObjectModel; diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetDataSourcesIncorrectly.cs similarity index 98% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetDataSourcesIncorrectly.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetDataSourcesIncorrectly.cs index 83252a68b..fd3c88f34 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetDataSourcesIncorrectly.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using System.Collections.Generic; diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetInsteadDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetInsteadDataSources.cs similarity index 96% rename from AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetInsteadDataSources.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetInsteadDataSources.cs index 911411b5e..a59e01967 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringToTargetInsteadDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringToTargetInsteadDataSources.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.UnitTests.Configuration +namespace AgileObjects.AgileMapper.UnitTests.Configuration.DataSources { using System; using System.Collections.Generic; @@ -138,10 +138,7 @@ public void ShouldUseANestedAlternateDataSourceConditionally() { Value2 = new PublicField> { - Value = new PublicField - { - Value = 200 - } + Value = new PublicField { Value = 200 } } }; @@ -156,10 +153,7 @@ public void ShouldUseANestedAlternateDataSourceConditionally() { Value2 = new PublicField> { - Value = new PublicField - { - Value = 100 - } + Value = new PublicField { Value = 100 } } }; diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs index 9509688e9..8fd36c240 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs @@ -17,7 +17,7 @@ public class WhenIgnoringSourceMembersByValueFilterInline { [Fact] - public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() + public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersInline() { using (var mapper = Mapper.CreateNew()) { @@ -25,11 +25,10 @@ public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() .Map(new PublicField { Value = 123 }) .ToANew>(cfg => cfg .IgnoreSources(c => - c.If(str => str == "123") || c.If(i => i == 123) || - (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); + c.If((string s) => s == "123") || c.If((int i) => i == 123) || + (c.If((string s) => s != "999") && !c.If((DateTime dt) => dt == DateTime.Today)))); - matchingIntResult.ShouldNotBeNull(); - matchingIntResult.Value.ShouldBeDefault(); + matchingIntResult.ShouldNotBeNull().Value.ShouldBeDefault(); var matchingStringResult = mapper .Map(new PublicField { Value = "123" }) @@ -38,8 +37,7 @@ public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() c.If(str => str == "123") || c.If(i => i == 123) || (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); - matchingStringResult.ShouldNotBeNull(); - matchingStringResult.Value.ShouldBeNull(); + matchingStringResult.ShouldNotBeNull().Value.ShouldBeNull(); var nonMatchingIntResult = mapper .Map(new PublicField { Value = 456 }) @@ -48,8 +46,7 @@ public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() c.If(str => str == "123") || c.If(i => i == 123) || (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); - nonMatchingIntResult.ShouldNotBeNull(); - nonMatchingIntResult.Value.ShouldBe(456); + nonMatchingIntResult.ShouldNotBeNull().Value.ShouldBe(456); var nonMatchingStringResult = mapper .Map(new PublicField { Value = "999" }) @@ -58,8 +55,7 @@ public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() c.If(str => str == "123") || c.If(i => i == 123) || (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); - nonMatchingStringResult.ShouldNotBeNull(); - nonMatchingStringResult.Value.ShouldBe("999"); + nonMatchingStringResult.ShouldNotBeNull().Value.ShouldBe("999"); var nonMatchingTypeResult = mapper .Map(new PublicField { Value = 123L }) @@ -68,8 +64,7 @@ public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() c.If(str => str == "123") || c.If(i => i == 123) || (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); - nonMatchingTypeResult.ShouldNotBeNull(); - nonMatchingTypeResult.Value.ShouldBe("123"); + nonMatchingTypeResult.ShouldNotBeNull().Value.ShouldBe("123"); } } diff --git a/AgileMapper.UnitTests/Configuration/MemberIgnores/WhenIgnoringMembersIncorrectly.cs b/AgileMapper.UnitTests/Configuration/MemberIgnores/WhenIgnoringMembersIncorrectly.cs index c0940dc24..f60bc7c79 100644 --- a/AgileMapper.UnitTests/Configuration/MemberIgnores/WhenIgnoringMembersIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/MemberIgnores/WhenIgnoringMembersIncorrectly.cs @@ -73,7 +73,7 @@ public void ShouldErrorIfConfiguredDataSourceMemberIsIgnored() } [Fact] - public void ShouldErrorIfNonPublicReadOnlySimpleTypeMemberSpecified() + public void ShouldErrorIfNonPublicReadOnlySimpleTypeMemberIgnored() { var configurationEx = Should.Throw(() => { @@ -89,7 +89,7 @@ public void ShouldErrorIfNonPublicReadOnlySimpleTypeMemberSpecified() } [Fact] - public void ShouldErrorIfReadOnlySimpleTypeMemberSpecified() + public void ShouldErrorIfReadOnlySimpleTypeMemberIgnored() { var configurationEx = Should.Throw(() => { @@ -97,7 +97,7 @@ public void ShouldErrorIfReadOnlySimpleTypeMemberSpecified() { mapper.WhenMapping .To>() - .Ignore(psm => psm.Value); + .Ignore(prof => prof.Value); } }); @@ -105,7 +105,7 @@ public void ShouldErrorIfReadOnlySimpleTypeMemberSpecified() } [Fact] - public void ShouldErrorIfFilteredMemberIsIgnored() + public void ShouldErrorIfGloballyFilteredMemberIsIgnored() { var ignoreEx = Should.Throw(() => { @@ -124,7 +124,7 @@ public void ShouldErrorIfFilteredMemberIsIgnored() } [Fact] - public void ShouldErrorIfDuplicateFilterIsConfigured() + public void ShouldErrorIfDuplicateGlobalFilterIsConfigured() { var ignoreEx = Should.Throw(() => { diff --git a/AgileMapper/AgileMapper.csproj b/AgileMapper/AgileMapper.csproj index ef6ae9ffd..ef494e4c7 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -2,7 +2,7 @@ net35;net40;netstandard1.0;netstandard1.3;netstandard2.0 - 8.0 + latest AgileObjects.AgileMapper AgileObjects.AgileMapper AgileObjects.AgileMapper diff --git a/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs b/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs index 283c1933a..56e074416 100644 --- a/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs +++ b/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs @@ -1,6 +1,12 @@ namespace AgileObjects.AgileMapper.Api.Configuration { + using System; + using System.Linq; + using System.Linq.Expressions; using AgileMapper.Configuration; + using Extensions.Internal; + using Members; + using ReadableExpressions; internal static class ConfigInfoDataSourceExtensions { @@ -25,5 +31,46 @@ public static MappingConfigInfo SetSequenceDataSourceFactories( { return configInfo.Set(dataSourceFactorySequence); } + + public static bool HasTargetMemberFilter(this MappingConfigInfo configInfo) + => configInfo.HasTargetMemberFilter(out _); + + public static bool HasTargetMemberFilter( + this MappingConfigInfo configInfo, + out Expression> targetMemberFilter) + { + targetMemberFilter = configInfo.Get>>(); + return targetMemberFilter != null; + } + + public static MappingConfigInfo SetTargetMemberFilter( + this MappingConfigInfo configInfo, + Expression> memberMatcherLambda) + { + return configInfo.Set(memberMatcherLambda); + } + + public static void ThrowIfTargetMemberFilterSpecified( + this MappingConfigInfo configInfo, + Func configDescriptionFactory, + params Expression>[] targetMembers) + { + if (!configInfo.HasTargetMemberFilter(out var filter)) + { + return; + } + + var configDescription = configDescriptionFactory.Invoke(configInfo); + + var targetMemberPaths = targetMembers + .Select(m => m + .ToTargetMemberOrNull(configInfo.MapperContext) + .GetFriendlyTargetPath(configInfo)) + .Join(", "); + + throw new MappingConfigurationException( + $"Member-agnostic target member filter '{filter.Body.ToReadableString()}' cannot " + + $"be combined with member-specific {configDescription} {targetMemberPaths}"); + } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index b57e3b034..065fb1ed3 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -76,46 +76,26 @@ public ICustomDataSourceMappingConfigContinuation To> targetMember) { ThrowIfTargetParameterSpecified(targetMember); + ThrowIfTargetMemberFilterSpecified(targetMember); ThrowIfSequentialDataSourceForSimpleMember(targetMember); ThrowIfRedundantSourceMember(targetMember); - SetTargetMemberForSequence(targetMember); - return RegisterDataSource(cdsff => cdsff.CreateFromLambda()); + return RegisterDataSourceLambda(targetMember); } IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.To( Expression> resultMember) { ThrowIfTargetParameterSpecified(resultMember); - - SetTargetMemberForSequence(resultMember); - return RegisterDataSource(cdsff => cdsff.CreateFromLambda()); + return RegisterDataSourceLambda(resultMember); } public IMappingConfigContinuation To( Expression>> targetSetMethod) { - SetTargetMemberForSequence(targetSetMethod); - return RegisterDataSource(cdsff => cdsff.CreateFromLambda()); + return RegisterDataSourceLambda(targetSetMethod); } - private void SetTargetMemberForSequence(LambdaExpression targetMember) - { - SetTargetMember(targetMember); - - if (_sequenceDataSourceFactories == null) - { - return; - } - - foreach (var dataSourceFactory in _sequenceDataSourceFactories) - { - dataSourceFactory.SetTargetMember(targetMember); - } - } - - private void SetTargetMember(LambdaExpression targetMember) => _targetMemberLambda = targetMember; - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local private static void ThrowIfTargetParameterSpecified(LambdaExpression targetMember) { @@ -127,6 +107,15 @@ private static void ThrowIfTargetParameterSpecified(LambdaExpression targetMembe } } + private void ThrowIfTargetMemberFilterSpecified( + Expression> targetMember) + { + _configInfo.ThrowIfTargetMemberFilterSpecified( + configDescriptionFactory: ci => + $"data source mapping '{GetValueLambdaInfo().GetDescription(ci)}' -> ", + targetMember); + } + private void ThrowIfSequentialDataSourceForSimpleMember( LambdaExpression targetMemberLambda) { @@ -227,7 +216,15 @@ private ConfiguredDataSourceFactory CreateFromLambda() return new ConfiguredDictionaryEntryDataSourceFactory(_configInfo, valueLambdaInfo, dictionaryEntryMember); } - return CreateDataSourceFactory(valueLambdaInfo); + return new ConfiguredDataSourceFactory( + _configInfo, + valueLambdaInfo, +#if NET35 + _targetMemberLambda.ToDlrExpression(), +#else + _targetMemberLambda, +#endif + _valueCouldBeSourceMember); } private ConfiguredLambdaInfo GetValueLambdaInfo() => GetValueLambdaInfo(typeof(TTargetValue)); @@ -247,7 +244,8 @@ private ConfiguredLambdaInfo GetValueLambdaInfo(Type targetValueType) #endif if ((customValueLambda.Body.NodeType != CONSTANT) || (targetValueType == typeof(object)) || - customValueLambda.ReturnType.IsAssignableTo(targetValueType)) + customValueLambda.ReturnType.IsAssignableTo(targetValueType) || + _configInfo.HasTargetMemberFilter()) { return _customValueLambdaInfo = ConfiguredLambdaInfo.For(customValueLambda, _configInfo); } @@ -294,32 +292,19 @@ private bool IsDictionaryEntry(out DictionaryTargetMember entryMember) var entryKey = (string)((ConstantExpression)entryKeyExpression).Value; - var rootMember = (DictionaryTargetMember)CreateRootTargetQualifiedMember(); + var rootMember = (DictionaryTargetMember)CreateToTargetQualifiedMember(); entryMember = rootMember.Append(typeof(TSource), entryKey); return true; } - private QualifiedMember CreateRootTargetQualifiedMember() + private QualifiedMember CreateToTargetQualifiedMember() { return (_configInfo.TargetType == typeof(ExpandoObject)) ? MapperContext.QualifiedMemberFactory.RootTarget() : MapperContext.QualifiedMemberFactory.RootTarget(); } - private ConfiguredDataSourceFactory CreateDataSourceFactory(ConfiguredLambdaInfo valueLambdaInfo) - { - return new ConfiguredDataSourceFactory( - _configInfo, - valueLambdaInfo, -#if NET35 - _targetMemberLambda.ToDlrExpression(), -#else - _targetMemberLambda, -#endif - _valueCouldBeSourceMember); - } - public IMappingConfigContinuation ToCtor() => RegisterDataSource(cdsff => cdsff.CreateForCtorParam()); @@ -431,7 +416,7 @@ private ConfiguredDataSourceFactory CreateForCtorParam() { var valueLambda = GetValueLambdaInfo(_targetCtorParameter.ParameterType); var ctorParameterMember = Member.ConstructorParameter(_targetCtorParameter); - var ctorParameter = CreateRootTargetQualifiedMember().Append(ctorParameterMember); + var ctorParameter = CreateToTargetQualifiedMember().Append(ctorParameterMember); ThrowIfRedundantSourceMember(valueLambda, ctorParameter); @@ -446,17 +431,26 @@ public IMappingConfigContinuation ToTarget() public IMappingConfigContinuation ToTargetInstead() => RegisterDataSource(cdsff => cdsff.CreateForToTarget(isSequential: false)); - private ConfiguredDataSourceFactory CreateForToTarget(bool isSequential) + private ConfiguredDataSourceFactoryBase CreateForToTarget(bool isSequential) { if (isSequential) { _configInfo.ForSequentialConfiguration(); } - return new ConfiguredDataSourceFactory( - _configInfo, - GetValueLambdaInfo(), - CreateRootTargetQualifiedMember()); + var dataSourceLambda = GetValueLambdaInfo(); + var toTargetMember = CreateToTargetQualifiedMember(); + + if (_configInfo.HasTargetMemberFilter(out var filter)) + { + return new ConfiguredFilterDataSourceFactory( + _configInfo, + filter, + dataSourceLambda, + toTargetMember); + } + + return new ConfiguredDataSourceFactory(_configInfo, dataSourceLambda, toTargetMember); } public IMappingConfigContinuation ToTarget() @@ -481,15 +475,39 @@ private void SetDerivedToTargetSource(MappingConfigInfo derivedT private static string GetTypeDescription(Type type) => $"of type '{type.GetFriendlyName()}'"; + private MappingConfigContinuation RegisterDataSourceLambda( + LambdaExpression targetMemberLambda) + { + SetTargetMemberForSequence(targetMemberLambda); + return RegisterDataSource(cdsff => cdsff.CreateFromLambda()); + } + + private void SetTargetMemberForSequence(LambdaExpression targetMember) + { + SetTargetMember(targetMember); + + if (_sequenceDataSourceFactories == null) + { + return; + } + + foreach (var dataSourceFactory in _sequenceDataSourceFactories) + { + dataSourceFactory.SetTargetMember(targetMember); + } + } + + private void SetTargetMember(LambdaExpression targetMember) => _targetMemberLambda = targetMember; + private MappingConfigContinuation RegisterDataSource( - Func dataSourceFactoryFactory) + Func dataSourceFactoryFactory) { return RegisterDataSource(typeof(TTargetValue), dataSourceFactoryFactory); } private MappingConfigContinuation RegisterDataSource( Type targetMemberType, - Func dataSourceFactoryFactory) + Func dataSourceFactoryFactory) { ThrowIfInvalid(targetMemberType); @@ -509,7 +527,7 @@ private MappingConfigContinuation RegisterDataSource( } private void Register( - Func dataSourceFactoryFactory, + Func dataSourceFactoryFactory, Type targetMemberType) { RegisterComplexTypeFactoryMethodIfAppropriate(targetMemberType); @@ -529,7 +547,8 @@ private void ThrowIfSimpleSourceForNonSimpleTargetMember(Type targetMemberType) if ((targetMemberType == typeof(object)) || targetMemberType.IsSimple() || !ConfiguredSourceType.IsSimple() || - ConversionOperatorExists(targetMemberType)) + ConversionOperatorExists(targetMemberType) || + _configInfo.HasTargetMemberFilter()) { return; } @@ -667,7 +686,7 @@ ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateForCtorPar ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateFromLambda() => CreateFromLambda(); - ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateForToTarget(bool isSequential) + ConfiguredDataSourceFactoryBase IConfiguredDataSourceFactoryFactory.CreateForToTarget(bool isSequential) => CreateForToTarget(isSequential); #endregion @@ -681,7 +700,7 @@ void ISequencedDataSourceFactory.SetTargetMember(LambdaExpression targetMember) => SetTargetMember(targetMember); void ISequencedDataSourceFactory.Register( - Func dataSourceFactoryFactory, + Func dataSourceFactoryFactory, Type targetMemberType) { Register(dataSourceFactoryFactory, targetMemberType); diff --git a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs index 1cf939ae6..4139f5156 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs @@ -90,7 +90,7 @@ private DictionaryMappingConfigContinuation RegisterCustomKey(L private static string GetConflictMessage( CustomDictionaryKey key, - ConfiguredDataSourceFactory conflictingDataSource) + ConfiguredDataSourceFactoryBase conflictingDataSource) { return key.GetConflictMessage(conflictingDataSource); } diff --git a/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs b/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs index 457c4e86b..874f1979c 100644 --- a/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs @@ -42,6 +42,26 @@ IConditionalRootMappingConfigurator If( /// An IConditionalRootMappingConfigurator with which to complete the configuration. IConditionalRootMappingConfigurator If( Expression> condition); + } + + /// + /// Provides options for configuring a filter which must evaluate to true for the configuration + /// to apply to mappings from and to the source and target types being configured. + /// + /// The source type to which the configuration should apply. + /// The target type to which the configuration should apply. + public interface IFilteredMappingConfigurator + { + /// + /// Configure a which target members must match for the + /// configuration to apply. + /// + /// + /// The matching function with which to select target members to which to apply the configuration. + /// + /// An IConditionalMappingConfigurator with which to complete the configuration. + IConditionalMappingConfigurator IfTargetMembersMatch( + Expression> memberFilter); /// /// Ignore all source members of the given type diff --git a/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs b/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs index 7323edbe2..79ba1a052 100644 --- a/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs +++ b/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs @@ -10,6 +10,6 @@ internal interface IConfiguredDataSourceFactoryFactory ConfiguredDataSourceFactory CreateForCtorParam(); - ConfiguredDataSourceFactory CreateForToTarget(bool isSequential); + ConfiguredDataSourceFactoryBase CreateForToTarget(bool isSequential); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs index 0a833dc7b..fac457d17 100644 --- a/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs @@ -42,12 +42,13 @@ IMappingConfigContinuation To( Expression>> targetSetMethod); /// - /// Apply the configuration to the constructor parameter with the type specified by the type argument. + /// Apply the configuration to the constructor parameter with the type specified by the type + /// argument. /// /// The target constructor parameter's type. /// - /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source - /// and target type being configured. + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. /// IMappingConfigContinuation ToCtor(); @@ -56,15 +57,15 @@ IMappingConfigContinuation To( /// /// The target constructor parameter's name. /// - /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. /// IMappingConfigContinuation ToCtor(string parameterName); /// /// Map the configured source value to the target object being configured, after any matching - /// source member has been mapped. To mapapply the configured source value without mapping - /// any matching source member, use ToTargetInstead(). + /// source member has been mapped. To map the configured source value without mapping + /// any matching source member, use ToTargetInstead(). /// /// /// An IMappingConfigContinuation to enable further configuration of mappings from and to the @@ -73,9 +74,11 @@ IMappingConfigContinuation To( IMappingConfigContinuation ToTarget(); /// - /// Map the configured source value to the target object being configured, instead of mapping - /// any matching source member. To map any matching source member as well as the configured - /// source value, use ToTarget(). + /// Map the configured source value to the target object being configured, instead of + /// mapping any matching source member. If this mapping configuration has an If() clause which + /// evaluates to false during mapping, no mapping is performed.

+ /// To map any matching source member as well as the configured source value, use + /// ToTarget(). ///
/// /// An IMappingConfigContinuation to enable further configuration of mappings from and to the diff --git a/AgileMapper/Api/Configuration/IFullMappingSettings.cs b/AgileMapper/Api/Configuration/IFullMappingSettings.cs index 0c454f355..fcf9cf377 100644 --- a/AgileMapper/Api/Configuration/IFullMappingSettings.cs +++ b/AgileMapper/Api/Configuration/IFullMappingSettings.cs @@ -8,7 +8,9 @@ /// /// The source type to which the configured settings should apply. /// The target type to which the configured settings should apply. - public interface IFullMappingSettings : IConditionalMappingConfigurator + public interface IFullMappingSettings : + IConditionalMappingConfigurator, + IFilteredMappingConfigurator { #region Exception Handling diff --git a/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs b/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs index c64adba43..c503322cd 100644 --- a/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs +++ b/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs @@ -12,7 +12,7 @@ internal interface ISequencedDataSourceFactory void SetTargetMember(LambdaExpression targetMember); void Register( - Func dataSourceFactoryFactory, + Func dataSourceFactoryFactory, Type targetMemberType); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/MappingConfigContinuation.cs b/AgileMapper/Api/Configuration/MappingConfigContinuation.cs index dfe07122e..eee875813 100644 --- a/AgileMapper/Api/Configuration/MappingConfigContinuation.cs +++ b/AgileMapper/Api/Configuration/MappingConfigContinuation.cs @@ -26,15 +26,16 @@ IMappingConfigContinuation ICustomDataSourceMappingConfigConti $"'{GetDataSourceDescription()}' cannot be reversed, because {reason}"); } - if (UserConfigurations.AutoDataSourceReversalEnabled(dataSourceFactory) == false) + if (UserConfigurations.AutoDataSourceReversalEnabled(dataSourceFactory)) { - UserConfigurations.AddReverseDataSourceFor(dataSourceFactory); - return this; + throw new MappingConfigurationException( + $"'{GetDataSourceDescription()}' does not need to be explicitly reversed, " + + "because configured data source reversal is enabled by default"); } - throw new MappingConfigurationException( - $"'{GetDataSourceDescription()}' does not need to be explicitly reversed, " + - "because configured data source reversal is enabled by default"); + UserConfigurations.AddReverseDataSourceFor(dataSourceFactory); + return this; + } IMappingConfigContinuation ICustomDataSourceMappingConfigContinuation.ButNotViceVersa() diff --git a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs index fd9c6d32c..29fa0b196 100644 --- a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; + using AgileMapper.Configuration.DataSources; using AgileMapper.Configuration.MemberIgnores; using AgileMapper.Configuration.MemberIgnores.SourceValueFilters; using Dictionaries; @@ -217,10 +218,11 @@ public IGlobalMappingSettings UseNamePatterns(params string[] patterns) #endregion /// - /// Ensure 1-to-1 relationships between source and mapped objects by tracking and reusing mapped objects if - /// they appear more than once in a source object tree. Mapped objects are automatically tracked in object - /// trees with circular relationships - unless is called - so configuring - /// this option is not necessary when mapping circular relationships. + /// Ensure 1-to-1 relationships between source and mapped objects by tracking and reusing + /// mapped objects if they appear more than once in a source object tree. Mapped objects + /// are automatically tracked in object trees with circular relationships - unless + /// DisableObjectTracking() is called - so configuring this option is not necessary when + /// mapping circular relationships. /// /// /// An , with which to globally configure other mapping aspects. @@ -236,7 +238,7 @@ public IGlobalMappingSettings MaintainIdentityIntegrity() /// Mapped objects are tracked by default when mapping circular relationships to prevent stack overflows /// if two objects in a source object tree hold references to each other, and to ensure 1-to-1 relationships /// between source and mapped objects. If you are confident that each object in a source object tree appears - /// only once, disabling object tracking will increase mapping performance. + /// only once, disabling object tracking will improve mapping performance. /// /// /// An , with which to globally configure other mapping aspects. @@ -260,7 +262,11 @@ public IGlobalMappingSettings MapNullCollectionsToNull() } /// - /// Map entity key values for all source and target types. + /// Map entity key values for all source and target types. Properties with [Key] attributes + /// are ignored by default, as ORMs commonly use them as unique identifiers, and throw + /// Exceptions if they are updated. This global option sets the default behaviour to + /// opt-in for all mappings; individual mapping-scoped configurations can subsequently + /// use IgnoreEntityKeys() to opt-out. /// /// /// This , with which to globally configure other mapping aspects. @@ -274,8 +280,9 @@ public IGlobalMappingSettings MapEntityKeys() /// /// Apply configured data sources in both mapping directions, for all source and target types. /// For example, configuring ProductDto.ProdId -> Product.Id will also apply Product.Id -> ProductDto.ProdId. - /// This global option sets the default behaviour; individual mapping- and member-scoped configurations can - /// subsequently opt-out. + /// This global option sets the default behaviour to opt-in for all mappings; individual + /// mapping- and member-scoped configurations can subsequently use + /// DoNotAutoReverseConfiguredDataSources() to opt-out. /// /// /// This , with which to globally configure other mapping aspects. diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index 845433212..1d49b9f47 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; + using AgileMapper.Configuration.DataSources; using AgileMapper.Configuration.Lambdas; using AgileMapper.Configuration.MemberIgnores; using AgileMapper.Configuration.MemberIgnores.SourceValueFilters; @@ -170,6 +171,13 @@ public IConditionalRootMappingConfigurator If(Expression If(Expression> condition) => SetCondition(condition); + public IConditionalMappingConfigurator IfTargetMembersMatch( + Expression> memberFilter) + { + ConfigInfo.SetTargetMemberFilter(memberFilter); + return this; + } + IMapSourceConfigurator IConditionalMapSourceConfigurator.If( Expression, bool>> condition) { @@ -277,7 +285,7 @@ public IMappingFactorySpecifier MapInstancesOf SwallowAllExceptions() => PassExceptionsTo(ctx => { }); + public IFullMappingSettings SwallowAllExceptions() => PassExceptionsTo(_ => { }); public IFullMappingSettings PassExceptionsTo(Action> callback) { @@ -344,7 +352,7 @@ IProjectionEnumPairSpecifier IFullProjectionSettings IgnoreSources( Expression> valuesFilter) { - return IgnoreMembersByFilter( + return FilterMembers( ConfiguredSourceValueFilter.Create(ConfigInfo, valuesFilter), UserConfigurations.Add); } @@ -363,7 +371,7 @@ public IMappingConfigContinuation IgnoreSourceMembersOfType IgnoreSourceMembersWhere( Expression> memberFilter) { - return IgnoreMembersByFilter( + return FilterMembers( new ConfiguredSourceMemberFilter(ConfigInfo, memberFilter), UserConfigurations.Add); } @@ -378,16 +386,20 @@ IProjectionConfigContinuation IRootProjectionConfigurator IgnoreTargetMembers( - IEnumerable>> targetMembers) + Expression>[] targetMembers) { + ConfigInfo.ThrowIfTargetMemberFilterSpecified( + configDescriptionFactory: _ => "ignore(s)", + targetMembers); + return IgnoreMembers( targetMembers, (ci, tm) => new ConfiguredMemberIgnore(ci, tm), UserConfigurations.Add); } - private MappingConfigContinuation IgnoreMembers( - IEnumerable>> members, + private MappingConfigContinuation IgnoreMembers( + IEnumerable members, Func configuredIgnoreFactory, Action configurationsAddMethod) where TConfig : UserConfiguredItemBase @@ -407,29 +419,29 @@ public IMappingConfigContinuation IgnoreTargetMembersOfType IgnoreTargetMembersWhere(member => member.HasType()); IProjectionConfigContinuation IRootProjectionConfigurator.IgnoreTargetMembersOfType() - => IgnoreTargetMembersByFilter(member => member.HasType()); + => FilterTargetMembers(member => member.HasType()); public IMappingConfigContinuation IgnoreTargetMembersWhere( Expression> memberFilter) { - return IgnoreTargetMembersByFilter(memberFilter); + return FilterTargetMembers(memberFilter); } IProjectionConfigContinuation IRootProjectionConfigurator.IgnoreTargetMembersWhere( Expression> memberFilter) { - return IgnoreTargetMembersByFilter(memberFilter); + return FilterTargetMembers(memberFilter); } - private MappingConfigContinuation IgnoreTargetMembersByFilter( + private MappingConfigContinuation FilterTargetMembers( Expression> memberFilter) { - return IgnoreMembersByFilter( + return FilterMembers( new ConfiguredMemberFilter(ConfigInfo, memberFilter), UserConfigurations.Add); } - private MappingConfigContinuation IgnoreMembersByFilter( + private MappingConfigContinuation FilterMembers( TIgnore memberIgnore, Action configurationsAddMethod) where TIgnore : UserConfiguredItemBase diff --git a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs index 22fe090f8..23de439f1 100644 --- a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs +++ b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs @@ -1,34 +1,26 @@ namespace AgileObjects.AgileMapper.Configuration.DataSources { #if NET35 - using System; using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif - using AgileMapper.DataSources; using Lambdas; using Members; internal class ConfiguredDataSourceFactory : - UserConfiguredItemBase, - IPotentialAutoCreatedItem -#if NET35 - , IComparable -#endif + ConfiguredDataSourceFactoryBase, + IReversibleConfiguredDataSourceFactory { - private readonly ConfiguredLambdaInfo _dataSourceLambda; - private bool _valueCouldBeSourceMember; - private MappingConfigInfo _reverseConfigInfo; private bool _isReversal; + private MappingConfigInfo _reverseConfigInfo; public ConfiguredDataSourceFactory( MappingConfigInfo configInfo, ConfiguredLambdaInfo dataSourceLambda, QualifiedMember targetMember) - : base(configInfo, targetMember) + : base(configInfo, dataSourceLambda, targetMember) { - _dataSourceLambda = dataSourceLambda; } public ConfiguredDataSourceFactory( @@ -36,24 +28,18 @@ public ConfiguredDataSourceFactory( ConfiguredLambdaInfo dataSourceLambda, LambdaExpression targetMemberLambda, bool valueCouldBeSourceMember) - : base(configInfo, targetMemberLambda) + : base(configInfo, dataSourceLambda, targetMemberLambda, valueCouldBeSourceMember) { - _valueCouldBeSourceMember = valueCouldBeSourceMember; - _dataSourceLambda = dataSourceLambda; } - public bool IsForToTargetDataSource => TargetMember.IsRoot; - - public bool IsSequential => ConfigInfo.IsSequentialConfiguration; - public bool CannotBeReversed(out string reason) => CannotBeReversed(out _, out reason); private bool CannotBeReversed(out QualifiedMember targetMember, out string reason) { - if (_valueCouldBeSourceMember == false) + if (ValueCouldBeSourceMember == false) { targetMember = null; - reason = $"configured value '{_dataSourceLambda.GetDescription(ConfigInfo)}' is not a source member"; + reason = $"configured value '{DataSourceLambda.GetDescription(ConfigInfo)}' is not a source member"; return true; } @@ -71,10 +57,10 @@ private bool CannotBeReversed(out QualifiedMember targetMember, out string reaso return true; } - if (!_dataSourceLambda.TryGetSourceMember(out var sourceMemberLambda)) + if (!DataSourceLambda.TryGetSourceMember(out var sourceMemberLambda)) { targetMember = null; - reason = $"configured value '{_dataSourceLambda.GetDescription(ConfigInfo)}' is not a source member"; + reason = $"configured value '{DataSourceLambda.GetDescription(ConfigInfo)}' is not a source member"; return true; } @@ -96,7 +82,7 @@ private bool CannotBeReversed(out QualifiedMember targetMember, out string reaso } - public ConfiguredDataSourceFactory CreateReverseIfAppropriate(bool isAutoReversal) + public ConfiguredDataSourceFactoryBase CreateReverseIfAppropriate(bool isAutoReversal) { if (CannotBeReversed(out var targetMember, out _)) { @@ -131,143 +117,36 @@ public MappingConfigInfo GetReverseConfigInfo() .ForSourceValueType(TargetMember.Type); } - public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) - { - if (!base.ConflictsWith(otherConfiguredItem)) - { - return false; - } - - var otherDataSource = otherConfiguredItem as ConfiguredDataSourceFactory; - var isOtherDataSource = otherDataSource != null; - var dataSourceLambdasAreTheSame = HasSameDataSourceAs(otherDataSource); - - if (WasAutoCreated && - (otherConfiguredItem is IPotentialAutoCreatedItem otherItem) && - !otherItem.WasAutoCreated) - { - return isOtherDataSource && dataSourceLambdasAreTheSame; - } - - if (isOtherDataSource == false) - { - return true; - } - - if (!ConfigInfo.HasSameTypesAs(otherDataSource)) - { - return dataSourceLambdasAreTheSame; - } - - if (otherDataSource.IsSequential) - { - return dataSourceLambdasAreTheSame; - } - - return true; - } - #region ConflictsWith Helpers - private bool HasSameDataSourceAs(ConfiguredDataSourceFactory otherDataSource) - => _dataSourceLambda.IsSameAs(otherDataSource?._dataSourceLambda); - protected override bool MembersConflict(UserConfiguredItemBase otherConfiguredItem) => TargetMember.LeafMember.Equals(otherConfiguredItem.TargetMember.LeafMember); #endregion - public string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + protected override string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource) { - var toTarget = TargetMember.IsRoot - ? conflictingDataSource.IsSequential ? "ToTarget() " : "ToTargetInstead() " - : null; - - var existingDataSource = conflictingDataSource.GetDataSourceDescription(); - - var reason = conflictingDataSource._isReversal + return conflictingDataSource is ConfiguredDataSourceFactory dsf && dsf._isReversal ? " from an automatically-configured reverse data source" : null; - - return $"{GetTargetMemberPath()} already has configured {toTarget}data source {existingDataSource}{reason}"; } - public string GetDescription() - { - var sourceMemberPath = GetDataSourceDescription(); - var targetMemberPath = GetTargetMemberPath(); - - return sourceMemberPath + " -> " + targetMemberPath; - } - - private string GetDataSourceDescription() - { - var description = _dataSourceLambda.GetDescription(ConfigInfo); - - return _dataSourceLambda.IsSourceMember ? description : "'" + description + "'"; - } - - private string GetTargetMemberPath() => TargetMember.GetFriendlyTargetPath(ConfigInfo); - - public override bool AppliesTo(IQualifiedMemberContext context) - => base.AppliesTo(context) && _dataSourceLambda.Supports(context.RuleSet); - - protected override bool TargetMembersAreCompatible(IQualifiedMember otherTargetMember) - { - if (base.TargetMembersAreCompatible(otherTargetMember)) - { - return true; - } - - return TargetMember.IsRoot && TargetMember.HasCompatibleType(otherTargetMember.Type); - } - - public IConfiguredDataSource Create(IMemberMapperData mapperData) - { - var configuredCondition = GetConditionOrNull(mapperData); - var value = _dataSourceLambda.GetBody(mapperData); - - return new ConfiguredDataSource( - configuredCondition, - value, - ConfigInfo.IsSequentialConfiguration, - mapperData); - } - - public QualifiedMember ToSourceMemberOrNull() - { - if (_valueCouldBeSourceMember && - _dataSourceLambda.TryGetSourceMember(out var sourceMemberLambda)) - { - return sourceMemberLambda.ToSourceMemberOrNull(ConfigInfo.MapperContext); - } - - return null; - } - - protected override int? GetSameTypesOrder(UserConfiguredItemBase other) - => ((ConfiguredDataSourceFactory)other).IsSequential ? -1 : base.GetSameTypesOrder(other); + protected override bool TargetMembersAreCompatibleForToTarget(QualifiedMember otherTargetMember) + => TargetMember.HasCompatibleType(otherTargetMember.Type); #region IPotentialAutoCreatedItem Members - public bool WasAutoCreated { get; private set; } - - public IPotentialAutoCreatedItem Clone() + public override IPotentialAutoCreatedItem Clone() { - return new ConfiguredDataSourceFactory(ConfigInfo, _dataSourceLambda, TargetMember) + return new ConfiguredDataSourceFactory(ConfigInfo, DataSourceLambda, TargetMember) { - _valueCouldBeSourceMember = _valueCouldBeSourceMember, + ValueCouldBeSourceMember = ValueCouldBeSourceMember, WasAutoCreated = true }; } - public bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedDataSourceFactory) + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedDataSourceFactory) => ConflictsWith((ConfiguredDataSourceFactory)autoCreatedDataSourceFactory); #endregion - -#if NET35 - int IComparable.CompareTo(ConfiguredDataSourceFactory other) - => DoComparisonTo(other); -#endif } } \ No newline at end of file diff --git a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs new file mode 100644 index 000000000..97d1c43b2 --- /dev/null +++ b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs @@ -0,0 +1,179 @@ +namespace AgileObjects.AgileMapper.Configuration.DataSources +{ +#if NET35 + using System; + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using AgileMapper.DataSources; + using Lambdas; + using Members; + + internal abstract class ConfiguredDataSourceFactoryBase : + UserConfiguredItemBase, + IPotentialAutoCreatedItem +#if NET35 + , IComparable +#endif + { + protected ConfiguredDataSourceFactoryBase( + MappingConfigInfo configInfo, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember targetMember) + : base(configInfo, targetMember) + { + DataSourceLambda = dataSourceLambda; + } + + protected ConfiguredDataSourceFactoryBase( + MappingConfigInfo configInfo, + ConfiguredLambdaInfo dataSourceLambda, + LambdaExpression targetMemberLambda, + bool valueCouldBeSourceMember) + : base(configInfo, targetMemberLambda) + { + DataSourceLambda = dataSourceLambda; + ValueCouldBeSourceMember = valueCouldBeSourceMember; + } + + public bool IsForToTargetDataSource => TargetMember.IsRoot; + + public bool IsSequential => ConfigInfo.IsSequentialConfiguration; + + protected ConfiguredLambdaInfo DataSourceLambda { get; } + + protected bool ValueCouldBeSourceMember { get; set; } + + public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) + { + if (!base.ConflictsWith(otherConfiguredItem)) + { + return false; + } + + var otherDataSource = otherConfiguredItem as ConfiguredDataSourceFactoryBase; + var isOtherDataSource = otherDataSource != null; + var dataSourceLambdasAreTheSame = HasSameDataSourceAs(otherDataSource); + + if (WasAutoCreated && + (otherConfiguredItem is IPotentialAutoCreatedItem otherItem) && + !otherItem.WasAutoCreated) + { + return isOtherDataSource && dataSourceLambdasAreTheSame; + } + + if (isOtherDataSource == false) + { + return true; + } + + if (!ConfigInfo.HasSameTypesAs(otherDataSource)) + { + return dataSourceLambdasAreTheSame; + } + + if (otherDataSource.IsSequential) + { + return dataSourceLambdasAreTheSame; + } + + return true; + } + + #region ConflictsWith Helpers + + private bool HasSameDataSourceAs(ConfiguredDataSourceFactoryBase otherDataSource) + => DataSourceLambda.IsSameAs(otherDataSource?.DataSourceLambda); + + #endregion + + public string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) + { + var toTarget = TargetMember.IsRoot + ? conflictingDataSource.IsSequential ? "ToTarget() " : "ToTargetInstead() " + : null; + + var existingDataSource = conflictingDataSource.GetDataSourceDescription(); + + var reason = GetConflictReasonOrNull(conflictingDataSource); + + return $"{GetTargetMemberPath()} already has configured {toTarget}data source {existingDataSource}{reason}"; + } + + protected abstract string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource); + + public string GetDescription() + { + var sourceMemberPath = GetDataSourceDescription(); + var targetMemberPath = GetTargetMemberPath(); + + return sourceMemberPath + " -> " + targetMemberPath; + } + + protected string GetDataSourceDescription() + { + var description = DataSourceLambda.GetDescription(ConfigInfo); + + return DataSourceLambda.IsSourceMember ? description : "'" + description + "'"; + } + + protected string GetTargetMemberPath() => TargetMember.GetFriendlyTargetPath(ConfigInfo); + + public override bool AppliesTo(IQualifiedMemberContext context) + => base.AppliesTo(context) && DataSourceLambda.Supports(context.RuleSet); + + protected override bool TargetMembersAreCompatible(QualifiedMember otherTargetMember) + { + if (base.TargetMembersAreCompatible(otherTargetMember)) + { + return true; + } + + return TargetMember.IsRoot && TargetMembersAreCompatibleForToTarget(otherTargetMember); + } + + protected abstract bool TargetMembersAreCompatibleForToTarget(QualifiedMember otherTargetMember); + + public IConfiguredDataSource Create(IMemberMapperData mapperData) + { + var configuredCondition = GetConditionOrNull(mapperData); + var value = DataSourceLambda.GetBody(mapperData); + + return new ConfiguredDataSource( + configuredCondition, + value, + ConfigInfo.IsSequentialConfiguration, + mapperData); + } + + public QualifiedMember ToSourceMemberOrNull() + { + if (ValueCouldBeSourceMember && + DataSourceLambda.TryGetSourceMember(out var sourceMemberLambda)) + { + return sourceMemberLambda.ToSourceMemberOrNull(ConfigInfo.MapperContext); + } + + return null; + } + + protected override int? GetSameTypesOrder(UserConfiguredItemBase other) + => ((ConfiguredDataSourceFactoryBase)other).IsSequential ? -1 : base.GetSameTypesOrder(other); + + #region IPotentialAutoCreatedItem Members + + public bool WasAutoCreated { get; protected set; } + + public abstract IPotentialAutoCreatedItem Clone(); + + public abstract bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedDataSourceFactory); + + #endregion + +#if NET35 + int IComparable.CompareTo(ConfiguredDataSourceFactoryBase other) + => DoComparisonTo(other); +#endif + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs new file mode 100644 index 000000000..07cbdffa9 --- /dev/null +++ b/AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs @@ -0,0 +1,91 @@ +namespace AgileObjects.AgileMapper.Configuration.DataSources +{ + using System; +#if NET35 + using Microsoft.Scripting.Ast; + using Extensions.Internal; +#else + using System.Linq.Expressions; +#endif + using Lambdas; + using Members; +#if NET35 + using LinqExp = System.Linq.Expressions; +#endif + + internal class ConfiguredFilterDataSourceFactory : ConfiguredDataSourceFactoryBase + { + private readonly Expression _targetMemberFilterExpression; + private readonly Func _targetMemberFilter; +#if NET35 + public ConfiguredFilterDataSourceFactory( + MappingConfigInfo configInfo, + LinqExp.Expression> targetMemberFilter, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember toTargetMember) + : this( + configInfo, + targetMemberFilter.ToDlrExpression(), + dataSourceLambda, + toTargetMember) + { + } +#endif + + public ConfiguredFilterDataSourceFactory( + MappingConfigInfo configInfo, + Expression> targetMemberFilter, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember toTargetMember) + : this( + configInfo, + targetMemberFilter?.Body, + targetMemberFilter?.Compile() ?? (_ => true), + dataSourceLambda, + toTargetMember) + { + } + + public ConfiguredFilterDataSourceFactory( + MappingConfigInfo configInfo, + Expression targetMemberFilterExpression, + Func targetMemberFilter, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember toTargetMember) + : base(configInfo, dataSourceLambda, toTargetMember) + { + _targetMemberFilterExpression = targetMemberFilterExpression; + _targetMemberFilter = targetMemberFilter; + } + + protected override string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource) + => null; + + protected override bool TargetMembersAreCompatibleForToTarget(QualifiedMember otherTargetMember) + => MatchesTargetMember(otherTargetMember); + + private bool MatchesTargetMember(QualifiedMember targetMember) + => _targetMemberFilter.Invoke(new TargetMemberSelector(targetMember)); + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new ConfiguredFilterDataSourceFactory( + ConfigInfo, + _targetMemberFilterExpression, + _targetMemberFilter, + DataSourceLambda, + TargetMember) + { + ValueCouldBeSourceMember = ValueCouldBeSourceMember, + WasAutoCreated = true + }; + } + + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedDataSourceFactory) + => false; + + #endregion + } +} diff --git a/AgileMapper/Configuration/DataSourceReversalSetting.cs b/AgileMapper/Configuration/DataSources/DataSourceReversalSetting.cs similarity index 97% rename from AgileMapper/Configuration/DataSourceReversalSetting.cs rename to AgileMapper/Configuration/DataSources/DataSourceReversalSetting.cs index 3b2aa1c9c..8c5136880 100644 --- a/AgileMapper/Configuration/DataSourceReversalSetting.cs +++ b/AgileMapper/Configuration/DataSources/DataSourceReversalSetting.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Configuration +namespace AgileObjects.AgileMapper.Configuration.DataSources { #if NET35 using System; diff --git a/AgileMapper/Configuration/DataSources/IReversibleConfiguredDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/IReversibleConfiguredDataSourceFactory.cs new file mode 100644 index 000000000..cb0f8bcd2 --- /dev/null +++ b/AgileMapper/Configuration/DataSources/IReversibleConfiguredDataSourceFactory.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.Configuration.DataSources +{ + internal interface IReversibleConfiguredDataSourceFactory + { + MappingConfigInfo ConfigInfo { get; } + + ConfiguredDataSourceFactoryBase CreateReverseIfAppropriate(bool isAutoReversal); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs b/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs index 55cccbac9..169df066d 100644 --- a/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs +++ b/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs @@ -52,7 +52,7 @@ public static CustomDictionaryKey ForTargetMember( public QualifiedMember SourceMember { get; } - public string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + public string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) => $"Configured dictionary key member {TargetMember.GetPath()} has a configured data source"; public bool AppliesTo(Member member, IQualifiedMemberContext context) diff --git a/AgileMapper/Configuration/MappingConfigInfo.cs b/AgileMapper/Configuration/MappingConfigInfo.cs index ca39c2199..68e926281 100644 --- a/AgileMapper/Configuration/MappingConfigInfo.cs +++ b/AgileMapper/Configuration/MappingConfigInfo.cs @@ -264,7 +264,7 @@ public MappingConfigInfo Set(T value) return this; } - private Dictionary Data => (_data ??= new Dictionary()); + private Dictionary Data => _data ??= new Dictionary(); public IObjectMappingData ToMappingData() { diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs index 81d979fe4..979114f2a 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs @@ -52,7 +52,7 @@ private ConfiguredMemberFilter( public override string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore) => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); - public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + public override string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) { return $"Configured data source {conflictingDataSource.GetDescription()} " + $"conflicts with member ignore pattern '{TargetMemberFilter}'"; @@ -77,7 +77,7 @@ protected override bool MembersConflict(UserConfiguredItemBase otherItem) return IsFiltered(otherItem.TargetMember); } - public bool IsFiltered(QualifiedMember member) + private bool IsFiltered(QualifiedMember member) => _memberFilter.Invoke(new TargetMemberSelector(member)); #region IPotentialAutoCreatedItem Members diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs index 073512e5c..727c0cc7f 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs @@ -2,15 +2,15 @@ namespace AgileObjects.AgileMapper.Configuration.MemberIgnores { #if NET35 using Microsoft.Scripting.Ast; - using LinqExp = System.Linq.Expressions; + using Extensions.Internal; #else using System.Linq.Expressions; -#endif -#if NET35 - using Extensions.Internal; #endif using DataSources; using Members; +#if NET35 + using LinqExp = System.Linq.Expressions; +#endif internal class ConfiguredMemberIgnore : ConfiguredMemberIgnoreBase, IMemberIgnore { @@ -30,7 +30,7 @@ private ConfiguredMemberIgnore(MappingConfigInfo configInfo, QualifiedMember tar { } - public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + public override string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) { return $"Configured data source {conflictingDataSource.GetDescription()} " + "conflicts with an ignored member"; diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs index 16aa2fb2b..00cf82e5b 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs @@ -48,7 +48,7 @@ public string GetConflictMessage(UserConfiguredItemBase conflictingConfiguredIte } } - public abstract string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource); + public abstract string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource); public abstract string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore); diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs index 04f02eedc..eb5537c9f 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs @@ -3,7 +3,6 @@ namespace AgileObjects.AgileMapper.Configuration.MemberIgnores using System; #if NET35 using Microsoft.Scripting.Ast; - using LinqExp = System.Linq.Expressions; #else using System.Linq.Expressions; #endif @@ -13,7 +12,9 @@ namespace AgileObjects.AgileMapper.Configuration.MemberIgnores #endif using Members; using ReadableExpressions; - +#if NET35 + using LinqExp = System.Linq.Expressions; +#endif internal class ConfiguredSourceMemberFilter : ConfiguredSourceMemberIgnoreBase, IMemberFilterIgnore { @@ -52,7 +53,7 @@ private ConfiguredSourceMemberFilter( protected override bool ConflictsWith(QualifiedMember sourceMember) => IsFiltered(sourceMember); - public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + public override string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) { return $"Configured data source {conflictingDataSource.GetDescription()} " + $"conflicts with source member ignore pattern '{SourceMemberFilter}'"; @@ -83,8 +84,8 @@ protected override bool MembersConflict(UserConfiguredItemBase otherItem) return false; } - public bool IsFiltered(QualifiedMember member) - => _memberFilter.Invoke(new SourceMemberSelector(member)); + private bool IsFiltered(QualifiedMember sourceMember) + => _memberFilter.Invoke(new SourceMemberSelector(sourceMember)); #region IPotentialAutoCreatedItem Members diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs index 938305fa6..dc1e7317a 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs @@ -42,7 +42,7 @@ private ConfiguredSourceMemberIgnore(MappingConfigInfo configInfo, QualifiedMemb protected override bool ConflictsWith(QualifiedMember sourceMember) => SourceMember.Matches(sourceMember); - public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + public override string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) { return $"Configured data source {conflictingDataSource.GetDescription()} " + "conflicts with an ignored source member"; diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs index 8ee8fd332..89775c2eb 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs @@ -41,7 +41,7 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) protected abstract bool ConflictsWith(QualifiedMember sourceMember); - public abstract string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource); + public abstract string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource); public abstract string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore); diff --git a/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs b/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs index 5f9fec293..655a3ad15 100644 --- a/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs +++ b/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs @@ -1,11 +1,7 @@ namespace AgileObjects.AgileMapper.Configuration.MemberIgnores { - using Members; - internal interface IMemberFilterIgnore : IMemberIgnoreBase { string MemberFilter { get; } - - bool IsFiltered(QualifiedMember member); } } \ No newline at end of file diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index 3d9cddd13..23b59cf83 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -39,7 +39,7 @@ internal class UserConfigurationSet private List _ignoredMembers; private List _enumPairings; private DictionarySettings _dictionaries; - private List _dataSourceFactories; + private List _dataSourceFactories; private List _mappingCallbackFactories; private List _creationCallbackFactories; private List _exceptionCallbackFactories; @@ -102,7 +102,7 @@ public void Add(MapToNullCondition condition) ThrowIfConflictingItemExists( condition, _mapToNullConditions, - (cdn, conflicting) => cdn.GetConflictMessage()); + (cdn, _) => cdn.GetConflictMessage()); MapToNullConditions.AddThenSort(condition); } @@ -163,7 +163,7 @@ public void Add(DataSourceReversalSetting setting) public void AddReverseDataSourceFor(ConfiguredDataSourceFactory dataSourceFactory) => AddReverse(dataSourceFactory, isAutoReversal: false); - private void AddReverse(ConfiguredDataSourceFactory dataSourceFactory, bool isAutoReversal) + private void AddReverse(IReversibleConfiguredDataSourceFactory dataSourceFactory, bool isAutoReversal) { var reverseDataSourceFactory = dataSourceFactory.CreateReverseIfAppropriate(isAutoReversal); @@ -188,20 +188,17 @@ public void RemoveReverseOf(MappingConfigInfo configInfo) } } - public bool AutoDataSourceReversalEnabled(ConfiguredDataSourceFactory dataSourceFactory) - => AutoDataSourceReversalEnabled(dataSourceFactory, dsf => dsf.ConfigInfo.ToMemberContext(dsf.TargetMember)); + public bool AutoDataSourceReversalEnabled(IReversibleConfiguredDataSourceFactory dataSourceFactory) + => AutoDataSourceReversalEnabled(dataSourceFactory.ConfigInfo); public bool AutoDataSourceReversalEnabled(MappingConfigInfo configInfo) - => AutoDataSourceReversalEnabled(configInfo, ci => ci.ToMemberContext()); - - private bool AutoDataSourceReversalEnabled(T dataItem, Func memberContextFactory) { if (_dataSourceReversalSettings == null) { return false; } - var memberContext = memberContextFactory.Invoke(dataItem); + var memberContext = configInfo.ToMemberContext(); return _dataSourceReversalSettings .FirstOrDefault(memberContext, (mc, s) => s.AppliesTo(mc))?.Reverse == true; @@ -336,7 +333,7 @@ public void Add(ConfiguredSourceValueFilter sourceValueFilter) ThrowIfConflictingItemExists( sourceValueFilter, _sourceValueFilters, - (svf, conflicting) => svf.GetConflictMessage()); + (svf, _) => svf.GetConflictMessage()); SourceValueFilters.AddOrReplaceThenSort(sourceValueFilter); } @@ -402,17 +399,17 @@ public IList GetEnumPairingsFor(Type sourceEnumType, Type target #region DataSources - private List DataSourceFactories - => _dataSourceFactories ??= new List(); + private List DataSourceFactories + => _dataSourceFactories ??= new List(); - public void Add(ConfiguredDataSourceFactory dataSourceFactory) + public void Add(ConfiguredDataSourceFactoryBase dataSourceFactory) { if (!dataSourceFactory.TargetMember.IsRoot) { ThrowIfConflictingIgnoredSourceMemberExists(dataSourceFactory, (dsf, cIsm) => cIsm.GetConflictMessage(dsf)); ThrowIfConflictingIgnoredMemberExists(dataSourceFactory); } - + ThrowIfConflictingDataSourceExists(dataSourceFactory, (dsf, cDsf) => dsf.GetConflictMessage(cDsf)); DataSourceFactories.AddOrReplaceThenSort(dataSourceFactory); @@ -423,18 +420,19 @@ public void Add(ConfiguredDataSourceFactory dataSourceFactory) return; } - if (AutoDataSourceReversalEnabled(dataSourceFactory)) + if (dataSourceFactory is IReversibleConfiguredDataSourceFactory reversibleDataSourceFactory && + AutoDataSourceReversalEnabled(reversibleDataSourceFactory)) { - AddReverse(dataSourceFactory, isAutoReversal: true); + AddReverse(reversibleDataSourceFactory, isAutoReversal: true); } } public ConfiguredDataSourceFactory GetDataSourceFactoryFor(MappingConfigInfo configInfo) - => _dataSourceFactories.First(configInfo, (ci, dsf) => dsf.ConfigInfo == ci); + => (ConfiguredDataSourceFactory)_dataSourceFactories.First(configInfo, (ci, dsf) => dsf.ConfigInfo == ci); public bool HasToTargetDataSources { get; private set; } - public IList GetRelevantDataSourceFactories(IMemberMapperData mapperData) + public IList GetRelevantDataSourceFactories(IMemberMapperData mapperData) => _dataSourceFactories.FindRelevantMatches(mapperData); public IList GetDataSourcesForToTarget(IMemberMapperData mapperData, bool? sequential) @@ -445,8 +443,8 @@ public IList GetDataSourcesForToTarget(IMemberMapperData } var toTargetDataSources = QueryDataSourceFactories(mapperData) - .Filter(dsf => - dsf.IsForToTargetDataSource && + .Filter(dsf => + dsf.IsForToTargetDataSource && (dsf.IsSequential == sequential || !sequential.HasValue)) .Project(mapperData, (md, dsf) => dsf.Create(md)) .ToArray(); @@ -460,7 +458,7 @@ public IEnumerable QueryDataSourceFactories() return _dataSourceFactories?.OfType() ?? Enumerable.Empty; } - public IEnumerable QueryDataSourceFactories(IQualifiedMemberContext context) + public IEnumerable QueryDataSourceFactories(IQualifiedMemberContext context) => _dataSourceFactories.FindMatches(context); #endregion @@ -573,8 +571,8 @@ private void ThrowIfConflictingIgnoredSourceMemberExists( where TConfiguredItem : UserConfiguredItemBase { ThrowIfConflictingItemExists( - configuredItem, - _ignoredSourceMembers, + configuredItem, + _ignoredSourceMembers, messageFactory); } @@ -594,7 +592,7 @@ private void ThrowIfConflictingIgnoredMemberExists( internal void ThrowIfConflictingDataSourceExists( TConfiguredItem configuredItem, - Func messageFactory) + Func messageFactory) where TConfiguredItem : UserConfiguredItemBase { ThrowIfConflictingItemExists(configuredItem, _dataSourceFactories, messageFactory); diff --git a/AgileMapper/Configuration/UserConfiguredItemBase.cs b/AgileMapper/Configuration/UserConfiguredItemBase.cs index 41646ab53..36954b14b 100644 --- a/AgileMapper/Configuration/UserConfiguredItemBase.cs +++ b/AgileMapper/Configuration/UserConfiguredItemBase.cs @@ -127,7 +127,7 @@ private bool TargetMembersMatch(IQualifiedMemberContext context) otherTargetMember.LeafMember.DeclaringType.IsAssignableTo(TargetMember.LeafMember.DeclaringType); } - protected virtual bool TargetMembersAreCompatible(IQualifiedMember otherTargetMember) + protected virtual bool TargetMembersAreCompatible(QualifiedMember otherTargetMember) => TargetMember == otherTargetMember; private bool HasCompatibleCondition(IRuleSetOwner ruleSetOwner) diff --git a/AgileMapper/DataSources/Factories/DataSourceFindContext.cs b/AgileMapper/DataSources/Factories/DataSourceFindContext.cs index b873c8841..65c446a2c 100644 --- a/AgileMapper/DataSources/Factories/DataSourceFindContext.cs +++ b/AgileMapper/DataSources/Factories/DataSourceFindContext.cs @@ -10,7 +10,7 @@ internal class DataSourceFindContext : IDataSourceSetInfo { - private IList _relevantConfiguredDataSourceFactories; + private IList _relevantConfiguredDataSourceFactories; private IList _configuredDataSources; private SourceMemberMatchContext _sourceMemberMatchContext; private SourceMemberMatch _bestSourceMemberMatch; @@ -34,10 +34,21 @@ public DataSourceFindContext(IChildMemberMappingData memberMappingData) public bool StopFind { get; set; } - private IEnumerable RelevantConfiguredDataSourceFactories + public IList ConfiguredDataSources + { + get + { + return _configuredDataSources ??= RelevantConfiguredDataSourceFactories + .FindMatches(MemberMapperData) + .Project(MemberMapperData, (md, dsf) => dsf.Create(md)) + .ToArray(); + } + } + + private IEnumerable RelevantConfiguredDataSourceFactories => _relevantConfiguredDataSourceFactories ??= GetRelevantConfiguredDataSourceFactories(); - private IList GetRelevantConfiguredDataSourceFactories() + private IList GetRelevantConfiguredDataSourceFactories() { var relevantDataSourceFactories = GetRelevantConfiguredDataSourceFactories(MemberMapperData); @@ -56,20 +67,9 @@ private IList GetRelevantConfiguredDataSourceFactor return relevantDataSourceFactories; } - private IList GetRelevantConfiguredDataSourceFactories(IMemberMapperData mapperData) + private IList GetRelevantConfiguredDataSourceFactories(IMemberMapperData mapperData) => MapperContext.UserConfigurations.GetRelevantDataSourceFactories(mapperData); - public IList ConfiguredDataSources - { - get - { - return _configuredDataSources ??= RelevantConfiguredDataSourceFactories - .FindMatches(MemberMapperData) - .Project(MemberMapperData, (md, dsf) => dsf.Create(md)) - .ToArray(); - } - } - public IDataSource MatchingSourceMemberDataSource => _matchingSourceMemberDataSource ??= GetSourceMemberDataSource(); diff --git a/AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs b/AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs index a46164cf4..16aa2c6f3 100644 --- a/AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs +++ b/AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs @@ -14,7 +14,6 @@ using NetStandardPolyfills; using ObjectPopulation; using ObjectPopulation.Enumerables; - using ReadableExpressions.Extensions; using TypeConversion; internal static class MetaMemberDataSourcesFactory @@ -140,7 +139,7 @@ private static bool TryGetMetaMember( var currentMemberPart = metaMember = default(MetaMemberPartBase); Func currentMappingDataFactory = - (sm, tm, md, c) => c.MemberMappingData.Parent; + (_, _, _, c) => c.MemberMappingData.Parent; for (var i = memberNameParts.Count - 1; i >= 0; --i) { diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 49b6381e0..61d0a4fcf 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -321,15 +321,15 @@ public static Expression GetRootExpression(this Expression expression) public static Expression ToExpression(this IList expressions) => expressions.HasOne() ? expressions.First() : Expression.Block(expressions); - public static bool TryGetVariableAssignment(this IList mappingExpressions, out BinaryExpression binaryExpression) + public static bool TryGetVariableAssignment(this IList mappingExpressions, out BinaryExpression assignment) { - if (mappingExpressions.TryFindMatch(exp => exp.NodeType == Assign, out var assignment)) + if (mappingExpressions.TryFindMatch(exp => exp.NodeType == Assign, out var assignmentExpression)) { - binaryExpression = (BinaryExpression)assignment; + assignment = (BinaryExpression)assignmentExpression; return true; } - binaryExpression = null; + assignment = null; return false; } #if NET35 diff --git a/AgileMapper/Members/MemberExtensionMethods.cs b/AgileMapper/Members/MemberExtensionMethods.cs index 47adfb881..24a5d63f5 100644 --- a/AgileMapper/Members/MemberExtensionMethods.cs +++ b/AgileMapper/Members/MemberExtensionMethods.cs @@ -332,7 +332,7 @@ public static QualifiedMember ToTargetMemberOrNull( this LambdaExpression memberAccess, MapperContext mapperContext) { - return memberAccess.ToTargetMember(mapperContext, nt => { }); + return memberAccess.ToTargetMember(mapperContext, _ => { }); } public static QualifiedMember ToTargetMemberOrNull( diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 9a0b9ecb5..d9a2fed37 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -224,7 +224,7 @@ public static void MergeTargetMemberDataSources( public static bool TargetMemberIsUnmappable( this TTMapperData mapperData, QualifiedMember targetMember, - Func> configuredDataSourcesFactory, + Func> configuredDataSourcesFactory, UserConfigurationSet userConfigurations, out string reason) where TTMapperData : IQualifiedMemberContext diff --git a/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs b/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs index ad5009a0b..595e67acd 100644 --- a/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs +++ b/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs @@ -41,7 +41,7 @@ public static Expression GetMappingOrNull( return mapping; } - public static Expression GetMappingOrNull( + private static Expression GetMappingOrNull( IObjectMappingData mappingData, out bool isConditional) { diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index fe6c32f4c..b6a17ef3d 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -4,6 +4,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Collections.Generic; using System.Linq; using ComplexTypes.ShortCircuits; + using DataSources; #if NET35 using Microsoft.Scripting.Ast; #else @@ -100,21 +101,20 @@ private bool ShortCircuitMapping(MappingCreationContext context) } context.MappingExpressions.Add(returnLabel); + return true; } - else - { - if (isConditional) - { - context.MappingExpressions.Add(mapping); - continue; - } - AddPopulationsAndCallbacks( - mapping, - context, - (m, ctx) => ctx.MappingExpressions.Add(m)); + if (isConditional) + { + context.MappingExpressions.Add(mapping); + continue; } + AddPopulationsAndCallbacks( + mapping, + context, + (m, ctx) => ctx.MappingExpressions.Add(m)); + return true; } @@ -134,10 +134,23 @@ private Expression GetConfiguredAlternateDataSourceMappingOrNull( MappingCreationContext context, out bool isConditional) { - isConditional = false; + var toTargetDataSource = context + .ToTargetDataSources + .FirstOrDefault(ds => ds.IsSequential == false); - return GetConfiguredToTargetDataSourceMappings(context, sequential: false) - .FirstOrDefault(); + if (toTargetDataSource == null) + { + isConditional = false; + return null; + } + + //isConditional = false; + isConditional = toTargetDataSource.HasConfiguredCondition; + + return GetConfiguredToTargetDataSourceMappingOrNull( + context, + toTargetDataSource, + isFirstDataSource: true); } private void InsertShortCircuitReturns(MappingCreationContext context) @@ -171,9 +184,7 @@ private void AddPopulationsAndCallbacks(MappingCreationContext context) AddPopulationsAndCallbacks(this, context, (factory, ctx) => { factory.AddObjectPopulation(ctx); - - ctx.MappingExpressions.AddRange( - GetConfiguredToTargetDataSourceMappings(ctx, sequential: true)); + ctx.MappingExpressions.AddRange(GetConfiguredToTargetDataSourceMappings(ctx)); }); } @@ -189,9 +200,8 @@ private static void AddPopulationsAndCallbacks( protected abstract void AddObjectPopulation(MappingCreationContext context); - protected IEnumerable GetConfiguredToTargetDataSourceMappings( - MappingCreationContext context, - bool sequential) + private IEnumerable GetConfiguredToTargetDataSourceMappings( + MappingCreationContext context) { if (context.MapperData.Context.IsForToTargetMapping) { @@ -200,67 +210,113 @@ protected IEnumerable GetConfiguredToTargetDataSourceMappings( var toTargetDataSources = context .ToTargetDataSources - .FilterToArray(sequential, (seq, cds) => cds.IsSequential == seq); + .Filter(cds => cds.IsSequential); - if (toTargetDataSources.None()) - { - yield break; - } + var i = 0; - for (var i = 0; i < toTargetDataSources.Count; ++i) + foreach (var toTargetDataSource in toTargetDataSources) { - var toTargetDataSource = toTargetDataSources[i]; - var toTargetContext = context.WithToTargetDataSource(toTargetDataSource); + var toTargetMapping = GetConfiguredToTargetDataSourceMappingOrNull( + context, + toTargetDataSource, + isFirstDataSource: i == 0); - AddPopulationsAndCallbacks(toTargetContext); + ++i; - if (toTargetContext.MappingExpressions.None()) + if (toTargetMapping != null) { - continue; + yield return toTargetMapping; } + } + } - context.UpdateFrom(toTargetContext, toTargetDataSource); + private Expression GetConfiguredToTargetDataSourceMappingOrNull( + MappingCreationContext context, + IConfiguredDataSource toTargetDataSource, + bool isFirstDataSource) + { + if (context.MapperData.Context.IsForToTargetMapping) + { + return null; + } - var mapperData = context.MapperData; + var toTargetContext = context.WithToTargetDataSource(toTargetDataSource); - var toTargetMapping = toTargetContext.GetMappingExpression(); + AddPopulationsAndCallbacks(toTargetContext); - toTargetMapping = MappingFactory.UseLocalToTargetDataSourceVariableIfAppropriate( - mapperData, - toTargetContext.MapperData, - toTargetDataSource.Value, - toTargetMapping); + if (toTargetContext.MappingExpressions.None()) + { + return null; + } - if ((sequential && !toTargetDataSource.IsConditional) || - (!sequential && !toTargetDataSource.HasConfiguredCondition)) - { - yield return toTargetMapping; - break; - } + context.UpdateFrom(toTargetContext, toTargetDataSource); - Expression fallback; + var originalMapperData = context.MapperData; - if (mapperData.TargetMember.IsComplex || (i > 0)) - { - if (sequential || !mapperData.TargetMemberIsEnumerableElement()) - { - yield return Expression.IfThen(toTargetDataSource.Condition, toTargetMapping); - continue; - } + var toTargetMapping = MappingFactory.UseLocalToTargetDataSourceVariableIfAppropriate( + originalMapperData, + toTargetContext.MapperData, + toTargetDataSource.Value, + toTargetContext.GetMappingExpression()); - fallback = mapperData.GetTargetMemberDefault(); - } - else + var isSequential = toTargetDataSource.IsSequential; + + var isConditional = isSequential + ? toTargetDataSource.IsConditional + : toTargetDataSource.HasConfiguredCondition; + + if (!isConditional) + { + return toTargetMapping; + } + + Expression fallback; + + if (!isFirstDataSource || originalMapperData.TargetMember.IsComplex) + { + if (isSequential) { - fallback = mapperData.LocalVariable.Type.GetEmptyInstanceCreation( - context.TargetMember.ElementType, - mapperData.EnumerablePopulationBuilder.TargetTypeHelper); + return Expression.IfThen(toTargetDataSource.Condition, toTargetMapping); } - var assignFallback = mapperData.LocalVariable.AssignTo(fallback); + if (context.MapperData.TargetMemberIsEnumerableElement()) + { + fallback = originalMapperData.GetTargetMemberDefault(); + goto AssignFallback; + } - yield return Expression.IfThenElse(toTargetDataSource.Condition, toTargetMapping, assignFallback); + return Expression.IfThenElse( + toTargetDataSource.Condition, + toTargetMapping, + GetDefaultMappingFallback(context)); } + + fallback = originalMapperData.LocalVariable.Type.GetEmptyInstanceCreation( + context.TargetMember.ElementType, + originalMapperData.EnumerablePopulationBuilder.TargetTypeHelper); + + AssignFallback: + var assignFallback = originalMapperData.LocalVariable.AssignTo(fallback); + + return Expression.IfThenElse(toTargetDataSource.Condition, toTargetMapping, assignFallback); + } + + private Expression GetDefaultMappingFallback(MappingCreationContext context) + { + //var mappingExpressionsCount = context.MappingExpressions.Count; + + AddPopulationsAndCallbacks(context); + + //var fallbackMappingStartIndex = mappingExpressionsCount - 1; + //var fallbackMappingEndIndex = context.MappingExpressions.Count - mappingExpressionsCount; + + //var fallbackMappingExpressions = context.MappingExpressions + // .GetRange(fallbackMappingStartIndex, fallbackMappingEndIndex); + + //context.MappingExpressions + // .RemoveRange(fallbackMappingStartIndex, fallbackMappingEndIndex); + + return context.GetMappingExpression(); } private static bool NothingIsBeingMapped(MappingCreationContext context) @@ -403,7 +459,8 @@ private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationC } if ((localVariableAssignment.Left.NodeType != Parameter) || - (localVariableAssignment != context.MappingExpressions.Last())) + (localVariableAssignment != context.MappingExpressions.Last()) || + (context.PreMappingCallback == null)) { returnExpression = null; return false; diff --git a/AgileMapper/ObjectPopulation/ObjectMapper.cs b/AgileMapper/ObjectPopulation/ObjectMapper.cs index 9a1897d80..912e2ec5b 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapper.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapper.cs @@ -89,7 +89,7 @@ public void CacheRepeatedMappingFuncs() mapperKey.MappingData, mapperKey.MappingData.MappingContext.LazyLoadRepeatMappingFuncs); - _repeatedMappingFuncsByKey.GetOrAdd(mapperKey, k => mapperFunc); + _repeatedMappingFuncsByKey.GetOrAdd(mapperKey, _ => mapperFunc); } } From 72c66c8ab7d719dac5dedd0cb8ef6b00042c864f Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 28 Apr 2021 10:01:41 +0100 Subject: [PATCH 2/5] Member matcher data sources / Conflict testing / Documentation improvements --- .../AgileMapper.UnitTests.Orms.EfCore1.csproj | 11 +- ...s => WhenConfiguringMatcherDataSources.cs} | 9 +- ...nfiguringMatcherDataSourcesIncorrectly.cs} | 30 +++- .../ConfigInfoDataSourceExtensions.cs | 12 +- .../CustomDataSourceTargetMemberSpecifier.cs | 10 +- .../ICustomDataSourceTargetMemberSpecifier.cs | 6 +- .../Api/Configuration/MappingConfigurator.cs | 4 +- .../ConfiguredDataSourceFactory.cs | 19 ++- .../ConfiguredDataSourceFactoryBase.cs | 42 ++---- .../ConfiguredFilterDataSourceFactory.cs | 91 ------------ .../ConfiguredMatcherDataSourceFactory.cs | 138 ++++++++++++++++++ AgileMapper/Configuration/IHasMemberFilter.cs | 7 + .../MemberIgnores/ConfiguredMemberFilter.cs | 27 ++-- .../ConfiguredSourceMemberIgnoreBase.cs | 2 +- .../Configuration/UserConfiguredItemBase.cs | 4 +- .../DataSources/ConfiguredDataSource.cs | 6 + .../DataSources/IConfiguredDataSource.cs | 2 + AgileMapper/DataSources/IDataSource.cs | 9 ++ .../Internal/ExpressionExtensions.cs | 33 +++++ .../ConfiguredMappingFactory.cs | 2 + .../ObjectPopulation/DerivedMappingFactory.cs | 12 -- .../DictionaryPopulationBuilder.cs | 3 +- .../EnumerableExpressionExtensions.cs | 2 +- .../MappingCreationContext.cs | 47 ++---- .../MappingExpressionFactoryBase.cs | 121 ++++++++------- .../ObjectPopulation/ObjectMapperData.cs | 20 +++ 26 files changed, 396 insertions(+), 273 deletions(-) rename AgileMapper.UnitTests/Configuration/DataSources/{WhenConfiguringDataSourcesByFilter.cs => WhenConfiguringMatcherDataSources.cs} (93%) rename AgileMapper.UnitTests/Configuration/DataSources/{WhenConfiguringDataSourcesByFilterIncorrectly.cs => WhenConfiguringMatcherDataSourcesIncorrectly.cs} (67%) delete mode 100644 AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs create mode 100644 AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs create mode 100644 AgileMapper/Configuration/IHasMemberFilter.cs diff --git a/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj b/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj index eac0b8a9f..72b747f3d 100644 --- a/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj +++ b/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj @@ -12,19 +12,12 @@
- $(DefineConstants);NETCOREAPP1_0;NET_STANDARD;NET_STANDARD_1;TRACE;FEATURE_DYNAMIC;FEATURE_ISET - - - - $(DefineConstants);DEBUG - - - - $(DefineConstants);RELEASE + $(DefineConstants);NETCOREAPP1_0;TRACE;FEATURE_DYNAMIC;FEATURE_ISET + all diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs similarity index 93% rename from AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs index c39c01a8d..66c53c4fb 100644 --- a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilter.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs @@ -11,7 +11,7 @@ [NUnit.Framework.TestFixture] #endif // See https://github.com/agileobjects/AgileMapper/issues/208 - public class WhenConfiguringDataSourcesByFilter + public class WhenConfiguringMatcherDataSources { [Fact] public void ShouldApplyAConstantByTargetMemberTypeAndMemberType() @@ -45,6 +45,13 @@ public void ShouldApplyAnAlternateConstantByTargetTypeAndTargetMemberType() .Map("Hurrah!") .ToTargetInstead(); + mapper.WhenMapping + .From>() + .To>() + .Map((s, t) => s.Value1).To(t => t.Value2) + .And + .Map((s, t) => s.Value2).To(t => t.Value1); + var source = new PublicTwoFields { Value1 = 123, diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs similarity index 67% rename from AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs rename to AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs index 9d90ca7eb..7070c541e 100644 --- a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringDataSourcesByFilterIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs @@ -11,7 +11,7 @@ [NUnit.Framework.TestFixture] #endif // See https://github.com/agileobjects/AgileMapper/issues/208 - public class WhenConfiguringDataSourcesByFilterIncorrectly + public class WhenConfiguringMatcherDataSourcesIncorrectly { [Fact] public void ShouldErrorIfMemberIgnoreSpecified() @@ -54,6 +54,30 @@ public void ShouldErrorIfDataSourceTargetMemberSpecified() configEx.Message.ShouldContain("PublicTwoFields.Value1"); } + [Fact] + public void ShouldErrorIfConflictingMatcherDataSourceConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To() + .IfTargetMembersMatch(member => member.Name == "AlwaysTrue") + .Map(true).ToTarget(); + + mapper.WhenMapping + .From().To() + .IfTargetMembersMatch(member => member.Name == "AlwaysTrue") + .Map(false).ToTarget(); + } + }); + + configEx.Message.ShouldContain("already has"); + configEx.Message.ShouldContain("'If mapping string -> bool and member.Name == \"AlwaysTrue\","); + configEx.Message.ShouldContain("map 'true' to target'"); + } + [Fact] public void ShouldErrorIfConflictingMemberFilterConfigured() { @@ -73,7 +97,9 @@ public void ShouldErrorIfConflictingMemberFilterConfigured() } }); - configEx.Message.ShouldContain("target member filter"); + configEx.Message.ShouldContain("'If mapping -> PublicTwoFields"); + configEx.Message.ShouldContain("and member.HasType(), map '\"Yippee!\"' to target members'"); + configEx.Message.ShouldContain("member ignore pattern 'member.HasType()'"); } } } diff --git a/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs b/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs index 56e074416..33f252f4b 100644 --- a/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs +++ b/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs @@ -32,10 +32,10 @@ public static MappingConfigInfo SetSequenceDataSourceFactories( return configInfo.Set(dataSourceFactorySequence); } - public static bool HasTargetMemberFilter(this MappingConfigInfo configInfo) - => configInfo.HasTargetMemberFilter(out _); + public static bool HasTargetMemberMatcher(this MappingConfigInfo configInfo) + => configInfo.HasTargetMemberMatcher(out _); - public static bool HasTargetMemberFilter( + public static bool HasTargetMemberMatcher( this MappingConfigInfo configInfo, out Expression> targetMemberFilter) { @@ -43,19 +43,19 @@ public static bool HasTargetMemberFilter( return targetMemberFilter != null; } - public static MappingConfigInfo SetTargetMemberFilter( + public static MappingConfigInfo SetTargetMemberMatcher( this MappingConfigInfo configInfo, Expression> memberMatcherLambda) { return configInfo.Set(memberMatcherLambda); } - public static void ThrowIfTargetMemberFilterSpecified( + public static void ThrowIfTargetMemberMatcherSpecified( this MappingConfigInfo configInfo, Func configDescriptionFactory, params Expression>[] targetMembers) { - if (!configInfo.HasTargetMemberFilter(out var filter)) + if (!configInfo.HasTargetMemberMatcher(out var filter)) { return; } diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index 065fb1ed3..22b24920c 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -110,7 +110,7 @@ private static void ThrowIfTargetParameterSpecified(LambdaExpression targetMembe private void ThrowIfTargetMemberFilterSpecified( Expression> targetMember) { - _configInfo.ThrowIfTargetMemberFilterSpecified( + _configInfo.ThrowIfTargetMemberMatcherSpecified( configDescriptionFactory: ci => $"data source mapping '{GetValueLambdaInfo().GetDescription(ci)}' -> ", targetMember); @@ -245,7 +245,7 @@ private ConfiguredLambdaInfo GetValueLambdaInfo(Type targetValueType) if ((customValueLambda.Body.NodeType != CONSTANT) || (targetValueType == typeof(object)) || customValueLambda.ReturnType.IsAssignableTo(targetValueType) || - _configInfo.HasTargetMemberFilter()) + _configInfo.HasTargetMemberMatcher()) { return _customValueLambdaInfo = ConfiguredLambdaInfo.For(customValueLambda, _configInfo); } @@ -441,9 +441,9 @@ private ConfiguredDataSourceFactoryBase CreateForToTarget(bool isSequential) var dataSourceLambda = GetValueLambdaInfo(); var toTargetMember = CreateToTargetQualifiedMember(); - if (_configInfo.HasTargetMemberFilter(out var filter)) + if (_configInfo.HasTargetMemberMatcher(out var filter)) { - return new ConfiguredFilterDataSourceFactory( + return new ConfiguredMatcherDataSourceFactory( _configInfo, filter, dataSourceLambda, @@ -548,7 +548,7 @@ private void ThrowIfSimpleSourceForNonSimpleTargetMember(Type targetMemberType) targetMemberType.IsSimple() || !ConfiguredSourceType.IsSimple() || ConversionOperatorExists(targetMemberType) || - _configInfo.HasTargetMemberFilter()) + _configInfo.HasTargetMemberMatcher()) { return; } diff --git a/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs index fac457d17..c6e45e187 100644 --- a/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs @@ -75,10 +75,8 @@ IMappingConfigContinuation To( /// /// Map the configured source value to the target object being configured, instead of - /// mapping any matching source member. If this mapping configuration has an If() clause which - /// evaluates to false during mapping, no mapping is performed.

- /// To map any matching source member as well as the configured source value, use - /// ToTarget(). + /// mapping any matching source member. To map any matching source member as well as + /// the configured source value, use ToTarget(). ///
/// /// An IMappingConfigContinuation to enable further configuration of mappings from and to the diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index 1d49b9f47..ace3b59c0 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -174,7 +174,7 @@ public IConditionalRootMappingConfigurator If(Expression IfTargetMembersMatch( Expression> memberFilter) { - ConfigInfo.SetTargetMemberFilter(memberFilter); + ConfigInfo.SetTargetMemberMatcher(memberFilter); return this; } @@ -388,7 +388,7 @@ IProjectionConfigContinuation IRootProjectionConfigurator IgnoreTargetMembers( Expression>[] targetMembers) { - ConfigInfo.ThrowIfTargetMemberFilterSpecified( + ConfigInfo.ThrowIfTargetMemberMatcherSpecified( configDescriptionFactory: _ => "ignore(s)", targetMembers); diff --git a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs index 23de439f1..5b792bab2 100644 --- a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs +++ b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactory.cs @@ -53,7 +53,7 @@ private bool CannotBeReversed(out QualifiedMember targetMember, out string reaso if (!TargetMember.IsReadable) { targetMember = null; - reason = $"target member '{GetTargetMemberPath()}' is not readable, so cannot be used as a source member"; + reason = $"target member '{GetTargetDescription()}' is not readable, so cannot be used as a source member"; return true; } @@ -122,14 +122,31 @@ public MappingConfigInfo GetReverseConfigInfo() protected override bool MembersConflict(UserConfiguredItemBase otherConfiguredItem) => TargetMember.LeafMember.Equals(otherConfiguredItem.TargetMember.LeafMember); + protected override bool HasSameCriteriaAs(ConfiguredDataSourceFactoryBase otherDataSource) + => DataSourceLambda.IsSameAs(otherDataSource?.DataSourceLambda); + #endregion + protected override string GetToTargetDescription(ConfiguredDataSourceFactoryBase conflictingDataSource) + { + return TargetMember.IsRoot + ? conflictingDataSource.IsSequential ? "ToTarget() " : "ToTargetInstead() " + : null; + } + protected override string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource) { return conflictingDataSource is ConfiguredDataSourceFactory dsf && dsf._isReversal ? " from an automatically-configured reverse data source" : null; } + public override string GetDescription() + => GetDataSourceDescription() + " -> " + GetTargetDescription(); + + protected override string GetDataSourceDescription() => GetDataSourceValueDescription(); + + protected override string GetTargetDescription() => TargetMember.GetFriendlyTargetPath(ConfigInfo); + protected override bool TargetMembersAreCompatibleForToTarget(QualifiedMember otherTargetMember) => TargetMember.HasCompatibleType(otherTargetMember.Type); diff --git a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs index 97d1c43b2..4e4d518bb 100644 --- a/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs +++ b/AgileMapper/Configuration/DataSources/ConfiguredDataSourceFactoryBase.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; #endif using AgileMapper.DataSources; + using Api.Configuration; using Lambdas; using Members; @@ -41,7 +42,7 @@ protected ConfiguredDataSourceFactoryBase( public bool IsSequential => ConfigInfo.IsSequentialConfiguration; - protected ConfiguredLambdaInfo DataSourceLambda { get; } + internal ConfiguredLambdaInfo DataSourceLambda { get; } protected bool ValueCouldBeSourceMember { get; set; } @@ -54,13 +55,13 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) var otherDataSource = otherConfiguredItem as ConfiguredDataSourceFactoryBase; var isOtherDataSource = otherDataSource != null; - var dataSourceLambdasAreTheSame = HasSameDataSourceAs(otherDataSource); + var criteriaAreTheSame = HasSameCriteriaAs(otherDataSource); if (WasAutoCreated && (otherConfiguredItem is IPotentialAutoCreatedItem otherItem) && !otherItem.WasAutoCreated) { - return isOtherDataSource && dataSourceLambdasAreTheSame; + return isOtherDataSource && criteriaAreTheSame; } if (isOtherDataSource == false) @@ -68,14 +69,9 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) return true; } - if (!ConfigInfo.HasSameTypesAs(otherDataSource)) + if (otherDataSource.IsSequential || !ConfigInfo.HasSameTypesAs(otherDataSource)) { - return dataSourceLambdasAreTheSame; - } - - if (otherDataSource.IsSequential) - { - return dataSourceLambdasAreTheSame; + return criteriaAreTheSame; } return true; @@ -83,42 +79,35 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) #region ConflictsWith Helpers - private bool HasSameDataSourceAs(ConfiguredDataSourceFactoryBase otherDataSource) - => DataSourceLambda.IsSameAs(otherDataSource?.DataSourceLambda); + protected abstract bool HasSameCriteriaAs(ConfiguredDataSourceFactoryBase otherDataSource); #endregion public string GetConflictMessage(ConfiguredDataSourceFactoryBase conflictingDataSource) { - var toTarget = TargetMember.IsRoot - ? conflictingDataSource.IsSequential ? "ToTarget() " : "ToTargetInstead() " - : null; - + var toTarget = GetToTargetDescription(conflictingDataSource); var existingDataSource = conflictingDataSource.GetDataSourceDescription(); - var reason = GetConflictReasonOrNull(conflictingDataSource); - return $"{GetTargetMemberPath()} already has configured {toTarget}data source {existingDataSource}{reason}"; + return $"{GetTargetDescription()} already has configured {toTarget}data source {existingDataSource}{reason}"; } + protected abstract string GetToTargetDescription(ConfiguredDataSourceFactoryBase conflictingDataSource); + protected abstract string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource); - public string GetDescription() - { - var sourceMemberPath = GetDataSourceDescription(); - var targetMemberPath = GetTargetMemberPath(); + public abstract string GetDescription(); - return sourceMemberPath + " -> " + targetMemberPath; - } + protected abstract string GetDataSourceDescription(); - protected string GetDataSourceDescription() + protected string GetDataSourceValueDescription() { var description = DataSourceLambda.GetDescription(ConfigInfo); return DataSourceLambda.IsSourceMember ? description : "'" + description + "'"; } - protected string GetTargetMemberPath() => TargetMember.GetFriendlyTargetPath(ConfigInfo); + protected abstract string GetTargetDescription(); public override bool AppliesTo(IQualifiedMemberContext context) => base.AppliesTo(context) && DataSourceLambda.Supports(context.RuleSet); @@ -144,6 +133,7 @@ public IConfiguredDataSource Create(IMemberMapperData mapperData) configuredCondition, value, ConfigInfo.IsSequentialConfiguration, + ConfigInfo.HasTargetMemberMatcher(), mapperData); } diff --git a/AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs deleted file mode 100644 index 07cbdffa9..000000000 --- a/AgileMapper/Configuration/DataSources/ConfiguredFilterDataSourceFactory.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace AgileObjects.AgileMapper.Configuration.DataSources -{ - using System; -#if NET35 - using Microsoft.Scripting.Ast; - using Extensions.Internal; -#else - using System.Linq.Expressions; -#endif - using Lambdas; - using Members; -#if NET35 - using LinqExp = System.Linq.Expressions; -#endif - - internal class ConfiguredFilterDataSourceFactory : ConfiguredDataSourceFactoryBase - { - private readonly Expression _targetMemberFilterExpression; - private readonly Func _targetMemberFilter; -#if NET35 - public ConfiguredFilterDataSourceFactory( - MappingConfigInfo configInfo, - LinqExp.Expression> targetMemberFilter, - ConfiguredLambdaInfo dataSourceLambda, - QualifiedMember toTargetMember) - : this( - configInfo, - targetMemberFilter.ToDlrExpression(), - dataSourceLambda, - toTargetMember) - { - } -#endif - - public ConfiguredFilterDataSourceFactory( - MappingConfigInfo configInfo, - Expression> targetMemberFilter, - ConfiguredLambdaInfo dataSourceLambda, - QualifiedMember toTargetMember) - : this( - configInfo, - targetMemberFilter?.Body, - targetMemberFilter?.Compile() ?? (_ => true), - dataSourceLambda, - toTargetMember) - { - } - - public ConfiguredFilterDataSourceFactory( - MappingConfigInfo configInfo, - Expression targetMemberFilterExpression, - Func targetMemberFilter, - ConfiguredLambdaInfo dataSourceLambda, - QualifiedMember toTargetMember) - : base(configInfo, dataSourceLambda, toTargetMember) - { - _targetMemberFilterExpression = targetMemberFilterExpression; - _targetMemberFilter = targetMemberFilter; - } - - protected override string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource) - => null; - - protected override bool TargetMembersAreCompatibleForToTarget(QualifiedMember otherTargetMember) - => MatchesTargetMember(otherTargetMember); - - private bool MatchesTargetMember(QualifiedMember targetMember) - => _targetMemberFilter.Invoke(new TargetMemberSelector(targetMember)); - - #region IPotentialAutoCreatedItem Members - - public override IPotentialAutoCreatedItem Clone() - { - return new ConfiguredFilterDataSourceFactory( - ConfigInfo, - _targetMemberFilterExpression, - _targetMemberFilter, - DataSourceLambda, - TargetMember) - { - ValueCouldBeSourceMember = ValueCouldBeSourceMember, - WasAutoCreated = true - }; - } - - public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedDataSourceFactory) - => false; - - #endregion - } -} diff --git a/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs new file mode 100644 index 000000000..01bab7d0e --- /dev/null +++ b/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs @@ -0,0 +1,138 @@ +namespace AgileObjects.AgileMapper.Configuration.DataSources +{ + using System; +#if NET35 + using Microsoft.Scripting.Ast; + using Extensions.Internal; +#else + using System.Linq.Expressions; +#endif + using Lambdas; + using Members; + using ReadableExpressions; +#if NET35 + using LinqExp = System.Linq.Expressions; +#endif + + internal class ConfiguredMatcherDataSourceFactory : + ConfiguredDataSourceFactoryBase, + IHasMemberFilter + { + private readonly Expression _targetMemberMatcherExpression; + private readonly Func _targetMemberMatcher; +#if NET35 + public ConfiguredMatcherDataSourceFactory( + MappingConfigInfo configInfo, + LinqExp.Expression> targetMemberMatcher, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember toTargetMember) + : this( + configInfo, + targetMemberMatcher.ToDlrExpression(), + dataSourceLambda, + toTargetMember) + { + } +#endif + + public ConfiguredMatcherDataSourceFactory( + MappingConfigInfo configInfo, + Expression> targetMemberMatcher, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember toTargetMember) + : this( + configInfo, + targetMemberMatcher?.Body, + targetMemberMatcher?.Compile() ?? (_ => true), + dataSourceLambda, + toTargetMember) + { + } + + private ConfiguredMatcherDataSourceFactory( + MappingConfigInfo configInfo, + Expression targetMemberMatcherExpression, + Func targetMemberMatcher, + ConfiguredLambdaInfo dataSourceLambda, + QualifiedMember toTargetMember) + : base(configInfo, dataSourceLambda, toTargetMember) + { + _targetMemberMatcherExpression = targetMemberMatcherExpression; + _targetMemberMatcher = targetMemberMatcher; + } + + private string TargetMemberMatcher => _targetMemberMatcherExpression?.ToReadableString(); + + string IHasMemberFilter.MemberFilter => TargetMemberMatcher; + + #region ConflictsWith Helpers + + protected override bool MembersConflict(UserConfiguredItemBase otherItem) + { + if (otherItem is IHasMemberFilter memberFilterOwner) + { + return HasSameCriteriaAs(memberFilterOwner); + } + + return MatcherMatches(otherItem.TargetMember); + } + + protected override bool HasSameCriteriaAs(ConfiguredDataSourceFactoryBase otherDataSource) + { + return otherDataSource is ConfiguredMatcherDataSourceFactory matcherDataSource && + HasSameCriteriaAs(matcherDataSource); + } + + private bool HasSameCriteriaAs(IHasMemberFilter memberFilterOwner) + => memberFilterOwner.MemberFilter == TargetMemberMatcher; + + #endregion + + protected override string GetToTargetDescription(ConfiguredDataSourceFactoryBase conflictingDataSource) + => null; + + protected override string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBase conflictingDataSource) + => null; + + public override string GetDescription() => GetDataSourceDescription(); + + protected override string GetDataSourceDescription() + { + var source = SourceType != typeof(object) ? SourceTypeName + " " : null; + var members = TargetType != DataSourceLambda.ReturnType ? " members" : null; + + return + $"'If mapping {source}-> {GetTargetDescription()} and {TargetMemberMatcher}, " + + $"map {GetDataSourceValueDescription()} to target{members}'"; + } + + protected override string GetTargetDescription() => TargetTypeName; + + protected override bool TargetMembersAreCompatibleForToTarget(QualifiedMember otherTargetMember) + => MatcherMatches(otherTargetMember); + + private bool MatcherMatches(QualifiedMember targetMember) + => _targetMemberMatcher.Invoke(new TargetMemberSelector(targetMember)); + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new ConfiguredMatcherDataSourceFactory( + ConfigInfo, + _targetMemberMatcherExpression, + _targetMemberMatcher, + DataSourceLambda, + TargetMember) + { + ValueCouldBeSourceMember = ValueCouldBeSourceMember, + WasAutoCreated = true + }; + } + + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedDataSourceFactory) + => false; + + #endregion + } +} diff --git a/AgileMapper/Configuration/IHasMemberFilter.cs b/AgileMapper/Configuration/IHasMemberFilter.cs new file mode 100644 index 000000000..682d479f0 --- /dev/null +++ b/AgileMapper/Configuration/IHasMemberFilter.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.Configuration +{ + internal interface IHasMemberFilter + { + string MemberFilter { get; } + } +} diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs index 979114f2a..084b6e1bc 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs @@ -16,7 +16,10 @@ namespace AgileObjects.AgileMapper.Configuration.MemberIgnores using LinqExp = System.Linq.Expressions; #endif - internal class ConfiguredMemberFilter : ConfiguredMemberIgnoreBase, IMemberFilterIgnore + internal class ConfiguredMemberFilter : + ConfiguredMemberIgnoreBase, + IHasMemberFilter, + IMemberFilterIgnore { private readonly Expression _memberFilterExpression; private readonly Func _memberFilter; @@ -47,8 +50,20 @@ private ConfiguredMemberFilter( private string TargetMemberFilter => _memberFilterExpression?.ToReadableString(); + string IHasMemberFilter.MemberFilter => TargetMemberFilter; + string IMemberFilterIgnore.MemberFilter => TargetMemberFilter; + protected override bool MembersConflict(UserConfiguredItemBase otherItem) + { + if (otherItem is IHasMemberFilter memberFilterOwner) + { + return memberFilterOwner.MemberFilter == TargetMemberFilter; + } + + return IsFiltered(otherItem.TargetMember); + } + public override string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore) => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); @@ -67,16 +82,6 @@ public override string GetIgnoreMessage(IQualifiedMember targetMember) public override bool AppliesTo(IQualifiedMemberContext context) => base.AppliesTo(context) && IsFiltered(context.TargetMember); - protected override bool MembersConflict(UserConfiguredItemBase otherItem) - { - if (otherItem is ConfiguredMemberFilter otherIgnoredMemberFilter) - { - return otherIgnoredMemberFilter.TargetMemberFilter == TargetMemberFilter; - } - - return IsFiltered(otherItem.TargetMember); - } - private bool IsFiltered(QualifiedMember member) => _memberFilter.Invoke(new TargetMemberSelector(member)); diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs index 89775c2eb..448d118b5 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs @@ -26,7 +26,7 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) return true; } - if (otherConfiguredItem is ConfiguredDataSourceFactory configuredDataSource) + if (otherConfiguredItem is ConfiguredDataSourceFactoryBase configuredDataSource) { var configuredSourceMember = configuredDataSource.ToSourceMemberOrNull(); diff --git a/AgileMapper/Configuration/UserConfiguredItemBase.cs b/AgileMapper/Configuration/UserConfiguredItemBase.cs index 36954b14b..747eead86 100644 --- a/AgileMapper/Configuration/UserConfiguredItemBase.cs +++ b/AgileMapper/Configuration/UserConfiguredItemBase.cs @@ -70,9 +70,7 @@ public virtual bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) } protected virtual bool HasReverseConflict(UserConfiguredItemBase otherItem) - { - return otherItem is IReverseConflictable conflictable && conflictable.ConflictsWith(this); - } + => otherItem is IReverseConflictable conflictable && conflictable.ConflictsWith(this); private bool HasOverlappingRuleSets(UserConfiguredItemBase otherItem) { diff --git a/AgileMapper/DataSources/ConfiguredDataSource.cs b/AgileMapper/DataSources/ConfiguredDataSource.cs index da16e3d19..836b5cd66 100644 --- a/AgileMapper/DataSources/ConfiguredDataSource.cs +++ b/AgileMapper/DataSources/ConfiguredDataSource.cs @@ -17,12 +17,14 @@ public ConfiguredDataSource( Expression configuredCondition, Expression value, bool isSequential, + bool hasMatcher, IMemberMapperData mapperData) : this( CreateSourceMember(value, mapperData), configuredCondition, GetConvertedValue(value, mapperData), isSequential, + hasMatcher, mapperData) { } @@ -57,11 +59,13 @@ public ConfiguredDataSource( Expression configuredCondition, Expression convertedValue, bool isSequential, + bool hasMatcher, IMemberMapperData mapperData) : base(sourceMember, convertedValue, mapperData) { _originalValue = convertedValue; IsSequential = isSequential; + HasConfiguredMatcher = hasMatcher; if (configuredCondition == null) { @@ -76,6 +80,8 @@ public ConfiguredDataSource( : configuredCondition; } + public bool HasConfiguredMatcher { get; } + public bool HasConfiguredCondition { get; } public override Expression Condition { get; } diff --git a/AgileMapper/DataSources/IConfiguredDataSource.cs b/AgileMapper/DataSources/IConfiguredDataSource.cs index 6b3b5cfb9..21f360460 100644 --- a/AgileMapper/DataSources/IConfiguredDataSource.cs +++ b/AgileMapper/DataSources/IConfiguredDataSource.cs @@ -2,6 +2,8 @@ { internal interface IConfiguredDataSource : IDataSource { + bool HasConfiguredMatcher { get; } + bool HasConfiguredCondition { get; } bool IsSameAs(IDataSource otherDataSource); diff --git a/AgileMapper/DataSources/IDataSource.cs b/AgileMapper/DataSources/IDataSource.cs index e4470229f..e77f97093 100644 --- a/AgileMapper/DataSources/IDataSource.cs +++ b/AgileMapper/DataSources/IDataSource.cs @@ -18,8 +18,17 @@ internal interface IDataSource bool IsConditional { get; } + /// + /// Gets a value indicating whether this is to be applied to the + /// mapping target object sequentially, i.e before or after other data sources are + /// applied. + /// bool IsSequential { get; } + /// + /// Gets a value indicating whether this provides a fallback value + /// to be applied to a mapping target when other data sources cannot be found or do not apply. + /// bool IsFallback { get; } IList Variables { get; } diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 61d0a4fcf..0a1984038 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -12,6 +12,7 @@ #endif using System.Reflection; using NetStandardPolyfills; + using ObjectPopulation; using ReadableExpressions.Extensions; #if NET35 using LinqExp = System.Linq.Expressions; @@ -321,6 +322,38 @@ public static Expression GetRootExpression(this Expression expression) public static Expression ToExpression(this IList expressions) => expressions.HasOne() ? expressions.First() : Expression.Block(expressions); + public static IList GetMemberMappingExpressions(this IList mappingExpressions) + => mappingExpressions.Filter(IsMemberMapping).ToList(); + + private static bool IsMemberMapping(Expression expression) + { + switch (expression.NodeType) + { + case Constant: + return false; + + case Call when ( + IsCallTo(nameof(IObjectMappingDataUntyped.Register), expression) || + IsCallTo(nameof(IObjectMappingDataUntyped.TryGet), expression)): + + return false; + + case Assign when IsMapRepeatedCall(((BinaryExpression)expression).Right): + return false; + } + + return true; + } + + private static bool IsMapRepeatedCall(Expression expression) + { + return (expression.NodeType == Call) && + IsCallTo(nameof(IObjectMappingDataUntyped.MapRepeated), expression); + } + + private static bool IsCallTo(string methodName, Expression call) + => ((MethodCallExpression)call).Method.Name == methodName; + public static bool TryGetVariableAssignment(this IList mappingExpressions, out BinaryExpression assignment) { if (mappingExpressions.TryFindMatch(exp => exp.NodeType == Assign, out var assignmentExpression)) diff --git a/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs b/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs index 595e67acd..82d24a9e4 100644 --- a/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs +++ b/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs @@ -2,6 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Collections.Generic; using System.Linq; + using Api.Configuration; using ComplexTypes; #if NET35 using Microsoft.Scripting.Ast; @@ -91,6 +92,7 @@ private static IDataSource GetMappingFactoryDataSource( condition, returnValue, factory.ConfigInfo.IsSequentialConfiguration, + factory.ConfigInfo.HasTargetMemberMatcher(), mapperData); } } diff --git a/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs b/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs index fc16f500e..37fbbb405 100644 --- a/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs +++ b/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs @@ -11,18 +11,6 @@ internal static class DerivedMappingFactory { - public static Expression GetDerivedTypeMapping( - IObjectMappingData declaredTypeMappingData, - Expression sourceValue, - Type targetType) - { - return GetDerivedTypeMapping( - declaredTypeMappingData, - sourceValue, - targetType, - out _); - } - public static Expression GetDerivedTypeMapping( IObjectMappingData declaredTypeMappingData, Expression sourceValue, diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index f3a848076..10ec81a27 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -290,7 +290,8 @@ private static Expression GetDerivedTypeMapping( var mappingTryCatch = DerivedMappingFactory.GetDerivedTypeMapping( mappingData, derivedSourceCheck.TypedVariable, - mappingData.MapperData.TargetType); + mappingData.MapperData.TargetType, + out _); return mappingTryCatch; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableExtensions/EnumerableExpressionExtensions.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableExtensions/EnumerableExpressionExtensions.cs index 6770453e7..e101baaa7 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableExtensions/EnumerableExpressionExtensions.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableExtensions/EnumerableExpressionExtensions.cs @@ -11,7 +11,7 @@ using Extensions; using Extensions.Internal; using NetStandardPolyfills; - using static AgileObjects.AgileMapper.Extensions.Internal.LinqExtensions; + using static Extensions.Internal.LinqExtensions; internal static class EnumerableExpressionExtensions { diff --git a/AgileMapper/ObjectPopulation/MappingCreationContext.cs b/AgileMapper/ObjectPopulation/MappingCreationContext.cs index 8a668c4fd..b0ddc14f5 100644 --- a/AgileMapper/ObjectPopulation/MappingCreationContext.cs +++ b/AgileMapper/ObjectPopulation/MappingCreationContext.cs @@ -1,7 +1,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Collections.Generic; - using System.Linq; #if NET35 using Microsoft.Scripting.Ast; #else @@ -11,11 +10,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Extensions; using Extensions.Internal; using Members; -#if NET35 - using static Microsoft.Scripting.Ast.ExpressionType; -#else - using static System.Linq.Expressions.ExpressionType; -#endif using static InvocationPosition; internal class MappingCreationContext @@ -58,12 +52,22 @@ private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperDa public Expression PostMappingCallback { get; } + public bool HasMappingCallbacks + => PreMappingCallback != null || PostMappingCallback != null; + public Expression MapToNullCondition { get; } public List MappingExpressions { get; } public bool InstantiateLocalVariable { get; set; } + public bool RemoveEmptyMappings + => !MapperData.TargetMemberIsEnumerableElement() || RuleSet.Settings.RemoveEmptyElementMappings; + + /// + /// Gets or sets a value indicating whether the MappingExpressions collection contains a + /// complete mapping, and no further Expressions are required. + /// public bool MappingComplete { get; set; } public IList ToTargetDataSources @@ -78,36 +82,7 @@ public IList GetMemberMappingExpressions() return _memberMappingExpressions ?? Enumerable.EmptyArray; } - return _memberMappingExpressions = MappingExpressions.Filter(IsMemberMapping).ToList(); - } - - private static bool IsMemberMapping(Expression expression) - { - switch (expression.NodeType) - { - case Constant: - return false; - - case Call when ( - IsCallTo(nameof(IObjectMappingDataUntyped.Register), expression) || - IsCallTo(nameof(IObjectMappingDataUntyped.TryGet), expression)): - - return false; - - case Assign when IsMapRepeatedCall(((BinaryExpression)expression).Right): - return false; - } - - return true; - } - - private static bool IsCallTo(string methodName, Expression call) - => ((MethodCallExpression)call).Method.Name == methodName; - - private static bool IsMapRepeatedCall(Expression expression) - { - return (expression.NodeType == Call) && - IsCallTo(nameof(IObjectMappingDataUntyped.MapRepeated), expression); + return _memberMappingExpressions = MappingExpressions.GetMemberMappingExpressions(); } public MappingCreationContext WithToTargetDataSource(IDataSource dataSource) diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index b6a17ef3d..65248f4e0 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -50,7 +50,7 @@ public Expression Create(IObjectMappingData mappingData) AddPopulationsAndCallbacks(context); - if (RemoveEmptyMappings(context) && NothingIsBeingMapped(context)) + if (context.RemoveEmptyMappings && NothingIsBeingMapped(context)) { return mapperData.IsEntryPoint ? mapperData.TargetObject : Constants.EmptyExpression; } @@ -136,7 +136,7 @@ private Expression GetConfiguredAlternateDataSourceMappingOrNull( { var toTargetDataSource = context .ToTargetDataSources - .FirstOrDefault(ds => ds.IsSequential == false); + .FirstOrDefault(ds => !ds.IsSequential && !ds.HasConfiguredMatcher); if (toTargetDataSource == null) { @@ -144,7 +144,6 @@ private Expression GetConfiguredAlternateDataSourceMappingOrNull( return null; } - //isConditional = false; isConditional = toTargetDataSource.HasConfiguredCondition; return GetConfiguredToTargetDataSourceMappingOrNull( @@ -183,8 +182,31 @@ private void AddPopulationsAndCallbacks(MappingCreationContext context) { AddPopulationsAndCallbacks(this, context, (factory, ctx) => { + var mappingExpressions = ctx.MappingExpressions; + var mappingExpressionCount = mappingExpressions.Count; + factory.AddObjectPopulation(ctx); - ctx.MappingExpressions.AddRange(GetConfiguredToTargetDataSourceMappings(ctx)); + mappingExpressions.AddRange(GetConfiguredToTargetDataSourceMappings(ctx)); + + if (!context.RemoveEmptyMappings) + { + return; + } + + var addedExpressionCount = mappingExpressions.Count - mappingExpressionCount; + + if (addedExpressionCount == 0) + { + return; + } + + var addedMappingExpressions = mappingExpressions + .GetRange(mappingExpressionCount, addedExpressionCount); + + if (NothingIsBeingMapped(addedMappingExpressions, ctx)) + { + mappingExpressions.RemoveRange(mappingExpressionCount, addedExpressionCount); + } }); } @@ -252,6 +274,13 @@ private Expression GetConfiguredToTargetDataSourceMappingOrNull( context.UpdateFrom(toTargetContext, toTargetDataSource); var originalMapperData = context.MapperData; + var isSequential = toTargetDataSource.IsSequential; + + if (!isSequential) + { + toTargetContext.MappingExpressions.Add( + context.MapperData.GetReturnExpression(GetExpressionToReturn(toTargetContext))); + } var toTargetMapping = MappingFactory.UseLocalToTargetDataSourceVariableIfAppropriate( originalMapperData, @@ -259,13 +288,11 @@ private Expression GetConfiguredToTargetDataSourceMappingOrNull( toTargetDataSource.Value, toTargetContext.GetMappingExpression()); - var isSequential = toTargetDataSource.IsSequential; - - var isConditional = isSequential + var hasCondition = isSequential ? toTargetDataSource.IsConditional : toTargetDataSource.HasConfiguredCondition; - if (!isConditional) + if (!hasCondition) { return toTargetMapping; } @@ -274,55 +301,36 @@ private Expression GetConfiguredToTargetDataSourceMappingOrNull( if (!isFirstDataSource || originalMapperData.TargetMember.IsComplex) { - if (isSequential) + if (isSequential || !originalMapperData.TargetMemberIsEnumerableElement()) { return Expression.IfThen(toTargetDataSource.Condition, toTargetMapping); } - if (context.MapperData.TargetMemberIsEnumerableElement()) - { - fallback = originalMapperData.GetTargetMemberDefault(); - goto AssignFallback; - } - - return Expression.IfThenElse( - toTargetDataSource.Condition, - toTargetMapping, - GetDefaultMappingFallback(context)); + // Mapping a configured ToTargetInstead() data source to + // a complex type enumerable element member; reset the + // local instance variable to null to prevent reuse of a + // previous element's mapping result: + fallback = originalMapperData.GetTargetMemberDefault(); + } + else + { + fallback = originalMapperData.LocalVariable.Type.GetEmptyInstanceCreation( + context.TargetMember.ElementType, + originalMapperData.EnumerablePopulationBuilder.TargetTypeHelper); } - fallback = originalMapperData.LocalVariable.Type.GetEmptyInstanceCreation( - context.TargetMember.ElementType, - originalMapperData.EnumerablePopulationBuilder.TargetTypeHelper); - - AssignFallback: var assignFallback = originalMapperData.LocalVariable.AssignTo(fallback); return Expression.IfThenElse(toTargetDataSource.Condition, toTargetMapping, assignFallback); } - private Expression GetDefaultMappingFallback(MappingCreationContext context) - { - //var mappingExpressionsCount = context.MappingExpressions.Count; - - AddPopulationsAndCallbacks(context); - - //var fallbackMappingStartIndex = mappingExpressionsCount - 1; - //var fallbackMappingEndIndex = context.MappingExpressions.Count - mappingExpressionsCount; - - //var fallbackMappingExpressions = context.MappingExpressions - // .GetRange(fallbackMappingStartIndex, fallbackMappingEndIndex); - - //context.MappingExpressions - // .RemoveRange(fallbackMappingStartIndex, fallbackMappingEndIndex); - - return context.GetMappingExpression(); - } - private static bool NothingIsBeingMapped(MappingCreationContext context) - { - var mappingExpressions = context.GetMemberMappingExpressions(); + => NothingIsBeingMapped(context.GetMemberMappingExpressions(), context); + private static bool NothingIsBeingMapped( + IList mappingExpressions, + MappingCreationContext context) + { if (mappingExpressions.None()) { return true; @@ -375,16 +383,6 @@ private static bool NothingIsBeingMapped(MappingCreationContext context) return objectNewing.Arguments.None() && (objectNewing.Type != typeof(object)); } - private static bool RemoveEmptyMappings(MappingCreationContext context) - { - if (context.MapperData.TargetMemberIsEnumerableElement()) - { - return context.RuleSet.Settings.RemoveEmptyElementMappings; - } - - return true; - } - private Expression GetMappingBlock(MappingCreationContext context) { var mappingExpressions = context.MappingExpressions; @@ -417,13 +415,11 @@ private Expression GetMappingBlock(MappingCreationContext context) return returnExpression; } - returnExpression = GetReturnExpression(GetReturnValue(context.MapperData), context); - - mappingExpressions.Add(context.MapperData.GetReturnLabel(returnExpression)); + mappingExpressions.Add(context.MapperData.GetReturnLabel(GetExpressionToReturn(context))); var mappingBlock = context.MapperData.Context.UseLocalVariable ? Expression.Block(new[] { context.MapperData.LocalVariable }, mappingExpressions) - : Expression.Block(mappingExpressions); + : mappingExpressions.ToExpression(); return mappingBlock; } @@ -471,8 +467,8 @@ private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationC returnExpression = (assignedValue.NodeType == Invoke) ? Expression.Block( new[] { (ParameterExpression)localVariableAssignment.Left }, - GetReturnExpression(localVariableAssignment, context)) - : GetReturnExpression(assignedValue, context); + GetExpressionToReturn(localVariableAssignment, context)) + : GetExpressionToReturn(assignedValue, context); if (context.MappingExpressions.HasOne()) { @@ -484,7 +480,10 @@ private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationC return true; } - private static Expression GetReturnExpression(Expression returnValue, MappingCreationContext context) + private Expression GetExpressionToReturn(MappingCreationContext context) + => GetExpressionToReturn(GetReturnValue(context.MapperData), context); + + private static Expression GetExpressionToReturn(Expression returnValue, MappingCreationContext context) { var mapToNullCondition = GetMapToNullConditionOrNull(context); diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index 2c1adb005..094d7464d 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -657,9 +657,29 @@ public MethodCallExpression GetMapRepeatedCall( return mapRepeatedCall; } + /// + /// Creates a GotoExpression passing the given to this + /// 's LabelTarget. + /// + /// The vlaue to pass to this 's LabelTarget. + /// + /// Aa GotoExpression passing the given to this + /// 's LabelTarget. + /// public Expression GetReturnExpression(Expression value) => Expression.Return(_returnLabelTarget, value, TargetType); + /// + /// Creates a LabelExpression for this 's LabelTarget, with + /// the given . The created LabelExpression marks the point + /// in the compiled mapping Func to which execution will jump from GotoExpressions created + /// by calls to this 's GetReturnExpression() method. + /// + /// The default value of the LabelExpression to create. + /// + /// A LabelExpression for this 's LabelTarget, with the given + /// . + /// public Expression GetReturnLabel(Expression defaultValue) => Expression.Label(_returnLabelTarget, defaultValue); From 5a4b2cc5aa0ab95a3c7e9c703742b49d01efa0e7 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 28 Apr 2021 11:15:19 +0100 Subject: [PATCH 3/5] Fixing query projection --- .../WhenConfiguringMatcherDataSources.cs | 35 +++++++++++++++++++ ...onfiguringMatcherDataSourcesIncorrectly.cs | 29 +++++++++++++-- .../ConfiguredMatcherDataSourceFactory.cs | 2 +- AgileMapper/DataSources/DataSourceBase.cs | 8 +++-- .../DataSourceFilteringExtensions.cs | 6 ++-- AgileMapper/DataSources/DataSourceSet.cs | 10 ++---- .../MappingExpressionFactoryBase.cs | 14 ++++---- 7 files changed, 81 insertions(+), 23 deletions(-) diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs index 66c53c4fb..1b7c2b162 100644 --- a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs @@ -128,6 +128,41 @@ public void ShouldApplyASourceMemberByTargetTypeAndTargetMemberNameConditionally } } + [Fact] + public void ShouldAllowOverlappingConditionalMemberSpecificDataSource() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>().To>() + .IfTargetMembersMatch(member => member.HasType()) + .Map((pf, pp) => pf.Value == 1 ? "Yes" : "No") + .ToTarget(); + + mapper.WhenMapping + .From>().To>() + .If(ctx => ctx.Source.Value >= 100) + .Map((pf, pp) => pf.Value >= 200 ? "JAH" : "NOPE") + .To(pp => pp.Value); + + var matcherYesSource = new PublicField { Value = 1 }; + var matcherYesResult = mapper.Map(matcherYesSource).ToANew>(); + matcherYesResult.Value.ShouldBe("Yes"); + + var matcherNoSource = new PublicField { Value = 0 }; + var matcherNoResult = mapper.Map(matcherNoSource).ToANew>(); + matcherNoResult.Value.ShouldBe("No"); + + var dataSourceYesSource = new PublicField { Value = 1000 }; + var dataSourceYesResult = mapper.Map(dataSourceYesSource).ToANew>(); + dataSourceYesResult.Value.ShouldBe("JAH"); + + var dataSourceNoSource = new PublicField { Value = 150 }; + var dataSourceNoResult = mapper.Map(dataSourceNoSource).ToANew>(); + dataSourceNoResult.Value.ShouldBe("NOPE"); + } + } + #region Helper Classes private static class Issue208 diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs index 7070c541e..7b95c6f56 100644 --- a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs @@ -72,7 +72,7 @@ public void ShouldErrorIfConflictingMatcherDataSourceConfigured() .Map(false).ToTarget(); } }); - + configEx.Message.ShouldContain("already has"); configEx.Message.ShouldContain("'If mapping string -> bool and member.Name == \"AlwaysTrue\","); configEx.Message.ShouldContain("map 'true' to target'"); @@ -98,8 +98,33 @@ public void ShouldErrorIfConflictingMemberFilterConfigured() }); configEx.Message.ShouldContain("'If mapping -> PublicTwoFields"); - configEx.Message.ShouldContain("and member.HasType(), map '\"Yippee!\"' to target members'"); + configEx.Message.ShouldContain("and member.HasType(), map '\"Yippee!\"' to target member'"); configEx.Message.ShouldContain("member ignore pattern 'member.HasType()'"); } + + [Fact] + public void ShouldErrorIfRedundantDataSourceConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>().To>() + .IfTargetMembersMatch(member => member.HasType()) + .Map((s, t) => s.Value * 3).ToTarget(); + + mapper.WhenMapping + .From>().To>() + .Map((s, t) => s.Value * 3) + .To(t => t.Value); + } + }); + + configEx.Message.ShouldContain("PublicField.Value already has"); + configEx.Message.ShouldContain("'If mapping PublicField -> PublicField "); + configEx.Message.ShouldContain("and member.HasType(),"); + configEx.Message.ShouldContain("map 's.Value * 3' to target member'"); + } } } diff --git a/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs index 01bab7d0e..df313f245 100644 --- a/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs +++ b/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs @@ -99,7 +99,7 @@ protected override string GetConflictReasonOrNull(ConfiguredDataSourceFactoryBas protected override string GetDataSourceDescription() { var source = SourceType != typeof(object) ? SourceTypeName + " " : null; - var members = TargetType != DataSourceLambda.ReturnType ? " members" : null; + var members = TargetType != DataSourceLambda.ReturnType ? " member" : null; return $"'If mapping {source}-> {GetTargetDescription()} and {TargetMemberMatcher}, " + diff --git a/AgileMapper/DataSources/DataSourceBase.cs b/AgileMapper/DataSources/DataSourceBase.cs index 4aa224ba8..81ddf523f 100644 --- a/AgileMapper/DataSources/DataSourceBase.cs +++ b/AgileMapper/DataSources/DataSourceBase.cs @@ -178,7 +178,8 @@ public virtual Expression FinalisePopulationBranch( condition, population, alternatePopulation, - nextDataSource); + nextDataSource, + mapperData); } if (variables.Any()) @@ -193,9 +194,10 @@ private static Expression GetBranchedPopulation( Expression condition, Expression population, Expression alternatePopulation, - IDataSource previousDataSource) + IDataSource alternateDataSource, + IQualifiedMemberContext memberContext) { - if (previousDataSource.IsSequential) + if (alternateDataSource.IsSequential && !memberContext.TargetMember.IsSimple) { return Expression.Block( Expression.IfThen(condition, population), diff --git a/AgileMapper/DataSources/DataSourceFilteringExtensions.cs b/AgileMapper/DataSources/DataSourceFilteringExtensions.cs index f2f86c02b..e2c379c6c 100644 --- a/AgileMapper/DataSources/DataSourceFilteringExtensions.cs +++ b/AgileMapper/DataSources/DataSourceFilteringExtensions.cs @@ -23,7 +23,7 @@ public static IList WithFilters( { var dataSource = dataSources[i]; - var filteredDataSource = filteredDataSources[i] = ApplyFilter( + var filteredDataSource = filteredDataSources[i] = ApplyFilterIfAppropriate( dataSource.IsFallback ? dataSources[i - 1].SourceMember : dataSource.SourceMember, dataSource, mapperData); @@ -49,9 +49,9 @@ public static IList WithFilters( } public static IDataSource WithFilter(this IDataSource dataSource, IMemberMapperData mapperData) - => ApplyFilter(dataSource.SourceMember, dataSource, mapperData); + => ApplyFilterIfAppropriate(dataSource.SourceMember, dataSource, mapperData); - private static IDataSource ApplyFilter( + private static IDataSource ApplyFilterIfAppropriate( IQualifiedMember sourceMember, IDataSource dataSource, IMemberMapperData mapperData) diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index f6362a442..b87727918 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -210,11 +210,7 @@ public MultipleValueDataSourceSet( if (dataSource.Variables.Any()) { - if (variables == null) - { - variables = new List(); - } - + variables ??= new List(); variables.AddRange(dataSource.Variables); } @@ -224,9 +220,7 @@ public MultipleValueDataSourceSet( } } - Variables = (variables != null) - ? (IList)variables - : Constants.EmptyParameters; + Variables = variables ?? (IList)Constants.EmptyParameters; } public bool None => false; diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 65248f4e0..872e73683 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -448,15 +448,17 @@ private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationC return false; } - if (!context.MappingExpressions.TryGetVariableAssignment(out var localVariableAssignment)) + var mappingExpressions = context.MappingExpressions; + + if (!mappingExpressions.TryGetVariableAssignment(out var localVariableAssignment)) { returnExpression = null; return false; } if ((localVariableAssignment.Left.NodeType != Parameter) || - (localVariableAssignment != context.MappingExpressions.Last()) || - (context.PreMappingCallback == null)) + (localVariableAssignment != mappingExpressions.Last()) || + (!mappingExpressions.HasOne() && context.PreMappingCallback == null)) { returnExpression = null; return false; @@ -470,13 +472,13 @@ private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationC GetExpressionToReturn(localVariableAssignment, context)) : GetExpressionToReturn(assignedValue, context); - if (context.MappingExpressions.HasOne()) + if (mappingExpressions.HasOne()) { return true; } - context.MappingExpressions[context.MappingExpressions.Count - 1] = context.MapperData.GetReturnLabel(returnExpression); - returnExpression = Expression.Block(context.MappingExpressions); + mappingExpressions[mappingExpressions.Count - 1] = context.MapperData.GetReturnLabel(returnExpression); + returnExpression = Expression.Block(mappingExpressions); return true; } From 9c6bbb0f2f5f753b618a961851a943982b61d90b Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 28 Apr 2021 12:20:05 +0100 Subject: [PATCH 4/5] All fixed --- .../MappingExpressionFactoryBase.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 872e73683..adf18a1b8 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -200,12 +200,15 @@ private void AddPopulationsAndCallbacks(MappingCreationContext context) return; } - var addedMappingExpressions = mappingExpressions - .GetRange(mappingExpressionCount, addedExpressionCount); + if (mappingExpressionCount > 0) + { + mappingExpressions = mappingExpressions + .GetRange(mappingExpressionCount, addedExpressionCount); + } - if (NothingIsBeingMapped(addedMappingExpressions, ctx)) + if (NothingIsBeingMapped(mappingExpressions, ctx)) { - mappingExpressions.RemoveRange(mappingExpressionCount, addedExpressionCount); + ctx.MappingExpressions.RemoveRange(mappingExpressionCount, addedExpressionCount); } }); } @@ -457,8 +460,7 @@ private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationC } if ((localVariableAssignment.Left.NodeType != Parameter) || - (localVariableAssignment != mappingExpressions.Last()) || - (!mappingExpressions.HasOne() && context.PreMappingCallback == null)) + (localVariableAssignment != mappingExpressions.Last())) { returnExpression = null; return false; From fba3f3223472729d2966e86f30907e8c4a992b44 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 28 Apr 2021 14:24:55 +0100 Subject: [PATCH 5/5] Adding documentation --- .../WhenConfiguringMatcherDataSources.cs | 45 ++++++++- ...onfiguringMatcherDataSourcesIncorrectly.cs | 12 +-- .../IConditionalMappingConfigurator.cs | 2 +- .../Api/Configuration/MappingConfigurator.cs | 2 +- .../ConfiguredMatcherDataSourceFactory.cs | 13 ++- .../configuration/Constructor-Arguments.md | 8 +- docs/src/configuration/Member-Values.md | 94 ++++++++++++++++++- docs/src/index.md | 2 +- 8 files changed, 156 insertions(+), 22 deletions(-) diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs index 1b7c2b162..cc7f67347 100644 --- a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSources.cs @@ -20,7 +20,7 @@ public void ShouldApplyAConstantByTargetMemberTypeAndMemberType() { mapper.WhenMapping .To() - .IfTargetMembersMatch(member => member.IsField) + .IfTargetMemberMatches(member => member.IsField) .Map(123) .ToTarget(); @@ -41,7 +41,7 @@ public void ShouldApplyAnAlternateConstantByTargetTypeAndTargetMemberType() { mapper.WhenMapping .To>() - .IfTargetMembersMatch(member => member.HasType()) + .IfTargetMemberMatches(member => member.HasType()) .Map("Hurrah!") .ToTargetInstead(); @@ -78,7 +78,7 @@ public void ShouldApplyAnExpressionBySourceTypeTargetTypeAndTargetMemberAttribut mapper.WhenMapping .From() .To() - .IfTargetMembersMatch(member => member.HasAttribute()) + .IfTargetMemberMatches(member => member.HasAttribute()) .Map((b, s) => b ? "Y" : "N") .ToTarget(); @@ -101,7 +101,7 @@ public void ShouldApplyASourceMemberByTargetTypeAndTargetMemberNameConditionally .Map(ctx => ctx.Source.Value1.Line1) .To(addr => addr.Line1) .And - .IfTargetMembersMatch(member => member.Name.StartsWith(nameof(Address.Line2))) + .IfTargetMemberMatches(member => member.Name.StartsWith(nameof(Address.Line2))) .If(ctx => !string.IsNullOrEmpty(ctx.Source.Value2)) .Map(ctx => ctx.Source.Value2) .ToTarget(); @@ -135,7 +135,7 @@ public void ShouldAllowOverlappingConditionalMemberSpecificDataSource() { mapper.WhenMapping .From>().To>() - .IfTargetMembersMatch(member => member.HasType()) + .IfTargetMemberMatches(member => member.HasType()) .Map((pf, pp) => pf.Value == 1 ? "Yes" : "No") .ToTarget(); @@ -163,6 +163,41 @@ public void ShouldAllowOverlappingConditionalMemberSpecificDataSource() } } + [Fact] + public void ShouldAllowOverlappingMemberSpecificIgnore() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .Over>() + .IfTargetMemberMatches(member => member + .IsFieldMatching(f => f.FieldType == typeof(string))) + .Map((s, _) => s.ToString()) + .ToTarget(); + + mapper.WhenMapping + .To>() + .Ignore(ptf => ptf.Value1); + + var source = new PublicTwoFields + { + Value1 = "Tata", + Value2 = "Goodbye" + }; + + var target = new PublicTwoFields + { + Value1 = "Cya", + Value2 = "Laterz" + }; + + mapper.Map(source).Over(target); + + target.Value1.ShouldBe("Cya"); + target.Value2.ShouldBe(source.ToString()); + } + } + #region Helper Classes private static class Issue208 diff --git a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs index 7b95c6f56..9903ff4bf 100644 --- a/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/DataSources/WhenConfiguringMatcherDataSourcesIncorrectly.cs @@ -22,7 +22,7 @@ public void ShouldErrorIfMemberIgnoreSpecified() { mapper.WhenMapping .ToANew>() - .IfTargetMembersMatch(member => member.IsField) + .IfTargetMemberMatches(member => member.IsField) .Ignore(ptf => ptf.Value1, ptf => ptf.Value2); } }); @@ -42,7 +42,7 @@ public void ShouldErrorIfDataSourceTargetMemberSpecified() { mapper.WhenMapping .ToANew>() - .IfTargetMembersMatch(member => member.IsProperty) + .IfTargetMemberMatches(member => member.IsProperty) .Map("Yippee!") .To(ptf => ptf.Value1); } @@ -63,12 +63,12 @@ public void ShouldErrorIfConflictingMatcherDataSourceConfigured() { mapper.WhenMapping .From().To() - .IfTargetMembersMatch(member => member.Name == "AlwaysTrue") + .IfTargetMemberMatches(member => member.Name == "AlwaysTrue") .Map(true).ToTarget(); mapper.WhenMapping .From().To() - .IfTargetMembersMatch(member => member.Name == "AlwaysTrue") + .IfTargetMemberMatches(member => member.Name == "AlwaysTrue") .Map(false).ToTarget(); } }); @@ -87,7 +87,7 @@ public void ShouldErrorIfConflictingMemberFilterConfigured() { mapper.WhenMapping .ToANew>() - .IfTargetMembersMatch(member => member.HasType()) + .IfTargetMemberMatches(member => member.HasType()) .Map("Yippee!") .ToTarget(); @@ -111,7 +111,7 @@ public void ShouldErrorIfRedundantDataSourceConfigured() { mapper.WhenMapping .From>().To>() - .IfTargetMembersMatch(member => member.HasType()) + .IfTargetMemberMatches(member => member.HasType()) .Map((s, t) => s.Value * 3).ToTarget(); mapper.WhenMapping diff --git a/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs b/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs index 874f1979c..8f455f85a 100644 --- a/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs @@ -60,7 +60,7 @@ public interface IFilteredMappingConfigurator /// The matching function with which to select target members to which to apply the configuration. /// /// An IConditionalMappingConfigurator with which to complete the configuration. - IConditionalMappingConfigurator IfTargetMembersMatch( + IConditionalMappingConfigurator IfTargetMemberMatches( Expression> memberFilter); /// diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index ace3b59c0..0af413145 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -171,7 +171,7 @@ public IConditionalRootMappingConfigurator If(Expression If(Expression> condition) => SetCondition(condition); - public IConditionalMappingConfigurator IfTargetMembersMatch( + public IConditionalMappingConfigurator IfTargetMemberMatches( Expression> memberFilter) { ConfigInfo.SetTargetMemberMatcher(memberFilter); diff --git a/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs b/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs index df313f245..9ccf27d79 100644 --- a/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs +++ b/AgileMapper/Configuration/DataSources/ConfiguredMatcherDataSourceFactory.cs @@ -69,12 +69,17 @@ private ConfiguredMatcherDataSourceFactory( protected override bool MembersConflict(UserConfiguredItemBase otherItem) { - if (otherItem is IHasMemberFilter memberFilterOwner) + switch (otherItem) { - return HasSameCriteriaAs(memberFilterOwner); - } + case IHasMemberFilter memberFilterOwner: + return HasSameCriteriaAs(memberFilterOwner); + + case ConfiguredDataSourceFactoryBase: + return MatcherMatches(otherItem.TargetMember); - return MatcherMatches(otherItem.TargetMember); + default: + return false; + } } protected override bool HasSameCriteriaAs(ConfiguredDataSourceFactoryBase otherDataSource) diff --git a/docs/src/configuration/Constructor-Arguments.md b/docs/src/configuration/Constructor-Arguments.md index d6d6ed094..a8cb236e7 100644 --- a/docs/src/configuration/Constructor-Arguments.md +++ b/docs/src/configuration/Constructor-Arguments.md @@ -1,6 +1,10 @@ -By default, types are created using the 'greediest' public constructor - the one with the most parameters that have [matching](/Member-Matching) source members. If there are no available constructors whose parameters can all be matched - and no parameterless constructor - the member for which the type would be created is ignored. +By default, types are created using the 'greediest' public constructor - the one with the most parameters +that have [matching](/Member-Matching) source members. If there are no available constructors whose +parameters can all be matched - and no parameterless constructor - the member for which the type would +be created is ignored. -Constructor arguments can be configured by type or name, and constant values or expressions can be specified. +Constructor arguments can be configured by type or name, and constant values or expressions can be +specified. For example, to configure mapping these types: diff --git a/docs/src/configuration/Member-Values.md b/docs/src/configuration/Member-Values.md index fe63cd0e1..0d2362cdc 100644 --- a/docs/src/configuration/Member-Values.md +++ b/docs/src/configuration/Member-Values.md @@ -79,7 +79,7 @@ Mapper.WhenMapping .To(vm => vm.AllAddresses); // vm is the CustomerViewModel ``` -### Conditional Data Sources: +### Conditional Data Sources Any of these methods can be made conditional: @@ -220,4 +220,94 @@ Mapper.WhenMapping .ToTargetInstead(); ``` In this example, in any mapping where `Dictionary` is matched to an `IList`, -the Dictionary's `Values` collection is used as the source _instead_ of the Dictionary. \ No newline at end of file +the Dictionary's `Values` collection is used as the source _instead_ of the Dictionary. + +If a [conditional](#conditional-data-sources) `ToTargetInstead()`'s `If()` clause evaluates to true, +no further mapping is carried out for the member being mapped. If it evaluates to false, the default +mapping is performed, if any. + +### Applying Data Sources with a Matcher + +To apply a mapping data source to all target members matching particular criteria, use: + +```csharp +// When mapping from bool -> string, map 'Y' or 'N' to any +// members marked with a YesNoAttribute: +Mapper.WhenMapping + .From() // Apply to bool mappings + .To() // Apply to all string mappings + .IfTargetMemberMatches(m => m.HasAttribute()) + .Map((b, str) => b ? "Y" : "N") // Map 'Y' or 'N' + .ToTarget(); // The bool is the target +``` + +`IfTargetMemberMatches()` data sources must be configured using `ToTarget()` or `ToTargetInstead()`; +as target member selection is performed using the matcher, it is invalid to specify a particular +target member, _e.g_ using `To(t => t.Name)`. + +[Source](/configuration/Ignoring-Source-Members#source-member-filtering) and +[target](/configuration/Ignoring-Target-Members#target-member-filtering) members can also be ignored +using a matcher. + +#### Matching Options + +Target members can be matched by type: + +```csharp +// Match all string members: +Mapper.WhenMapping + .To() // Apply to all ProductDto mappings + .IfTargetMemberMatches(m => m.HasType()) + .Map("MatchedByType") + .ToTarget(); +``` + +...by member type: + +```csharp +// Match fields and properties: +Mapper.WhenMapping + .ToANew() // Apply to ProductDto creations + .IfTargetMemberMatches(m => m.IsField || m.IsProperty) + .Map("MatchedByMatcher") + .ToTarget(); +``` + +...by member name: + +```csharp +// Match any Product members with names starting with 'Id': +Mapper.WhenMapping + .To() // Apply to all Product mappings + .IfTargetMemberMatches(m => m.Name.StartsWith("Id")) + .Map("MatchedByName") + .ToTarget(); +``` + +...by member path: + +```csharp +Mapper.WhenMapping + .To() // Apply to all Customer mappings + .IfTargetMemberMatches(m => + m.Path.StartsWith("ContactDetails.") && + m.Path.Contains("Address")) + .Map("MatchedByPath") + .ToTarget(); +``` + +...or by `MemberInfo` matcher: + +```csharp +Mapper.WhenMapping + .To() // Apply to all Customer mappings + .IfTargetMemberMatches(m => + m.IsFieldMatching(f => f.IsSpecialName)) + .Map("MatchedByFieldMatcher") + .ToTarget(); +``` + + + + + diff --git a/docs/src/index.md b/docs/src/index.md index e96190619..09769ee86 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,6 +1,6 @@ ## Overview -AgileMapper is a zero-configuration, [highly-configurable](/configuration) object-object mapper with [viewable execution plans](/Using-Execution-Plans), targetting [.NET Standard 1.0+](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) and .NET 3.5+. It performs [query projections](/query-projection), object creation, deep clones, id-aware [updates](/Performing-Updates) and [merges](/Performing-Merges), and can be used via [extension methods](/Mapping-Extension-Methods), or a [static or instance](/Static-vs-Instance-Mappers) API. +AgileMapper is a zero-configuration, [highly-configurable](/configuration) object-object mapper with [viewable execution plans](/Using-Execution-Plans), targeting [.NET Standard 1.0+](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) and .NET 3.5+. It performs [query projections](/query-projection), object creation, deep clones, id-aware [updates](/Performing-Updates) and [merges](/Performing-Merges), and can be used via [extension methods](/Mapping-Extension-Methods), or a [static or instance](/Static-vs-Instance-Mappers) API. Mapping functions are created and cached the first time two types are mapped - no up-front configuration is necessary. You can [cache up-front](/Using-Execution-Plans) if you prefer, though.