From edc051086386cedcc7649a20d75305a5843bd7a0 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Mon, 20 Apr 2020 16:40:00 +0100 Subject: [PATCH] Features/sequential data sources (#196) * Basic sequential data source support, re: issue #184 * Erroring if duplicate sequential data sources are configured * Extra test coverage * Erroring if sequential data source configured for simple type members * Extra test coverage * Erroring if configured data sources conflict with ignored source members * Start of API update * Updating sequential data source API * Support for conditional sequential data sources * Extra test coverage * Extending test coverage * Adding documentation * Extra test coverage * Start of sequential data sources for ctor parameters / Properly using fallback values in more places / Fixing tests * Moving map methods back into interface to fix test overload resolution?! * Fixing data source conflict tests * Removing sequential ctor data source test * Updating to v1.7-preview1 --- .../WhenConfiguringDataSourcesInline.cs | 2 - ...ConfiguringDataSourcesInlineIncorrectly.cs | 8 +- .../WhenIgnoringMembersInlineIncorrectly.cs | 4 +- .../WhenConfiguringDataSourcesIncorrectly.cs | 2 +- .../WhenConfiguringSequentialDataSources.cs | 360 ++++++++++++++++++ .../MapperCloning/WhenCloningDataSources.cs | 6 +- .../WhenMappingToConstructors.cs | 12 +- AgileMapper/AgileMapper.csproj | 3 + .../ConfigInfoDataSourceExtensions.cs | 29 ++ .../CustomDataSourceTargetMemberSpecifier.cs | 227 +++++++++-- .../IConditionalMapSourceConfigurator.cs | 46 +++ .../IConditionalMappingConfigurator.cs | 4 +- .../IConditionalRootMappingConfigurator.cs | 54 +-- .../IConfiguredDataSourceFactoryFactory.cs | 15 + .../ICustomDataSourceTargetMemberSpecifier.cs | 7 + .../Configuration/IMapSourceConfigurator.cs | 57 +++ .../Configuration/IRootMappingConfigurator.cs | 76 ++-- .../ISequencedDataSourceFactory.cs | 18 + .../MappingConfigContinuation.cs | 8 +- .../Api/Configuration/MappingConfigurator.cs | 21 +- .../Configuration/ConfigurationType.cs | 8 + .../ConfiguredDataSourceFactory.cs | 49 ++- .../Lambdas/ConfiguredLambdaInfo.cs | 32 +- .../Configuration/MappingConfigInfo.cs | 26 +- .../MemberIgnores/ConfiguredMemberFilter.cs | 9 +- .../MemberIgnores/ConfiguredMemberIgnore.cs | 9 +- .../ConfiguredMemberIgnoreBase.cs | 22 +- .../ConfiguredSourceMemberFilter.cs | 11 +- .../ConfiguredSourceMemberIgnore.cs | 9 + .../ConfiguredSourceMemberIgnoreBase.cs | 26 ++ .../Configuration/UserConfigurationSet.cs | 13 +- .../Configuration/UserConfiguredItemBase.cs | 6 +- .../DataSources/ConfiguredDataSource.cs | 4 + AgileMapper/DataSources/DataSourceBase.cs | 30 +- AgileMapper/DataSources/DataSourceSet.cs | 27 +- .../DataSources/DictionaryEntryDataSource.cs | 12 +- .../Factories/ConfiguredDataSourcesFactory.cs | 6 +- .../Factories/MemberDataSourceSetFactory.cs | 5 +- AgileMapper/DataSources/IDataSource.cs | 7 +- .../DataSources/ValueExpressionBuilders.cs | 10 +- .../Internal/EnumerableExtensions.cs | 37 +- .../Internal/ExpressionExtensions.Replace.cs | 26 +- .../Internal/ExpressionExtensions.cs | 12 +- AgileMapper/Members/ConfiguredSourceMember.cs | 2 +- AgileMapper/Members/MemberExtensionMethods.cs | 48 ++- .../Members/MemberMapperDataExtensions.cs | 8 + .../Members/Population/MemberPopulator.cs | 27 +- .../ConfiguredMappingFactory.cs | 1 + Directory.Build.props | 2 +- docs/src/configuration/Member-Values.md | 35 +- 50 files changed, 1213 insertions(+), 265 deletions(-) create mode 100644 AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs create mode 100644 AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs create mode 100644 AgileMapper/Api/Configuration/IConditionalMapSourceConfigurator.cs create mode 100644 AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs create mode 100644 AgileMapper/Api/Configuration/IMapSourceConfigurator.cs create mode 100644 AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs create mode 100644 AgileMapper/Configuration/ConfigurationType.cs diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs index 250f4af91..7ea4ea930 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -162,7 +162,6 @@ public void ShouldApplyDifferingInlineDelegateDataSources() .Map(SubtractOne) .To(pf => pf.Value)); - var result2 = mapper .Map(source2) .ToANew>(c => c @@ -172,7 +171,6 @@ public void ShouldApplyDifferingInlineDelegateDataSources() result1.Value.ShouldBe(source1.Value - 1); result2.Value.ShouldBe(source2.Value - 3); } - } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs index b19c35e01..9371b8085 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs @@ -73,7 +73,9 @@ public void ShouldErrorIfIgnoredMemberHasDataSourceConfiguredInline() } }); - inlineConfigEx.Message.ShouldContain("Ignored member Target.Value has a configured data source"); + inlineConfigEx.Message.ShouldContain("'ctx.Source.Value + \"?!\"'"); + inlineConfigEx.Message.ShouldContain("PublicField.Value"); + inlineConfigEx.Message.ShouldContain("conflicts with an ignored member"); } [Fact] @@ -97,7 +99,7 @@ public void ShouldErrorIfRedundantDataSourceIsConfiguredInline() } }); - inlineConfigEx.Message.ShouldContain("already has configured data source 'Person.Id'"); + inlineConfigEx.Message.ShouldContain("already has configured data source Person.Id"); } [Fact] @@ -118,7 +120,7 @@ public void ShouldErrorIfConflictingDataSourceIsConfigured() } }); - conflictEx.Message.ShouldContain("already has configured data source 'Customer.Id'"); + conflictEx.Message.ShouldContain("already has configured data source Customer.Id"); } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringMembersInlineIncorrectly.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringMembersInlineIncorrectly.cs index 6d9bdd87b..25fa53df5 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringMembersInlineIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringMembersInlineIncorrectly.cs @@ -29,7 +29,9 @@ public void ShouldErrorIfConfiguredDataSourceMemberIsIgnoredInline() } }); - inlineConfigEx.Message.ShouldContain("Ignored member Target.Value has a configured data source"); + inlineConfigEx.Message.ShouldContain("'ctx.Source.Value + \"?!\"'"); + inlineConfigEx.Message.ShouldContain("PublicField.Value"); + inlineConfigEx.Message.ShouldContain("conflicts with an ignored member"); } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs index 615a98127..63812b5f8 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs @@ -195,7 +195,7 @@ public void ShouldErrorIfConflictingDataSourceIsConfigured() } }); - conflictEx.Message.ShouldContain("already has configured data source 'Person.Id'"); + conflictEx.Message.ShouldContain("already has configured data source Person.Id"); } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs new file mode 100644 index 000000000..b6e37dfe8 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringSequentialDataSources.cs @@ -0,0 +1,360 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using System; + using AgileMapper.Configuration; + using AgileMapper.Extensions.Internal; + 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/184 + public class WhenConfiguringSequentialDataSources + { + [Fact] + public void ShouldApplyASequentialDataSourceToANestedComplexTypeMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .Map((src, _) => src.TheCat) + .Then.Map((src, _) => src.TheDog) + .To(tp => tp.PetNames); + + var source = new Issue184.SourcePets + { + TheCat = new Issue184.Cat { CatName = "Tiddles" }, + TheDog = new Issue184.Dog { DogName = "Rover" } + }; + + var result = mapper.Map(source).ToANew(); + + result.PetNames.ShouldNotBeNull(); + result.PetNames.CatName.ShouldBe("Tiddles"); + result.PetNames.DogName.ShouldBe("Rover"); + } + } + + [Fact] + public void ShouldApplyASequentialDataSourceToANestedComplexTypeMemberConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .If((src, _) => src.TheCat.CatName.Length > 5) + .Map((src, _) => src.TheCat) + .Then.Map((src, _) => src.TheDog) + .To(tp => tp.PetNames); + + var nonMatchingSource = new Issue184.SourcePets + { + TheCat = new Issue184.Cat { CatName = "Meow" }, + TheDog = new Issue184.Dog { DogName = "Woof" } + }; + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew(); + + nonMatchingResult.PetNames.ShouldNotBeNull(); + nonMatchingResult.PetNames.CatName.ShouldBeNull(); + nonMatchingResult.PetNames.DogName.ShouldBe("Woof"); + + var matchingSource = new Issue184.SourcePets + { + TheCat = new Issue184.Cat { CatName = "Tiddles" }, + TheDog = new Issue184.Dog { DogName = "Rover" } + }; + + var matchingResult = mapper.Map(matchingSource).ToANew(); + + matchingResult.PetNames.ShouldNotBeNull(); + matchingResult.PetNames.CatName.ShouldBe("Tiddles"); + matchingResult.PetNames.DogName.ShouldBe("Rover"); + } + } + + [Fact] + public void ShouldApplyASequentialDataSourceToARootArray() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew() + .Map((src, _) => src.Value1) + .Then.Map((src, _) => src.Value2) + .ToTarget(); + + var source = new PublicTwoFields + { + Value1 = new[] { new Address { Line1 = "Address 1" } }, + Value2 = new[] + { + new Address { Line1 = "Address 2" }, + new Address { Line1 = "Address 3" } + } + }; + + var result = mapper.Map(source).ToANew(); + + result.Length.ShouldBe(3); + result.First().Line1.ShouldBe("Address 1"); + result.Second().Line1.ShouldBe("Address 2"); + result.Third().Line1.ShouldBe("Address 3"); + } + } + + [Fact] + public void ShouldApplyASequentialDataSourceToARootArrayConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew() + .Map((src, _) => src.Value1) + .Then + .If(ctx => ctx.Source.Value2.Length > 1) + .Map((src, _) => src.Value2) + .ToTarget(); + + var nonMatchingSource = new PublicTwoFields + { + Value1 = new[] { new Address { Line1 = "Address 1" } }, + Value2 = new[] { new Address { Line1 = "Address 2" } } + }; + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew(); + + nonMatchingResult.ShouldHaveSingleItem().Line1.ShouldBe("Address 1"); + + var matchingSource = new PublicTwoFields + { + Value1 = new[] { new Address { Line1 = "Address 1" } }, + Value2 = new[] + { + new Address { Line1 = "Address 2" }, + new Address { Line1 = "Address 3" } + } + }; + + var matchingResult = mapper.Map(matchingSource).ToANew(); + + matchingResult.Length.ShouldBe(3); + matchingResult.First().Line1.ShouldBe("Address 1"); + matchingResult.Second().Line1.ShouldBe("Address 2"); + matchingResult.Third().Line1.ShouldBe("Address 3"); + } + } + + [Fact] + public void ShouldApplySequentialDataSourcesToANestedArrayConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + var sourceType = new + { + First = default(Address[]), + Second = default(Address[]), + Third = default(Address[]) + }; + + mapper.WhenMapping + .From(sourceType) + .To>() + .If((src, tgt, i) => -i.GetValueOrDefault() == 0) + .Map((src, _) => src.First) + .Then + .If((src, _) => src.Second[0].Line2 != null) + .Map((src, _) => src.Second) + .Then + .Map((src, _) => src.Third) + .To(pf => pf.Value); + + var nonMatchingSource = new + { + First = new[] { new Address { Line1 = "Addr 1" } }, + Second = new[] { new Address { Line1 = "Addr 2" } }, + Third = new[] { new Address { Line1 = "Addr 3" } } + }; + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + + nonMatchingResult.Value.ShouldNotBeNull().Length.ShouldBe(2); + nonMatchingResult.Value.First().Line1.ShouldBe("Addr 1"); + nonMatchingResult.Value.Second().Line1.ShouldBe("Addr 3"); + + var matchingSource = new + { + First = new[] { new Address { Line1 = "Addr 1" } }, + Second = new[] { new Address { Line1 = "Addr 2.1", Line2 = "Addr 2.2" } }, + Third = new[] { new Address { Line1 = "Addr 3" } } + }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + + matchingResult.Value.ShouldNotBeNull().Length.ShouldBe(3); + matchingResult.Value.First().Line1.ShouldBe("Addr 1"); + matchingResult.Value.Second().Line1.ShouldBe("Addr 2.1"); + matchingResult.Value.Second().Line2.ShouldBe("Addr 2.2"); + matchingResult.Value.Third().Line1.ShouldBe("Addr 3"); + } + } + + [Fact] + public void ShouldHandleANullSequentialDataSource() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .Map((src, _) => src.TheCat) + .Then.Map((src, _) => src.TheDog) + .To(tp => tp.PetNames); + + var source = new Issue184.SourcePets + { + TheDog = new Issue184.Dog { DogName = "Spot" } + }; + + var result = mapper.Map(source).ToANew(); + + result.PetNames.ShouldNotBeNull(); + result.PetNames.CatName.ShouldBeNull(); + result.PetNames.DogName.ShouldBe("Spot"); + } + } + + [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 + { + public class SourcePets + { + public Cat TheCat { get; set; } + + public Dog TheDog { get; set; } + } + + public class Cat + { + public string CatName { get; set; } + } + + public class Dog + { + public string DogName { get; set; } + } + + public class TargetPets + { + public PetNames PetNames { get; set; } + } + + public class PetNames + { + public string CatName { get; set; } + + public string DogName { get; set; } + } + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests/MapperCloning/WhenCloningDataSources.cs b/AgileMapper.UnitTests/MapperCloning/WhenCloningDataSources.cs index 014b816aa..f26f695f3 100644 --- a/AgileMapper.UnitTests/MapperCloning/WhenCloningDataSources.cs +++ b/AgileMapper.UnitTests/MapperCloning/WhenCloningDataSources.cs @@ -218,7 +218,7 @@ public void ShouldErrorIfRedundantDataSourceIsConfigured() } }); - conflictEx.Message.ShouldContain("already has configured data source 'Address.Line1'"); + conflictEx.Message.ShouldContain("already has configured data source Address.Line1"); } [Fact] @@ -243,7 +243,9 @@ public void ShouldErrorIfFilteredMemberDataSourceIsConfigured() } }); - dataSourceEx.Message.ShouldContain("conflicts with a configured data source"); + dataSourceEx.Message.ShouldContain("Address.Line1 -> Address.Line2"); + dataSourceEx.Message.ShouldContain("conflicts with member ignore"); + dataSourceEx.Message.ShouldContain("'member.Name == \"Line2\"'"); } } } diff --git a/AgileMapper.UnitTests/WhenMappingToConstructors.cs b/AgileMapper.UnitTests/WhenMappingToConstructors.cs index 18ade044b..c060b4c17 100644 --- a/AgileMapper.UnitTests/WhenMappingToConstructors.cs +++ b/AgileMapper.UnitTests/WhenMappingToConstructors.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests { using System; + using System.Collections.Generic; using Common; using TestClasses; #if !NET35 @@ -104,10 +105,19 @@ public void ShouldPopulateMembersMatchingUnusedConstructorParameters() { var source = new { Value1 = 123 }; var result = Mapper.Map(source).ToANew>(); - + result.Value1.ShouldBe(123); } + [Fact] + public void ShouldDefaultCollectionParametersToEmpty() + { + var source = new PublicField>(); + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBeEmpty(); + } + #region Helper Classes // ReSharper disable ClassNeverInstantiated.Local diff --git a/AgileMapper/AgileMapper.csproj b/AgileMapper/AgileMapper.csproj index 134e826b5..9be3172fd 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -21,8 +21,11 @@ MIT ./Icon.png + - Support for configuring sequential data sources, re: #184 - Including ToTarget data sources in mapping plan validation, re: #184 - Fixing caching of custom method struct creation factory + - Erroring if configured data sources conflict with ignored source members + - Improving configuration error messages - Performance improvements diff --git a/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs b/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs new file mode 100644 index 000000000..283c1933a --- /dev/null +++ b/AgileMapper/Api/Configuration/ConfigInfoDataSourceExtensions.cs @@ -0,0 +1,29 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using AgileMapper.Configuration; + + internal static class ConfigInfoDataSourceExtensions + { + public static ISequencedDataSourceFactory[] GetSequenceDataSourceFactories( + this MappingConfigInfo configInfo) + { + return configInfo.Get(); + } + + public static MappingConfigInfo ForSequentialConfiguration( + this MappingConfigInfo configInfo, + ISequencedDataSourceFactory[] dataSourceFactorySequence) + { + return configInfo.Copy() + .ForSequentialConfiguration() + .SetSequenceDataSourceFactories(dataSourceFactorySequence); + } + + public static MappingConfigInfo SetSequenceDataSourceFactories( + this MappingConfigInfo configInfo, + ISequencedDataSourceFactory[] dataSourceFactorySequence) + { + return configInfo.Set(dataSourceFactorySequence); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index abbba97d1..5a0fd72e1 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -15,8 +15,10 @@ using Members.Dictionaries; using NetStandardPolyfills; using Projection; + using ReadableExpressions; using ReadableExpressions.Extensions; using TypeConversion; + using static System.Linq.Expressions.ExpressionType; #if NET35 using Expr = Microsoft.Scripting.Ast.Expression; using ExprType = Microsoft.Scripting.Ast.ExpressionType; @@ -24,17 +26,20 @@ using Expr = System.Linq.Expressions.Expression; using ExprType = System.Linq.Expressions.ExpressionType; #endif - using static System.Linq.Expressions.ExpressionType; - using static ObjectPopulation.InvocationPosition; internal class CustomDataSourceTargetMemberSpecifier : ICustomDataSourceTargetMemberSpecifier, - ICustomProjectionDataSourceTargetMemberSpecifier + ICustomProjectionDataSourceTargetMemberSpecifier, + IConfiguredDataSourceFactoryFactory, + ISequencedDataSourceFactory { private readonly MappingConfigInfo _configInfo; private readonly LambdaExpression _customValueLambda; private readonly bool _valueCouldBeSourceMember; + private readonly ISequencedDataSourceFactory[] _sequenceDataSourceFactories; private ConfiguredLambdaInfo _customValueLambdaInfo; + private ParameterInfo _targetCtorParameter; + private LambdaExpression _targetMemberLambda; public CustomDataSourceTargetMemberSpecifier( MappingConfigInfo configInfo, @@ -48,21 +53,33 @@ public CustomDataSourceTargetMemberSpecifier( public CustomDataSourceTargetMemberSpecifier( MappingConfigInfo configInfo, - ConfiguredLambdaInfo customValueLambda) + ConfiguredLambdaInfo customValueLambdaInfo) + : this(configInfo) + { + _customValueLambdaInfo = customValueLambdaInfo; + } + + private CustomDataSourceTargetMemberSpecifier(MappingConfigInfo configInfo) { _configInfo = configInfo; - _customValueLambdaInfo = customValueLambda; + _sequenceDataSourceFactories = configInfo.GetSequenceDataSourceFactories(); } private MapperContext MapperContext => _configInfo.MapperContext; + public IConditionalMapSourceConfigurator Then => + new MappingConfigurator(_configInfo + .ForSequentialConfiguration(_sequenceDataSourceFactories.Append(this))); + public ICustomDataSourceMappingConfigContinuation To( Expression> targetMember) { ThrowIfTargetParameterSpecified(targetMember); + ThrowIfSequentialDataSourceForSimpleMember(targetMember); ThrowIfRedundantSourceMember(targetMember); - return RegisterDataSource(() => CreateFromLambda(targetMember)); + SetTargetMemberForSequence(targetMember); + return RegisterDataSource(cdsff => cdsff.CreateFromLambda()); } IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.To( @@ -70,15 +87,34 @@ IProjectionConfigContinuation ICustomProjectionDataSourceTarge { ThrowIfTargetParameterSpecified(resultMember); - return RegisterDataSource(() => CreateFromLambda(resultMember)); + SetTargetMemberForSequence(resultMember); + return RegisterDataSource(cdsff => cdsff.CreateFromLambda()); } public IMappingConfigContinuation To( Expression>> targetSetMethod) { - return RegisterDataSource(() => CreateFromLambda(targetSetMethod)); + SetTargetMemberForSequence(targetSetMethod); + 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; + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local private static void ThrowIfTargetParameterSpecified(LambdaExpression targetMember) { @@ -90,6 +126,38 @@ private static void ThrowIfTargetParameterSpecified(LambdaExpression targetMembe } } + private void ThrowIfSequentialDataSourceForSimpleMember( + LambdaExpression targetMemberLambda) + { + if (_configInfo.IsSequentialConfiguration && typeof(TTargetValue).IsSimple()) + { + ThrowSimpleMemberSequentialDataSourceError(targetMemberLambda); + } + } + + private void ThrowSimpleMemberSequentialDataSourceError( + LambdaExpression targetMemberLambda) + { + var targetMember = GetTargetMemberOrNull(targetMemberLambda); + + if (targetMember == null) + { + return; + } + + var sourceValue = GetValueLambdaInfo(); + + throw new MappingConfigurationException(string.Format( + CultureInfo.InvariantCulture, + "Source {0} {1} cannot be sequentially applied to target {2} {3} {4} - " + + "simple type {2}s cannot have sequential data sources", + sourceValue.GetDescription(_configInfo), + GetTypeDescription(sourceValue.ReturnType), + GetTargetMemberType(targetMember), + targetMember.GetFriendlyTargetPath(_configInfo), + GetTypeDescription(typeof(TTargetValue)))); + } + private void ThrowIfRedundantSourceMember(LambdaExpression targetMemberLambda) { if (!_valueCouldBeSourceMember) @@ -97,7 +165,7 @@ private void ThrowIfRedundantSourceMember(LambdaExpression targetM return; } - var targetMember = targetMemberLambda.ToTargetMember(MapperContext, nt => { }); + var targetMember = GetTargetMemberOrNull(targetMemberLambda); if (targetMember == null) { @@ -109,9 +177,12 @@ private void ThrowIfRedundantSourceMember(LambdaExpression targetM ThrowIfRedundantSourceMember(valueLambdaInfo, targetMember); } + private QualifiedMember GetTargetMemberOrNull(LambdaExpression targetMemberLambda) + => targetMemberLambda.ToTargetMemberOrNull(MapperContext); + private void ThrowIfRedundantSourceMember(ConfiguredLambdaInfo valueLambdaInfo, QualifiedMember targetMember) { - if (!valueLambdaInfo.IsSourceMember(out var sourceMemberLambda)) + if (!valueLambdaInfo.TryGetSourceMember(out var sourceMemberLambda)) { return; } @@ -134,27 +205,28 @@ private void ThrowIfRedundantSourceMember(ConfiguredLambdaInfo valueLambdaInfo, return; } - var targetMemberType = targetMember.IsConstructorParameter() ? "constructor parameter" : "member"; - throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, "Source member {0} will automatically be mapped to target {1} {2}, " + "and does not need to be configured", GetSourceMemberDescription(configuredSourceMember), - targetMemberType, + GetTargetMemberType(targetMember), targetMember.GetFriendlyTargetPath(_configInfo))); } - private ConfiguredDataSourceFactory CreateFromLambda(LambdaExpression targetMemberLambda) + private static string GetTargetMemberType(QualifiedMember targetMember) + => targetMember.IsConstructorParameter() ? "constructor parameter" : "member"; + + private ConfiguredDataSourceFactory CreateFromLambda() { var valueLambdaInfo = GetValueLambdaInfo(); - if (IsDictionaryEntry(targetMemberLambda, out var dictionaryEntryMember)) + if (IsDictionaryEntry(out var dictionaryEntryMember)) { return new ConfiguredDictionaryEntryDataSourceFactory(_configInfo, valueLambdaInfo, dictionaryEntryMember); } - return CreateDataSourceFactory(valueLambdaInfo, targetMemberLambda); + return CreateDataSourceFactory(valueLambdaInfo); } private ConfiguredLambdaInfo GetValueLambdaInfo() => GetValueLambdaInfo(typeof(TTargetValue)); @@ -192,15 +264,15 @@ private ConfiguredLambdaInfo GetValueLambdaInfo(Type targetValueType) return _customValueLambdaInfo = valueLambdaInfo; } - private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out DictionaryTargetMember entryMember) + private bool IsDictionaryEntry(out DictionaryTargetMember entryMember) { - if (targetMemberLambda.Body.NodeType != Call) + if (_targetMemberLambda.Body.NodeType != Call) { entryMember = null; return false; } - var methodCall = (MethodCallExpression)targetMemberLambda.Body; + var methodCall = (MethodCallExpression)_targetMemberLambda.Body; if (!methodCall.Method.IsSpecialName || (methodCall.Method.Name != "get_Item") || @@ -234,26 +306,24 @@ private QualifiedMember CreateRootTargetQualifiedMember() : MapperContext.QualifiedMemberFactory.RootTarget(); } - private ConfiguredDataSourceFactory CreateDataSourceFactory( - ConfiguredLambdaInfo valueLambdaInfo, - LambdaExpression targetMemberLambda) + private ConfiguredDataSourceFactory CreateDataSourceFactory(ConfiguredLambdaInfo valueLambdaInfo) { return new ConfiguredDataSourceFactory( _configInfo, valueLambdaInfo, #if NET35 - targetMemberLambda.ToDlrExpression(), + _targetMemberLambda.ToDlrExpression(), #else - targetMemberLambda, + _targetMemberLambda, #endif _valueCouldBeSourceMember); } public IMappingConfigContinuation ToCtor() - => RegisterDataSource(CreateForCtorParam); + => RegisterDataSource(cdsff => cdsff.CreateForCtorParam()); IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.ToCtor() - => RegisterDataSource(CreateForCtorParam); + => RegisterDataSource(cdsff => cdsff.CreateForCtorParam()); public IMappingConfigContinuation ToCtor(string parameterName) => RegisterNamedContructorParameterDataSource(parameterName); @@ -267,13 +337,16 @@ IProjectionConfigContinuation ICustomProjectionDataSourceTarge #region Ctor Helpers private ConfiguredDataSourceFactory CreateForCtorParam() - => CreateForCtorParam(GetUniqueConstructorParameterOrThrow()); + { + SetTargetCtorParameterForSequence(GetUniqueConstructorParameterOrThrow()); + return CreateForCtorParam(); + } private MappingConfigContinuation RegisterNamedContructorParameterDataSource(string name) { - var parameter = GetUniqueConstructorParameterOrThrow(name); + SetTargetCtorParameterForSequence(GetUniqueConstructorParameterOrThrow(name)); - return RegisterDataSource(parameter.ParameterType, () => CreateForCtorParam(parameter)); + return RegisterDataSource(_targetCtorParameter.ParameterType, cdsff => cdsff.CreateForCtorParam()); } private static ParameterInfo GetUniqueConstructorParameterOrThrow(string name = null) @@ -336,24 +409,45 @@ private static Exception AmbiguousParameterException(string parameterMatchInfo) typeof(TTarget).GetFriendlyName())); } - private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) + private void SetTargetCtorParameterForSequence(ParameterInfo parameter) + { + SetTargetCtorParameter(parameter); + + if (_sequenceDataSourceFactories == null) + { + return; + } + + foreach (var dataSourceFactory in _sequenceDataSourceFactories) + { + dataSourceFactory.SetTargetCtorParameter(parameter); + } + } + + private void SetTargetCtorParameter(ParameterInfo parameter) => _targetCtorParameter = parameter; + + private ConfiguredDataSourceFactory CreateForCtorParam() { - var valueLambda = GetValueLambdaInfo(parameter.ParameterType); - var constructorParameter = CreateRootTargetQualifiedMember().Append(Member.ConstructorParameter(parameter)); + var valueLambda = GetValueLambdaInfo(_targetCtorParameter.ParameterType); + var ctorParameterMember = Member.ConstructorParameter(_targetCtorParameter); + var ctorParameter = CreateRootTargetQualifiedMember().Append(ctorParameterMember); - ThrowIfRedundantSourceMember(valueLambda, constructorParameter); + ThrowIfRedundantSourceMember(valueLambda, ctorParameter); - return new ConfiguredDataSourceFactory(_configInfo, valueLambda, constructorParameter); + return new ConfiguredDataSourceFactory(_configInfo, valueLambda, ctorParameter); } #endregion public IMappingConfigContinuation ToTarget() + => RegisterDataSource(cdsff => cdsff.CreateForToTarget()); + + private ConfiguredDataSourceFactory CreateForToTarget() { - return RegisterDataSource(() => new ConfiguredDataSourceFactory( + return new ConfiguredDataSourceFactory( _configInfo, GetValueLambdaInfo(), - CreateRootTargetQualifiedMember())); + CreateRootTargetQualifiedMember()); } public IMappingConfigContinuation ToTarget() @@ -369,7 +463,6 @@ public IMappingConfigContinuation ToTarget() return new MappingConfigurator(_configInfo).MapTo(); } - // ReSharper disable once UnusedMember.Local private void SetDerivedToTargetSource(MappingConfigInfo derivedTypeConfigInfo) { new MappingConfigurator(derivedTypeConfigInfo) @@ -380,23 +473,40 @@ private void SetDerivedToTargetSource(MappingConfigInfo derivedT private static string GetTypeDescription(Type type) => $"of type '{type.GetFriendlyName()}'"; private MappingConfigContinuation RegisterDataSource( - Func dataSourceFactoryFactory) + Func dataSourceFactoryFactory) { return RegisterDataSource(typeof(TTargetValue), dataSourceFactoryFactory); } private MappingConfigContinuation RegisterDataSource( Type targetMemberType, - Func dataSourceFactoryFactory) + Func dataSourceFactoryFactory) { ThrowIfInvalid(targetMemberType); - RegisterComplexTypeFactoryMethodIfAppropriate(targetMemberType); - MapperContext.UserConfigurations.Add(dataSourceFactoryFactory.Invoke()); + if (_sequenceDataSourceFactories != null) + { + foreach (var dataSourceFactory in _sequenceDataSourceFactories) + { + dataSourceFactory.Register(dataSourceFactoryFactory, targetMemberType); + } + + _configInfo.SetSequenceDataSourceFactories(null); + } + + Register(dataSourceFactoryFactory, targetMemberType); return new MappingConfigContinuation(_configInfo); } + private void Register( + Func dataSourceFactoryFactory, + Type targetMemberType) + { + RegisterComplexTypeFactoryMethodIfAppropriate(targetMemberType); + MapperContext.UserConfigurations.Add(dataSourceFactoryFactory.Invoke(this)); + } + private void ThrowIfInvalid(Type targetMemberType) { ThrowIfSimpleSourceForNonSimpleTargetMember(targetMemberType); @@ -474,7 +584,7 @@ private string GetSourceValueDescription(Expression customValue) { if (customValue.NodeType != MemberAccess) { - return $"Source type '{customValue.Type.GetFriendlyName()}'"; + return $"Source value {customValue.ToReadableString()} {GetTypeDescription(customValue.Type)}"; } var sourceMember = customValue.ToSourceMember(MapperContext); @@ -531,5 +641,38 @@ private void RegisterComplexTypeFactoryMethod() } private struct AnyParameterType { } + + #region IConfiguredDataSourceFactoryFactory Members + + ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateForCtorParam() + => CreateForCtorParam(); + + ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateForCtorParam() + => CreateForCtorParam(); + + ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateFromLambda() + => CreateFromLambda(); + + ConfiguredDataSourceFactory IConfiguredDataSourceFactoryFactory.CreateForToTarget() + => CreateForToTarget(); + + #endregion + + #region ISequencedDataSourceFactory Members + + void ISequencedDataSourceFactory.SetTargetCtorParameter(ParameterInfo parameter) + => SetTargetCtorParameter(parameter); + + void ISequencedDataSourceFactory.SetTargetMember(LambdaExpression targetMember) + => SetTargetMember(targetMember); + + void ISequencedDataSourceFactory.Register( + Func dataSourceFactoryFactory, + Type targetMemberType) + { + Register(dataSourceFactoryFactory, targetMemberType); + } + + #endregion } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IConditionalMapSourceConfigurator.cs b/AgileMapper/Api/Configuration/IConditionalMapSourceConfigurator.cs new file mode 100644 index 000000000..111bf82ab --- /dev/null +++ b/AgileMapper/Api/Configuration/IConditionalMapSourceConfigurator.cs @@ -0,0 +1,46 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using System; + using System.Linq.Expressions; + using Members; + + /// + /// Provides options for configuring a condition which must evaluate to true for a configured + /// value to be mapped to a given target when mapping 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 IConditionalMapSourceConfigurator : IMapSourceConfigurator + { + /// + /// Configure a condition which must evaluate to true for a configured value to be mapped + /// to a given target. The condition expression is passed a context object containing the + /// current mapping's source and target objects. + /// + /// The condition to evaluate. + /// An IConditionalRootMappingConfigurator with which to complete the configuration. + IMapSourceConfigurator If( + Expression, bool>> condition); + + /// + /// Configure a condition which must evaluate to true for a configured value to be mapped + /// to a given target. The condition expression is passed the current mapping's source and + /// target objects. + /// + /// The condition to evaluate. + /// An IConditionalRootMappingConfigurator with which to complete the configuration. + IMapSourceConfigurator If( + Expression> condition); + + /// + /// Configure a condition which must evaluate to true for a configured value to be mapped + /// to a given target. The condition expression is passed the current mapping's source and + /// target objects and the current enumerable index, if applicable. + /// + /// The condition to evaluate. + /// An IConditionalRootMappingConfigurator with which to complete the configuration. + IMapSourceConfigurator If( + Expression> condition); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs b/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs index a4c68866d..0a2343b3c 100644 --- a/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IConditionalMappingConfigurator.cs @@ -5,8 +5,8 @@ using Members; /// - /// Provides options for configuring a condition which must evaluate to true for the configuration to apply - /// to mappings from and to the source and target types being configured. + /// Provides options for configuring a condition 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. diff --git a/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs b/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs index cdcf65690..49609d1fd 100644 --- a/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs @@ -1,32 +1,32 @@ -namespace AgileObjects.AgileMapper.Api.Configuration -{ +namespace AgileObjects.AgileMapper.Api.Configuration +{ /// /// Provides options for configuring a mapping based on the preceding condition. /// /// The source type to which the configuration should apply. - /// The target type to which the configuration should apply. - public interface IConditionalRootMappingConfigurator - : IRootMappingConfigurator - { - /// - /// Map the source type being configured to the derived target type specified by - /// if the preceding condition evaluates to true. - /// - /// The derived target type to create. - /// - /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - IMappingConfigContinuation MapTo() - where TDerivedTarget : TTarget; - - /// - /// Map the target type being configured to null if the preceding condition evaluates to true. - /// - /// - /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - IMappingConfigContinuation MapToNull(); - } + /// The target type to which the configuration should apply. + public interface IConditionalRootMappingConfigurator + : IRootMappingConfigurator + { + /// + /// Map the source type being configured to the derived target type specified by + /// if the preceding condition evaluates to true. + /// + /// The derived target type to create. + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and + /// target type being configured. + /// + IMappingConfigContinuation MapTo() + where TDerivedTarget : TTarget; + + /// + /// Map the target type being configured to null if the preceding condition evaluates to true. + /// + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and + /// target type being configured. + /// + IMappingConfigContinuation MapToNull(); + } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs b/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs new file mode 100644 index 000000000..9042e51da --- /dev/null +++ b/AgileMapper/Api/Configuration/IConfiguredDataSourceFactoryFactory.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using AgileMapper.Configuration; + + internal interface IConfiguredDataSourceFactoryFactory + { + ConfiguredDataSourceFactory CreateFromLambda(); + + ConfiguredDataSourceFactory CreateForCtorParam(); + + ConfiguredDataSourceFactory CreateForCtorParam(); + + ConfiguredDataSourceFactory CreateForToTarget(); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs index 5c7a0b03f..82342e99c 100644 --- a/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs @@ -10,6 +10,13 @@ /// The target type to which the configuration should apply. public interface ICustomDataSourceTargetMemberSpecifier { + /// + /// Perform another configuration of how this mapper maps to and from the source and target + /// types being configured. This property can be used to set up a series of configurations + /// to be applied in sequence. + /// + IConditionalMapSourceConfigurator Then { get; } + /// /// Apply the configuration to the given . /// diff --git a/AgileMapper/Api/Configuration/IMapSourceConfigurator.cs b/AgileMapper/Api/Configuration/IMapSourceConfigurator.cs new file mode 100644 index 000000000..7e8b493d3 --- /dev/null +++ b/AgileMapper/Api/Configuration/IMapSourceConfigurator.cs @@ -0,0 +1,57 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using System; + using System.Linq.Expressions; + using Members; + + /// + /// Provides options for configuring values to map to a specified target in mappings from and to + /// the given source and target type. + /// + /// The source type to which the configuration should apply. + /// The target type to which the configuration should apply. + public interface IMapSourceConfigurator + { + /// + /// Configure a custom data source for a particular target member when mapping from and to the source and + /// target types being configured. The factory expression is passed a context object containing the current + /// mapping's source and target objects. + /// + /// The type of the custom value being configured. + /// The expression to map to the configured target member. + /// + /// An ICustomDataSourceMappingConfigContinuation with which to control the reverse configuration, or further + /// configure mappings from and to the source and target type being configured. + /// + ICustomDataSourceTargetMemberSpecifier Map( + Expression, TSourceValue>> valueFactoryExpression); + + /// + /// Configure a custom data source for a particular target member when mapping from and to the source and + /// target types being configured. The factory expression is passed the current mapping's source and target + /// objects. + /// + /// The type of the custom value being configured. + /// The expression to map to the configured target member. + /// + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom value should be applied. + /// + ICustomDataSourceTargetMemberSpecifier Map( + Expression> valueFactoryExpression); + + /// + /// Configure a custom data source for a particular target member when mapping from and to the source and + /// target types being configured. The factory expression is passed the current mapping's source and target + /// objects and the current element index, if applicable. + /// + /// The type of the custom value being configured. + /// The expression to map to the configured target member. + /// + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom value should be applied. + /// + ICustomDataSourceTargetMemberSpecifier Map( + Expression> valueFactoryExpression); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs b/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs index c3660b8be..5def359a7 100644 --- a/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs @@ -6,7 +6,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration using Members; /// - /// Provides options for configuring mappings from and to a given source and target type. + /// Provides options for configuring mappings from and to the given source and target type. /// /// The source type to which the configuration should apply. /// The target type to which the configuration should apply. @@ -239,60 +239,43 @@ IMappingConfigContinuation IgnoreTargetMembersWhere( Expression> memberFilter); /// - /// Configure a custom data source for the given when mapping from and to the - /// source and target types being configured. The factory expression is passed a context object containing the - /// current mapping's source and target objects. + /// Configure a custom data source for a particular target member when mapping from and to + /// the source and target types being configured. The factory expression is passed a context + /// object containing the current mapping's source and target objects. /// /// The type of the custom value being configured. - /// The target member's type. /// The expression to map to the configured target member. - /// The target member to which to apply the configuration. /// - /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source - /// and target type being configured. - /// - ICustomDataSourceMappingConfigContinuation Map( - Expression> valueFactoryExpression, - Expression> targetMember); - - /// - /// Configure a custom data source for a particular target member when mapping from and to the source and - /// target types being configured. The factory expression is passed a context object containing the current - /// mapping's source and target objects. - /// - /// The type of the custom value being configured. - /// The expression to map to the configured target member. - /// - /// An ICustomDataSourceMappingConfigContinuation with which to control the reverse configuration, or further - /// configure mappings from and to the source and target type being configured. + /// An ICustomDataSourceMappingConfigContinuation with which to control the reverse configuration, + /// or further configure mappings from and to the source and target type being configured. /// ICustomDataSourceTargetMemberSpecifier Map( Expression, TSourceValue>> valueFactoryExpression); /// - /// Configure a custom data source for a particular target member when mapping from and to the source and - /// target types being configured. The factory expression is passed the current mapping's source and target - /// objects. + /// Configure a custom data source for a particular target member when mapping from and to + /// the source and target types being configured. The factory expression is passed the current + /// mapping's source and target objects. /// /// The type of the custom value being configured. /// The expression to map to the configured target member. /// - /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the - /// custom value should be applied. + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to + /// which the custom value should be applied. /// ICustomDataSourceTargetMemberSpecifier Map( Expression> valueFactoryExpression); /// - /// Configure a custom data source for a particular target member when mapping from and to the source and - /// target types being configured. The factory expression is passed the current mapping's source and target - /// objects and the current element index, if applicable. + /// Configure a custom data source for a particular target member when mapping from and to + /// the source and target types being configured. The factory expression is passed the current + /// mapping's source and target objects and the current element index, if applicable. /// /// The type of the custom value being configured. /// The expression to map to the configured target member. /// - /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the - /// custom value should be applied. + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to + /// which the custom value should be applied. /// ICustomDataSourceTargetMemberSpecifier Map( Expression> valueFactoryExpression); @@ -322,19 +305,38 @@ ICustomDataSourceTargetMemberSpecifier MapFunc( ICustomDataSourceTargetMemberSpecifier Map(TSourceValue value); /// - /// Configure a constant value for the given when mapping from and to the - /// source and target types being configured. + /// Configure a custom data source for the given when mapping + /// from and to the source and target types being configured. The factory expression is passed + /// a context object containing the current mapping's source and target objects. + /// + /// The type of the custom value being configured. + /// The target member's type. + /// The expression to map to the configured target member. + /// The target member to which to apply the configuration. + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. + /// + ICustomDataSourceMappingConfigContinuation Map( + Expression> valueFactoryExpression, + Expression> targetMember); + + /// + /// Configure a constant value for the given when mapping from + /// and to the source and target types being configured. /// /// The type of the custom constant value being configured. /// The target member's type. /// The constant value to map to the configured target member. /// The target member to which to apply the configuration. /// - /// 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 Map( TSourceValue value, Expression> targetMember); + + } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs b/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs new file mode 100644 index 000000000..96069fad7 --- /dev/null +++ b/AgileMapper/Api/Configuration/ISequencedDataSourceFactory.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using System; + using System.Linq.Expressions; + using System.Reflection; + using AgileMapper.Configuration; + + internal interface ISequencedDataSourceFactory + { + void SetTargetCtorParameter(ParameterInfo parameter); + + void SetTargetMember(LambdaExpression targetMember); + + void Register( + 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 2419392c8..dfe07122e 100644 --- a/AgileMapper/Api/Configuration/MappingConfigContinuation.cs +++ b/AgileMapper/Api/Configuration/MappingConfigContinuation.cs @@ -67,6 +67,12 @@ IFullProjectionConfigurator IProjectionConfigContinuation CreateNewConfigurator(); private MappingConfigurator CreateNewConfigurator() - => new MappingConfigurator(_configInfo.Copy()); + => CreateNewConfigurator(_configInfo.Copy()); + + private static MappingConfigurator CreateNewConfigurator( + MappingConfigInfo configInfo) + { + return new MappingConfigurator(configInfo); + } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index 1f0aa6b15..0c02237ab 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -27,7 +27,8 @@ internal class MappingConfigurator : IFullProjectionConfigurator, IFullProjectionInlineConfigurator, IConditionalRootMappingConfigurator, - IConditionalRootProjectionConfigurator + IConditionalRootProjectionConfigurator, + IConditionalMapSourceConfigurator { public MappingConfigurator(MappingConfigInfo configInfo) { @@ -169,6 +170,24 @@ public IConditionalRootMappingConfigurator If(Expression If(Expression> condition) => SetCondition(condition); + IMapSourceConfigurator IConditionalMapSourceConfigurator.If( + Expression, bool>> condition) + { + return SetCondition(condition); + } + + IMapSourceConfigurator IConditionalMapSourceConfigurator.If( + Expression> condition) + { + return SetCondition(condition); + } + + IMapSourceConfigurator IConditionalMapSourceConfigurator.If( + Expression> condition) + { + return SetCondition(condition); + } + private MappingConfigurator SetCondition(LambdaExpression conditionLambda) { ConfigInfo.AddConditionOrThrow(conditionLambda); diff --git a/AgileMapper/Configuration/ConfigurationType.cs b/AgileMapper/Configuration/ConfigurationType.cs new file mode 100644 index 000000000..f554ad800 --- /dev/null +++ b/AgileMapper/Configuration/ConfigurationType.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.Configuration +{ + internal enum ConfigurationType + { + Default, + Sequential + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/ConfiguredDataSourceFactory.cs b/AgileMapper/Configuration/ConfiguredDataSourceFactory.cs index 0db57b2fe..85dbca906 100644 --- a/AgileMapper/Configuration/ConfiguredDataSourceFactory.cs +++ b/AgileMapper/Configuration/ConfiguredDataSourceFactory.cs @@ -44,6 +44,8 @@ public ConfiguredDataSourceFactory( 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) @@ -69,7 +71,7 @@ private bool CannotBeReversed(out QualifiedMember targetMember, out string reaso return true; } - if (!_dataSourceLambda.IsSourceMember(out var sourceMemberLambda)) + if (!_dataSourceLambda.TryGetSourceMember(out var sourceMemberLambda)) { targetMember = null; reason = $"configured value '{_dataSourceLambda.GetDescription(ConfigInfo)}' is not a source member"; @@ -138,7 +140,7 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) var otherDataSource = otherConfiguredItem as ConfiguredDataSourceFactory; var isOtherDataSource = otherDataSource != null; - var dataSourceLambdasAreTheSame = HasSameDataSourceLambdaAs(otherDataSource); + var dataSourceLambdasAreTheSame = HasSameDataSourceAs(otherDataSource); if (WasAutoCreated && (otherConfiguredItem is IPotentialAutoCreatedItem otherItem) && @@ -152,17 +154,22 @@ public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) return true; } - if (ConfigInfo.HasSameTypesAs(otherDataSource)) + if (!ConfigInfo.HasSameTypesAs(otherDataSource)) { - return true; + return dataSourceLambdasAreTheSame; + } + + if (otherDataSource.IsSequential) + { + return dataSourceLambdasAreTheSame; } - return dataSourceLambdasAreTheSame; + return true; } #region ConflictsWith Helpers - private bool HasSameDataSourceLambdaAs(ConfiguredDataSourceFactory otherDataSource) + private bool HasSameDataSourceAs(ConfiguredDataSourceFactory otherDataSource) => _dataSourceLambda.IsSameAs(otherDataSource?._dataSourceLambda); protected override bool MembersConflict(UserConfiguredItemBase otherConfiguredItem) @@ -177,8 +184,7 @@ public string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSour var reason = conflictingDataSource._isReversal ? " from an automatically-configured reverse data source" : null; - - return $"{GetTargetMemberPath()} already has configured data source '{existingDataSource}'{reason}"; + return $"{GetTargetMemberPath()} already has configured data source {existingDataSource}{reason}"; } public string GetDescription() @@ -189,7 +195,12 @@ public string GetDescription() return sourceMemberPath + " -> " + targetMemberPath; } - private string GetDataSourceDescription() => _dataSourceLambda.GetDescription(ConfigInfo); + private string GetDataSourceDescription() + { + var description = _dataSourceLambda.GetDescription(ConfigInfo); + + return _dataSourceLambda.IsSourceMember ? description : "'" + description + "'"; + } private string GetTargetMemberPath() => TargetMember.GetFriendlyTargetPath(ConfigInfo); @@ -211,9 +222,27 @@ public IConfiguredDataSource Create(IMemberMapperData mapperData) var configuredCondition = GetConditionOrNull(mapperData); var value = _dataSourceLambda.GetBody(mapperData); - return new ConfiguredDataSource(configuredCondition, value, 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); + #region IPotentialAutoCreatedItem Members public bool WasAutoCreated { get; private set; } diff --git a/AgileMapper/Configuration/Lambdas/ConfiguredLambdaInfo.cs b/AgileMapper/Configuration/Lambdas/ConfiguredLambdaInfo.cs index 3c35b6371..24467e721 100644 --- a/AgileMapper/Configuration/Lambdas/ConfiguredLambdaInfo.cs +++ b/AgileMapper/Configuration/Lambdas/ConfiguredLambdaInfo.cs @@ -22,6 +22,7 @@ internal class ConfiguredLambdaInfo private readonly bool _isForTargetDictionary; private readonly IValueInjector _valueInjector; private LambdaExpression _sourceMemberLambda; + private bool? _isSourceMember; private string _description; private ConfiguredLambdaInfo( @@ -158,26 +159,26 @@ private static ConfiguredLambdaInfo For( public Type ReturnType { get; } - public bool IsSourceMember(out LambdaExpression sourceMemberLambda) - { - if (_lambdaBody.NodeType != ExpressionType.MemberAccess) - { - sourceMemberLambda = null; - return false; - } + public bool IsSourceMember => _isSourceMember ??= TryGetSourceMember(out _); + public bool TryGetSourceMember(out LambdaExpression sourceMemberLambda) + { if (_sourceMemberLambda != null) { sourceMemberLambda = _sourceMemberLambda; return true; } + if (_lambdaBody.NodeType != ExpressionType.MemberAccess) + { + return IsNotSourceMember(out sourceMemberLambda); + } + var memberAccesses = _lambdaBody.GetMemberAccessChain(nt => { }, out var rootExpression); if (memberAccesses.None()) { - sourceMemberLambda = null; - return false; + return IsNotSourceMember(out sourceMemberLambda); } var sourceParameter = default(ParameterExpression); @@ -187,8 +188,7 @@ public bool IsSourceMember(out LambdaExpression sourceMemberLambda) { if (memberAccess.NodeType != ExpressionType.MemberAccess) { - sourceMemberLambda = null; - return false; + return IsNotSourceMember(out sourceMemberLambda); } if (sourceParameter == null) @@ -209,9 +209,17 @@ public bool IsSourceMember(out LambdaExpression sourceMemberLambda) sourceParameter); // ReSharper restore PossibleNullReferenceException + _isSourceMember = true; return true; } + private bool IsNotSourceMember(out LambdaExpression sourceLambda) + { + sourceLambda = null; + _isSourceMember = false; + return false; + } + public bool Supports(MappingRuleSet ruleSet) => ruleSet.Settings?.ExpressionIsSupported(_lambda) != false; @@ -238,7 +246,7 @@ public string GetDescription(MappingConfigInfo configInfo) return _description; } - if (IsSourceMember(out var sourceMemberLambda)) + if (TryGetSourceMember(out var sourceMemberLambda)) { return _description = sourceMemberLambda .ToSourceMember(configInfo.MapperContext) diff --git a/AgileMapper/Configuration/MappingConfigInfo.cs b/AgileMapper/Configuration/MappingConfigInfo.cs index b2c1de2f4..17c23abea 100644 --- a/AgileMapper/Configuration/MappingConfigInfo.cs +++ b/AgileMapper/Configuration/MappingConfigInfo.cs @@ -183,7 +183,8 @@ private void ErrorIfConditionHasTypeTest(LambdaExpression conditionLambda) throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, - "Instead of type testing in condition '{0}', configure for a more specific source or target type.", + "Instead of type testing in condition '{0}', " + + "configure for a more derived source or target type.", condition)); } @@ -237,6 +238,17 @@ public MappingConfigInfo WithInvocationPosition(InvocationPosition position) return this; } + public bool IsSequentialConfiguration { get; private set; } + + public ConfigurationType ConfigurationType { get; private set; } + + public MappingConfigInfo ForSequentialConfiguration() + { + ConfigurationType = ConfigurationType.Sequential; + IsSequentialConfiguration = true; + return this; + } + public T Get() { if (_data == null) @@ -294,13 +306,11 @@ public IQualifiedMemberContext ToMemberContext(QualifiedMember targetMember = nu public MappingConfigInfo Copy() { var cloned = new MappingConfigInfo(MapperContext) - { - SourceType = SourceType, - TargetType = TargetType, - SourceValueType = SourceValueType, - RuleSet = RuleSet, - InvocationPosition = InvocationPosition - }; + .ForRuleSet(RuleSet) + .ForSourceType(SourceType) + .ForTargetType(TargetType) + .ForSourceValueType(SourceValueType) + .WithInvocationPosition(InvocationPosition); if (_data == null) { diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs index ceb3e690c..ff6329a6d 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs @@ -49,12 +49,15 @@ private ConfiguredMemberFilter( public override string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore) => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); + public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + { + return $"Configured data source {conflictingDataSource.GetDescription()} " + + $"conflicts with member ignore pattern '{TargetMemberFilter}'"; + } + public string GetConflictMessage(ConfiguredMemberIgnore conflictingMemberIgnore) => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); - public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) - => $"Member ignore pattern '{TargetMemberFilter}' conflicts with a configured data source"; - public override string GetIgnoreMessage(IQualifiedMember targetMember) => $"{targetMember.Name} is ignored by filter:{Environment.NewLine}{TargetMemberFilter}"; diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs index ecb3aed9e..0cbe2a1c1 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs @@ -29,12 +29,15 @@ private ConfiguredMemberIgnore(MappingConfigInfo configInfo, QualifiedMember tar { } + public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + { + return $"Configured data source {conflictingDataSource.GetDescription()} " + + "conflicts with an ignored member"; + } + public override string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore) => ((IMemberIgnore)this).GetConflictMessage(conflictingMemberIgnore); - public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) - => $"Ignored member {TargetMember.GetPath()} has a configured data source"; - public override string GetIgnoreMessage(IQualifiedMember targetMember) => targetMember.Name + " is ignored"; diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs index cf7ec0638..645321c9c 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs @@ -34,23 +34,23 @@ protected ConfiguredMemberIgnoreBase(MappingConfigInfo configInfo, QualifiedMemb public string GetConflictMessage(UserConfiguredItemBase conflictingConfiguredItem) { - if (conflictingConfiguredItem is ConfiguredDataSourceFactory conflictingDataSource) + switch (conflictingConfiguredItem) { - return GetConflictMessage(conflictingDataSource); + case ConfiguredDataSourceFactory conflictingDataSource: + return GetConflictMessage(conflictingDataSource); + + case ConfiguredMemberIgnoreBase conflictingMemberIgnore: + return GetConflictMessage(conflictingMemberIgnore); + + default: + return $"Member {TargetMember.GetPath()} has been ignored"; } - - if (conflictingConfiguredItem is ConfiguredMemberIgnoreBase conflictingMemberIgnore) - { - return GetConflictMessage(conflictingMemberIgnore); - } - - return $"Member {TargetMember.GetPath()} has been ignored"; } - public abstract string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore); - public abstract string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource); + public abstract string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore); + public abstract string GetIgnoreMessage(IQualifiedMember targetMember); protected override bool HasReverseConflict(UserConfiguredItemBase otherItem) => false; diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs index 5466b3e83..20ec165b8 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs @@ -47,12 +47,17 @@ private ConfiguredSourceMemberFilter( string IMemberFilterIgnore.MemberFilter => SourceMemberFilter; + protected override bool ConflictsWith(QualifiedMember sourceMember) => IsFiltered(sourceMember); + + public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + { + return $"Configured data source {conflictingDataSource.GetDescription()} " + + $"conflicts with source member ignore pattern '{SourceMemberFilter}'"; + } + public override string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore) => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingSourceMemberIgnore); - public string GetConflictMessage(ConfiguredSourceMemberIgnore conflictingMemberIgnore) - => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); - public override bool AppliesTo(IQualifiedMemberContext context) { return base.AppliesTo(context) && diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs index 37408a194..97d2b090b 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs @@ -34,6 +34,15 @@ private ConfiguredSourceMemberIgnore(MappingConfigInfo configInfo, QualifiedMemb QualifiedMember IMemberIgnore.Member => SourceMember; + protected override bool ConflictsWith(QualifiedMember sourceMember) + => SourceMember.Matches(sourceMember); + + public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + { + return $"Configured data source {conflictingDataSource.GetDescription()} " + + "conflicts with an ignored source member"; + } + public override string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore) => ((IMemberIgnore)this).GetConflictMessage(conflictingSourceMemberIgnore); diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs index 6880e3d86..2ecab4926 100644 --- a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs @@ -1,5 +1,7 @@ namespace AgileObjects.AgileMapper.Configuration.MemberIgnores { + using Members; + #if NET35 using System; #endif @@ -17,6 +19,30 @@ protected ConfiguredSourceMemberIgnoreBase(MappingConfigInfo configInfo) { } + public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) + { + if (base.ConflictsWith(otherConfiguredItem)) + { + return true; + } + + if (otherConfiguredItem is ConfiguredDataSourceFactory configuredDataSource) + { + var configuredSourceMember = configuredDataSource.ToSourceMemberOrNull(); + + if (configuredSourceMember != null) + { + return ConflictsWith(configuredSourceMember); + } + } + + return false; + } + + protected abstract bool ConflictsWith(QualifiedMember sourceMember); + + public abstract string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource); + public abstract string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore); #region IPotentialAutoCreatedItem Members diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index f7c059365..06fe92cb9 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -52,8 +52,7 @@ public UserConfigurationSet(MapperContext mapperContext) public bool ValidateMappingPlans { get; set; } - public ICollection AppliedConfigurationTypes - => _appliedConfigurationTypes ??= new List(); + public ICollection AppliedConfigurationTypes => _appliedConfigurationTypes ??= new List(); #region MappedObjectCachingSettings @@ -201,10 +200,10 @@ private bool AutoDataSourceReversalEnabled(T dataItem, Func s.AppliesTo(bd))?.Reverse == true; + .FirstOrDefault(memberContext, (mc, s) => s.AppliesTo(mc))?.Reverse == true; } #endregion @@ -415,6 +414,7 @@ public void Add(ConfiguredDataSourceFactory dataSourceFactory) { if (!dataSourceFactory.TargetMember.IsRoot) { + ThrowIfConflictingIgnoredSourceMemberExists(dataSourceFactory, (dsf, cIsm) => cIsm.GetConflictMessage(dsf)); ThrowIfConflictingIgnoredMemberExists(dataSourceFactory); ThrowIfConflictingDataSourceExists(dataSourceFactory, (dsf, cDsf) => dsf.GetConflictMessage(cDsf)); } @@ -568,7 +568,10 @@ private void ThrowIfConflictingIgnoredSourceMemberExists( Func messageFactory) where TConfiguredItem : UserConfiguredItemBase { - ThrowIfConflictingItemExists(configuredItem, _ignoredSourceMembers, messageFactory); + ThrowIfConflictingItemExists( + configuredItem, + _ignoredSourceMembers, + messageFactory); } internal void ThrowIfConflictingIgnoredMemberExists(TConfiguredItem configuredItem) diff --git a/AgileMapper/Configuration/UserConfiguredItemBase.cs b/AgileMapper/Configuration/UserConfiguredItemBase.cs index a3f01de43..41646ab53 100644 --- a/AgileMapper/Configuration/UserConfiguredItemBase.cs +++ b/AgileMapper/Configuration/UserConfiguredItemBase.cs @@ -8,7 +8,6 @@ #endif using Members; using NetStandardPolyfills; - using ObjectPopulation; using ReadableExpressions.Extensions; internal abstract class UserConfiguredItemBase : IComparable @@ -183,7 +182,7 @@ protected int DoComparisonTo(UserConfiguredItemBase other) { if (ConfigInfo.HasSameTargetTypeAs(other)) { - return GetConditionOrder(other) ?? 0; + return GetSameTypesOrder(other) ?? 0; } if (ConfigInfo.IsForTargetType(other.ConfigInfo)) @@ -205,6 +204,9 @@ protected int DoComparisonTo(UserConfiguredItemBase other) return GetConditionOrder(other) ?? OrderAlphabetically(other); } + protected virtual int? GetSameTypesOrder(UserConfiguredItemBase other) + => GetConditionOrder(other); + private int? GetConditionOrder(UserConfiguredItemBase other) { if (HasConfiguredCondition == other.HasConfiguredCondition) diff --git a/AgileMapper/DataSources/ConfiguredDataSource.cs b/AgileMapper/DataSources/ConfiguredDataSource.cs index 7d09da928..5a7b57d90 100644 --- a/AgileMapper/DataSources/ConfiguredDataSource.cs +++ b/AgileMapper/DataSources/ConfiguredDataSource.cs @@ -16,11 +16,13 @@ internal class ConfiguredDataSource : DataSourceBase, IConfiguredDataSource public ConfiguredDataSource( Expression configuredCondition, Expression value, + bool isSequential, IMemberMapperData mapperData) : this( CreateSourceMember(value, mapperData), configuredCondition, GetConvertedValue(value, mapperData), + isSequential, mapperData) { } @@ -54,10 +56,12 @@ public ConfiguredDataSource( IQualifiedMember sourceMember, Expression configuredCondition, Expression convertedValue, + bool isSequential, IMemberMapperData mapperData) : base(sourceMember, convertedValue, mapperData) { _originalValue = convertedValue; + IsSequential = isSequential; if (configuredCondition == null) { diff --git a/AgileMapper/DataSources/DataSourceBase.cs b/AgileMapper/DataSources/DataSourceBase.cs index c24ceed85..f48d9b959 100644 --- a/AgileMapper/DataSources/DataSourceBase.cs +++ b/AgileMapper/DataSources/DataSourceBase.cs @@ -25,6 +25,7 @@ protected DataSourceBase(IDataSource wrappedDataSource, Expression value) value, wrappedDataSource.Condition) { + IsSequential = wrappedDataSource.IsSequential; SourceMemberTypeTest = wrappedDataSource.SourceMemberTypeTest; } @@ -143,6 +144,8 @@ private static bool IsNotOptionalEntityMemberId(IMemberMapperData mapperData) public bool IsConditional => Condition != null; + public bool IsSequential { get; protected set; } + public virtual bool IsFallback => false; public virtual Expression Condition { get; } @@ -155,6 +158,7 @@ private static bool IsNotOptionalEntityMemberId(IMemberMapperData mapperData) public virtual Expression FinalisePopulationBranch( Expression alternatePopulation, + IDataSource nextDataSource, IMemberMapperData mapperData) { MultiInvocationsProcessor.Process( @@ -168,9 +172,13 @@ public virtual Expression FinalisePopulationBranch( if (condition != null) { - population = (alternatePopulation != null) - ? Expression.IfThenElse(condition, population, alternatePopulation) - : Expression.IfThen(condition, population); + population = (alternatePopulation == null) + ? Expression.IfThen(condition, population) + : GetBranchedPopulation( + condition, + population, + alternatePopulation, + nextDataSource); } if (variables.Any()) @@ -180,5 +188,21 @@ public virtual Expression FinalisePopulationBranch( return population; } + + private static Expression GetBranchedPopulation( + Expression condition, + Expression population, + Expression alternatePopulation, + IDataSource previousDataSource) + { + if (previousDataSource.IsSequential) + { + return Expression.Block( + Expression.IfThen(condition, population), + alternatePopulation); + } + + return Expression.IfThenElse(condition, population, alternatePopulation); + } } } \ No newline at end of file diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index ef5845564..f6362a442 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -14,10 +14,13 @@ internal static class DataSourceSet { #region Factory Methods - public static IDataSourceSet For( + public static IDataSourceSet For(IDataSource dataSource, IDataSourceSetInfo info) + => For(dataSource, info, ValueExpressionBuilders.SingleDataSource); + + private static IDataSourceSet For( IDataSource dataSource, IDataSourceSetInfo info, - Func, IMemberMapperData, Expression> valueBuilder = null) + Func valueBuilder) { if (!dataSource.IsValid) { @@ -39,7 +42,7 @@ public static IDataSourceSet For( public static IDataSourceSet For( IList dataSources, IDataSourceSetInfo info, - Func, IMemberMapperData, Expression> valueBuilder = null) + Func, IMemberMapperData, Expression> valueBuilder) { switch (dataSources.Count) { @@ -47,11 +50,11 @@ public static IDataSourceSet For( return EmptyDataSourceSet.Instance; case 1: - return For(dataSources.First(), info, valueBuilder); + return For(dataSources.First(), info, (ds, md) => valueBuilder.Invoke(new[] { ds }, md)); default: var mapperData = info.MapperData; - + if (TryAdjustToSingleUseableDataSource(ref dataSources, mapperData)) { goto case 1; @@ -147,19 +150,11 @@ private class SingleValueDataSourceSet : IDataSourceSet public SingleValueDataSourceSet( IDataSource dataSource, IMemberMapperData mapperData, - Func, IMemberMapperData, Expression> valueBuilder) + Func valueBuilder) { _dataSource = dataSource; _mapperData = mapperData; - - if (valueBuilder == null) - { - _valueBuilder = ValueExpressionBuilders.SingleDataSource; - } - else - { - _valueBuilder = (ds, md) => valueBuilder.Invoke(new[] { ds }, md); - } + _valueBuilder = valueBuilder; } public bool None => false; @@ -194,7 +189,7 @@ public MultipleValueDataSourceSet( { _dataSources = dataSources; _mapperData = mapperData; - _valueBuilder = valueBuilder ?? ValueExpressionBuilders.ConditionTree; + _valueBuilder = valueBuilder; var dataSourcesCount = dataSources.Count; var variables = default(List); diff --git a/AgileMapper/DataSources/DictionaryEntryDataSource.cs b/AgileMapper/DataSources/DictionaryEntryDataSource.cs index e2b1dd47b..36179000c 100644 --- a/AgileMapper/DataSources/DictionaryEntryDataSource.cs +++ b/AgileMapper/DataSources/DictionaryEntryDataSource.cs @@ -53,7 +53,7 @@ private static Expression GetValidEntryExistsTest(DictionaryEntryVariablePair di public override Expression AddSourceCondition(Expression value) { - var preCondition = _preCondition ?? (_preCondition = CreatePreCondition()); + var preCondition = _preCondition ??= CreatePreCondition(); return value.ToIfFalseDefaultCondition(preCondition); } @@ -72,9 +72,15 @@ private Expression CreatePreCondition() return Expression.Block(keyAssignment, matchingKeyExists); } - public override Expression FinalisePopulationBranch(Expression alternatePopulation, IMemberMapperData mapperData) + public override Expression FinalisePopulationBranch( + Expression alternatePopulation, + IDataSource nextDataSource, + IMemberMapperData mapperData) { - var population = base.FinalisePopulationBranch(alternatePopulation, mapperData); + var population = base.FinalisePopulationBranch( + alternatePopulation, + nextDataSource, + mapperData); var matchingKeyExists = GetMatchingKeyExistsTest(); var ifKeyExistsPopulate = Expression.IfThen(matchingKeyExists, population); diff --git a/AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs b/AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs index 1ddc4815d..5160c0adb 100644 --- a/AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs +++ b/AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs @@ -12,11 +12,11 @@ public static IEnumerable Create(DataSourceFindContext context) yield break; } - foreach (var configuredDataSource in context.ConfiguredDataSources) + foreach (var dataSource in context.ConfiguredDataSources) { - yield return context.GetFinalDataSource(configuredDataSource); + yield return context.GetFinalDataSource(dataSource); - if (!configuredDataSource.IsConditional) + if (!dataSource.IsConditional) { yield break; } diff --git a/AgileMapper/DataSources/Factories/MemberDataSourceSetFactory.cs b/AgileMapper/DataSources/Factories/MemberDataSourceSetFactory.cs index 77fb5102a..01faec7d6 100644 --- a/AgileMapper/DataSources/Factories/MemberDataSourceSetFactory.cs +++ b/AgileMapper/DataSources/Factories/MemberDataSourceSetFactory.cs @@ -18,7 +18,10 @@ public static IDataSourceSet CreateFor(DataSourceFindContext findContext) { var validDataSources = EnumerateDataSources(findContext).ToArray(); - return DataSourceSet.For(validDataSources, findContext); + return DataSourceSet.For( + validDataSources, + findContext, + ValueExpressionBuilders.ValueTree); } private static IEnumerable EnumerateDataSources(DataSourceFindContext context) diff --git a/AgileMapper/DataSources/IDataSource.cs b/AgileMapper/DataSources/IDataSource.cs index 31fc976cd..e4470229f 100644 --- a/AgileMapper/DataSources/IDataSource.cs +++ b/AgileMapper/DataSources/IDataSource.cs @@ -18,6 +18,8 @@ internal interface IDataSource bool IsConditional { get; } + bool IsSequential { get; } + bool IsFallback { get; } IList Variables { get; } @@ -28,6 +30,9 @@ internal interface IDataSource Expression AddSourceCondition(Expression value); - Expression FinalisePopulationBranch(Expression alternatePopulation, IMemberMapperData mapperData); + Expression FinalisePopulationBranch( + Expression alternatePopulation, + IDataSource nextDataSource, + IMemberMapperData mapperData); } } diff --git a/AgileMapper/DataSources/ValueExpressionBuilders.cs b/AgileMapper/DataSources/ValueExpressionBuilders.cs index b0e55db9d..c8a834f11 100644 --- a/AgileMapper/DataSources/ValueExpressionBuilders.cs +++ b/AgileMapper/DataSources/ValueExpressionBuilders.cs @@ -11,22 +11,26 @@ namespace AgileObjects.AgileMapper.DataSources internal static class ValueExpressionBuilders { + public static Expression SingleDataSource(IList dataSources, IMemberMapperData mapperData) + => SingleDataSource(dataSources.First(), mapperData); + public static Expression SingleDataSource(IDataSource dataSource, IMemberMapperData mapperData) { var value = dataSource.IsConditional - ? dataSource.Value.ToIfFalseDefaultCondition(dataSource.Condition) + ? dataSource.Value.ToIfFalseDefaultCondition(dataSource.Condition, mapperData) : dataSource.Value; return dataSource.AddSourceCondition(value); } - public static Expression ConditionTree(IList dataSources, IMemberMapperData mapperData) + public static Expression ValueTree(IList dataSources, IMemberMapperData mapperData) { var value = SingleDataSource(dataSources.Last(), mapperData); for (var i = dataSources.Count - 2; i >= 0;) { - var dataSource = dataSources[i--]; + var dataSource = dataSources[i]; + --i; var dataSourceValue = dataSource.IsConditional ? Expression.Condition( diff --git a/AgileMapper/Extensions/Internal/EnumerableExtensions.cs b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs index b0fc08b98..88a4a030a 100644 --- a/AgileMapper/Extensions/Internal/EnumerableExtensions.cs +++ b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs @@ -180,7 +180,7 @@ public static TResult[] ProjectToArray( public static T[] EnlargeToArray(this IList items, int newCapacity) { var enlargedArray = new T[newCapacity]; - + enlargedArray.CopyFrom(items); return enlargedArray; @@ -188,16 +188,26 @@ public static T[] EnlargeToArray(this IList items, int newCapacity) public static T[] CopyToArray(this IList items) { - if (items.Count == 0) + var itemCount = items.Count; + + switch (itemCount) { - return Enumerable.EmptyArray; - } + case 0: + return Enumerable.EmptyArray; + + case 1: + return new[] { items[0] }; - var clonedArray = new T[items.Count]; + case 2: + return new[] { items[0], items[1] }; + + default: + var clonedArray = new T[itemCount]; - clonedArray.CopyFrom(items); + clonedArray.CopyFrom(items); - return clonedArray; + return clonedArray; + } } public static Expression Chain( @@ -336,7 +346,14 @@ public static void Insert(this IList items, T item, int insertionOffset) public static T[] Append(this IList array, T extraItem) { - switch (array.Count) + if (array == null) + { + return new[] { extraItem }; + } + + var itemsCount = array.Count; + + switch (itemsCount) { case 0: return new[] { extraItem }; @@ -348,11 +365,11 @@ public static T[] Append(this IList array, T extraItem) return new[] { array[0], array[1], extraItem }; default: - var newArray = new T[array.Count + 1]; + var newArray = new T[itemsCount + 1]; newArray.CopyFrom(array); - newArray[array.Count] = extraItem; + newArray[itemsCount] = extraItem; return newArray; } diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs index e6ffcd772..854b167c2 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs @@ -17,25 +17,6 @@ internal static partial class ExpressionExtensions { - public static Expression ReplaceParametersWith(this LambdaExpression lambda, params Expression[] replacements) - { - if (lambda.Parameters.HasOne()) - { - return lambda.ReplaceParameterWith(replacements.First()); - } - - var parameterCount = lambda.Parameters.Count; - - var replacementsByParameter = FixedSizeExpressionReplacementDictionary.WithEqualKeys(parameterCount); - - for (var i = 0; i < parameterCount; ++i) - { - replacementsByParameter.Add(lambda.Parameters[i], replacements[i]); - } - - return lambda.Body.Replace(replacementsByParameter); - } - public static Expression ReplaceParameterWith(this LambdaExpression lambda, Expression replacement) => ReplaceParameter(lambda.Body, lambda.Parameters[0], replacement); @@ -185,10 +166,12 @@ private Expression ReplaceIn(Expression expression) case Add: case And: case AndAlso: + case ArrayIndex: case Assign: case Coalesce: case Divide: case Equal: + case ExclusiveOr: case GreaterThan: case GreaterThanOrEqual: case LessThan: @@ -198,6 +181,7 @@ private Expression ReplaceIn(Expression expression) case NotEqual: case Or: case OrElse: + case Power: case Subtract: return ReplaceIn((BinaryExpression)expression); @@ -210,12 +194,15 @@ private Expression ReplaceIn(Expression expression) case Conditional: return ReplaceIn((ConditionalExpression)expression); + case ArrayLength: case ExpressionType.Convert: case IsFalse: case IsTrue: + case ExpressionType.Negate: case Not: case Throw: case TypeAs: + case Unbox: return ReplaceIn((UnaryExpression)expression); case Goto: @@ -248,6 +235,7 @@ private Expression ReplaceIn(Expression expression) case New: return ReplaceIn((NewExpression)expression); + case NewArrayBounds: case NewArrayInit: return ReplaceIn((NewArrayExpression)expression); diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 6295acb2c..e9a5ff074 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -61,8 +61,16 @@ public static ConstantExpression ToConstantExpression(this TItem item, Ty public static DefaultExpression ToDefaultExpression(this Type type) => Expression.Default(type); [DebuggerStepThrough] - public static ConditionalExpression ToIfFalseDefaultCondition(this Expression value, Expression condition) - => Expression.Condition(condition, value, value.Type.ToDefaultExpression()); + public static ConditionalExpression ToIfFalseDefaultCondition( + this Expression value, + Expression condition, + Expression defaultValue = null) + { + return Expression.Condition( + condition, + value, + defaultValue ?? value.Type.ToDefaultExpression()); + } public static Expression AndTogether(this IList expressions) { diff --git a/AgileMapper/Members/ConfiguredSourceMember.cs b/AgileMapper/Members/ConfiguredSourceMember.cs index 061b6d4a8..3fe00e30f 100644 --- a/AgileMapper/Members/ConfiguredSourceMember.cs +++ b/AgileMapper/Members/ConfiguredSourceMember.cs @@ -39,7 +39,7 @@ public ConfiguredSourceMember(Expression value, IQualifiedMemberContext context) } private static Member[] GetConfiguredMemberChainOrNull(Expression value, IMapperContextOwner contextOwner) - => value.ToSourceMember(contextOwner.MapperContext, nt => { })?.MemberChain; + => value.ToSourceMemberOrNull(contextOwner.MapperContext)?.MemberChain; private ConfiguredSourceMember(ConfiguredSourceMember parent, Member childMember) : this( diff --git a/AgileMapper/Members/MemberExtensionMethods.cs b/AgileMapper/Members/MemberExtensionMethods.cs index 369ed5f40..47adfb881 100644 --- a/AgileMapper/Members/MemberExtensionMethods.cs +++ b/AgileMapper/Members/MemberExtensionMethods.cs @@ -186,7 +186,7 @@ public static bool CouldMatch(this IList memberNames, IList othe .Any(otherJoinedName, (ojn, joinedName) => (joinedName == RootMemberName) || ojn.StartsWithIgnoreCase(joinedName))); } - public static bool Match(this ICollection memberNames, ICollection otherMemberNames) + public static bool Match(this IList memberNames, IList otherMemberNames) { if (!memberNames.HasOne()) { @@ -199,7 +199,7 @@ public static bool Match(this ICollection memberNames, ICollection otherMemberName.EqualsIgnoreCase(memberName)); + : otherMemberNames.Any(memberName, (mn, omn) => omn.EqualsIgnoreCase(mn)); } public static TMember GetElementMember(this TMember enumerableMember) @@ -276,21 +276,11 @@ public static Expression GetPopulation(this Member targetMember, Expression inst return population; } -#if NET35 - public static QualifiedMember ToSourceMember(this LinqExp.Expression memberAccess, MapperContext mapperContext) - => memberAccess.ToDlrExpression().ToSourceMember(mapperContext); -#endif - public static QualifiedMember ToSourceMember( + public static QualifiedMember ToSourceMemberOrNull( this Expression memberAccess, - MapperContext mapperContext, - Action nonMemberAction = null) + MapperContext mapperContext) { - return CreateMember( - memberAccess, - RootSource, - GlobalContext.Instance.MemberCache.GetSourceMembers, - nonMemberAction ?? ThrowIfUnsupported, - mapperContext); + return memberAccess.ToSourceMember(mapperContext, nt => { }); } public static QualifiedMember ToSourceMemberOrNull( @@ -317,6 +307,34 @@ public static QualifiedMember ToSourceMemberOrNull( return sourceMember; } +#if NET35 + public static QualifiedMember ToSourceMember(this LinqExp.Expression memberAccess, MapperContext mapperContext) + => memberAccess.ToDlrExpression().ToSourceMember(mapperContext); +#endif + public static QualifiedMember ToSourceMember( + this Expression memberAccess, + MapperContext mapperContext, + Action nonMemberAction = null) + { + return CreateMember( + memberAccess, + RootSource, + GlobalContext.Instance.MemberCache.GetSourceMembers, + nonMemberAction ?? ThrowIfUnsupported, + mapperContext); + } + +#if NET35 + public static QualifiedMember ToTargetMemberOrNull(this LinqExp.LambdaExpression memberAccess, MapperContext mapperContext) + => memberAccess.ToDlrExpression().ToTargetMemberOrNull(mapperContext); +#endif + public static QualifiedMember ToTargetMemberOrNull( + this LambdaExpression memberAccess, + MapperContext mapperContext) + { + return memberAccess.ToTargetMember(mapperContext, nt => { }); + } + public static QualifiedMember ToTargetMemberOrNull( this LambdaExpression memberAccess, Type targetType, diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index cf91148cf..ee1cb888f 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -141,6 +141,14 @@ public static Expression GetTargetMemberAccess(this IMemberMapperData mapperData public static Expression GetTargetMemberDefault(this IQualifiedMemberContext context) => context.TargetMember.Type.ToDefaultExpression(); + public static ConditionalExpression ToIfFalseDefaultCondition( + this Expression value, + Expression condition, + IMemberMapperData mapperData) + { + return value.ToIfFalseDefaultCondition(condition, mapperData.GetTargetFallbackValue()); + } + public static Expression GetTargetFallbackValue(this IMemberMapperData mapperData) => mapperData.RuleSet.FallbackDataSourceFactory.Invoke(mapperData).Value; diff --git a/AgileMapper/Members/Population/MemberPopulator.cs b/AgileMapper/Members/Population/MemberPopulator.cs index 72189a2cd..47134673f 100644 --- a/AgileMapper/Members/Population/MemberPopulator.cs +++ b/AgileMapper/Members/Population/MemberPopulator.cs @@ -64,7 +64,7 @@ private Expression GetBinding(Expression populationGuard) if (MapperData.RuleSet.Settings.AllowGuardedBindings && (populationGuard != null)) { - bindingValue = bindingValue.ToIfFalseDefaultCondition(populationGuard); + bindingValue = bindingValue.ToIfFalseDefaultCondition(populationGuard, MapperData); } return MapperData.GetTargetMemberPopulation(bindingValue); @@ -78,28 +78,43 @@ private Expression GetReadOnlyMemberPopulation() return Expression.IfThen(targetMemberNotNull, _dataSources.BuildValue()); } - private Expression GetPopulationExpression() + public Expression GetPopulationExpression() { - if (_dataSources.Count == 1) + var dataSourcesCount = _dataSources.Count; + + if (dataSourcesCount == 1) { return GetPopulation(_dataSources[0]); } var population = default(Expression); + var nextDataSource = default(IDataSource); for (var i = _dataSources.Count; ;) { - population = GetPopulation(_dataSources[--i], population); + --i; + var dataSource = _dataSources[i]; + population = GetPopulation(dataSource, population, nextDataSource); if (i == 0) { return population; } + + nextDataSource = dataSource; } } - private Expression GetPopulation(IDataSource dataSource, Expression population = null) - => dataSource.FinalisePopulationBranch(population, MapperData); + private Expression GetPopulation( + IDataSource dataSource, + Expression populationSoFar = null, + IDataSource nextDataSource = null) + { + return dataSource.FinalisePopulationBranch( + populationSoFar, + nextDataSource, + MapperData); + } private Expression GetPopulationWithVariables(Expression population) { diff --git a/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs b/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs index 82989d714..b14dd3757 100644 --- a/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs +++ b/AgileMapper/ObjectPopulation/ConfiguredMappingFactory.cs @@ -60,6 +60,7 @@ private static IDataSource GetMappingFactoryDataSource( mapperData.SourceMember, condition, returnValue, + factory.ConfigInfo.IsSequentialConfiguration, mapperData); } } diff --git a/Directory.Build.props b/Directory.Build.props index 5a56b9539..4eb7d43f1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ true git https://github.com/AgileObjects/AgileMapper - 1.7.0 + 1.7.0-preview1 1.7.0.0 1.7.0.0 diff --git a/docs/src/configuration/Member-Values.md b/docs/src/configuration/Member-Values.md index 2bee4c09f..e64ade405 100644 --- a/docs/src/configuration/Member-Values.md +++ b/docs/src/configuration/Member-Values.md @@ -64,6 +64,21 @@ Mapper.Map(productDto).ToANew(cfg => cfg .To(p => p.CompanyName)); // p is the Product ``` +**A sequence of data sources** + +```cs +// Map the Customer Home Address, Work Address +// then Address History to the CustomerViewModel +// AllAddresses property: +Mapper.WhenMapping + .From() // Apply to Customer mappings + .ToANew() // Apply to CustomerViewModel creation + .Map((c, vm) => new[] { c.HomeAddress }) + .Then.Map((c, vm) => new[] { c.WorkAddress }) + .Then.Map((c, vm) => c.AddressHistory) + .To(vm => vm.AllAddresses); // vm is the CustomerViewModel +``` + ### Making Data Sources Conditional: Any of these methods can be configured to be conditional: @@ -77,6 +92,18 @@ Mapper.WhenMapping .To(p => p.CompanyName); // p is the Product ``` +```cs +// Only include WorkAddress if it's different to HomeAddress: +Mapper.WhenMapping + .From() // Apply to Customer mappings + .ToANew() // Apply to CustomerViewModel creation + .Map((c, vm) => new[] { c.HomeAddress }) + .Then.If((c, vm) => c.WorkAddress != c.HomeAddress ) + .Map((c, vm) => new[] { c.WorkAddress }) + .Then.Map((c, vm) => c.AddressHistory) + .To(vm => vm.AllAddresses); // vm is the CustomerViewModel +``` + And in an [inline](/configuration/Inline) example: ```cs @@ -162,10 +189,10 @@ class VideoDto } Mapper.WhenMapping - .From