diff --git a/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj b/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj index f0c622748..cf7abc181 100644 --- a/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj +++ b/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj @@ -37,8 +37,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.0.0\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net40\AgileObjects.ReadableExpressions.dll ..\packages\AutoMapper.7.0.1\lib\net45\AutoMapper.dll diff --git a/AgileMapper.PerformanceTester.Net45/packages.config b/AgileMapper.PerformanceTester.Net45/packages.config index da651426a..92d57fe9a 100644 --- a/AgileMapper.PerformanceTester.Net45/packages.config +++ b/AgileMapper.PerformanceTester.Net45/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.Common/ShouldExtensions.cs b/AgileMapper.UnitTests.Common/ShouldExtensions.cs index 59b3843fc..4fd39e6de 100644 --- a/AgileMapper.UnitTests.Common/ShouldExtensions.cs +++ b/AgileMapper.UnitTests.Common/ShouldExtensions.cs @@ -400,7 +400,7 @@ public static IDictionary ShouldContainKeyAndValue( Asplode("Dictionary with key " + expectedKey, "No contained key"); } - value.ShouldBeSameAs(expectedValue); + value.ShouldBe(expectedValue); return dictionary; } diff --git a/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj b/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj index fde472b1b..0e0c4f28a 100644 --- a/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj +++ b/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj @@ -37,8 +37,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net35\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.0.0\lib\net35\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net35\AgileObjects.ReadableExpressions.dll ..\packages\DynamicLanguageRuntime.1.1.2\lib\Net35\Microsoft.Dynamic.dll diff --git a/AgileMapper.UnitTests.Net35/packages.config b/AgileMapper.UnitTests.Net35/packages.config index bd262d035..e4f4503b6 100644 --- a/AgileMapper.UnitTests.Net35/packages.config +++ b/AgileMapper.UnitTests.Net35/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj index 85845179b..1fc3c9e92 100644 --- a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj +++ b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj @@ -42,8 +42,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.0.0\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net40\AgileObjects.ReadableExpressions.dll @@ -75,6 +75,7 @@ VersionInfo.cs + diff --git a/AgileMapper.UnitTests.NonParallel/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs b/AgileMapper.UnitTests.NonParallel/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs new file mode 100644 index 000000000..ab9d772d0 --- /dev/null +++ b/AgileMapper.UnitTests.NonParallel/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs @@ -0,0 +1,26 @@ +namespace AgileObjects.AgileMapper.UnitTests.NonParallel.Configuration.Inline +{ + using Common; + using MoreTestClasses; + using NetStandardPolyfills; + using TestClasses; + using Xunit; + + public class WhenConfiguringDerivedTypesInline : NonParallelTestsBase + { + [Fact] + public void ShouldScanConfiguredAssembliesInline() + { + TestThenReset(() => + { + var result = Mapper + .Map(new { NumberOfLegs = 100, SlitherNoise = "ththtth" }) + .Over(new Earthworm() as AnimalBase, cgf => cgf + .LookForDerivedTypesIn(typeof(Dog).GetAssembly(), typeof(Earthworm).GetAssembly())); + + result.NumberOfLegs.ShouldBe(100); + ((Earthworm)result).SlitherNoise.ShouldBe("ththtth"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.NonParallel/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests.NonParallel/Configuration/WhenConfiguringDerivedTypes.cs index e44c49977..0f40f4428 100644 --- a/AgileMapper.UnitTests.NonParallel/Configuration/WhenConfiguringDerivedTypes.cs +++ b/AgileMapper.UnitTests.NonParallel/Configuration/WhenConfiguringDerivedTypes.cs @@ -2,13 +2,14 @@ { using Common; using MoreTestClasses; + using NetStandardPolyfills; using TestClasses; using Xunit; public class WhenConfiguringDerivedTypes : NonParallelTestsBase { [Fact] - public void ShouldScanConfiguredAssemblies() + public void ShouldScanConfiguredAssembliesViaTheStaticApi() { TestThenReset(() => { @@ -30,5 +31,57 @@ public void ShouldScanConfiguredAssemblies() ((Earthworm)wormResult).SlitherNoise.ShouldBe("sssSSS"); }); } + + [Fact] + public void ShouldScanConfiguredAssembliesViaTheInstanceApi() + { + TestThenReset(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .LookForDerivedTypesIn(typeof(Dog).GetAssembly(), typeof(Earthworm).GetAssembly()); + + var result = mapper + .Map(new { NumberOfLegs = 1000, SlitherNoise = "thththtth" }) + .Over(new Earthworm() as AnimalBase); + + result.NumberOfLegs.ShouldBe(1000); + ((Earthworm)result).SlitherNoise.ShouldBe("thththtth"); + } + }); + } + + [Fact] + public void ShouldSetAssembliesToScanGlobally() + { + TestThenReset(() => + { + using (var mapper1 = Mapper.CreateNew()) + using (var mapper2 = Mapper.CreateNew()) + { + // Set assembly scanning on mapper1... + mapper1.WhenMapping + .LookForDerivedTypesIn(typeof(Dog).GetAssembly(), typeof(Earthworm).GetAssembly()); + + // ...use mapper2 to cache the assembly scan results... + var result1 = mapper2 + .Map(new { NumberOfLegs = 4, WoofSound = "AWESOME" }) + .OnTo(new Dog() as AnimalBase); + + result1.NumberOfLegs.ShouldBe(4); + ((Dog)result1).WoofSound.ShouldBe("AWESOME"); + + // ...use mapper1 with a type outside the base type's assembly; + // assemblies are cached globally, so the scan settings should be too: + var result2 = mapper1 + .Map(new { NumberOfLegs = 100, SlitherNoise = "SLITHERRR" }) + .OnTo(new Earthworm() as AnimalBase); + + result2.NumberOfLegs.ShouldBe(100); + ((Earthworm)result2).SlitherNoise.ShouldBe("SLITHERRR"); + } + }); + } } } diff --git a/AgileMapper.UnitTests.NonParallel/packages.config b/AgileMapper.UnitTests.NonParallel/packages.config index 68f01a437..574dc374e 100644 --- a/AgileMapper.UnitTests.NonParallel/packages.config +++ b/AgileMapper.UnitTests.NonParallel/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index ef2f96729..9a22ca05a 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -44,8 +44,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.0.0\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net40\AgileObjects.ReadableExpressions.dll ..\packages\Microsoft.Extensions.Primitives.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs index 18d12b785..b19c35e01 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInlineIncorrectly.cs @@ -28,7 +28,7 @@ public void ShouldErrorIfUnconvertibleConstantSpecifiedInline() } }); - inlineConfigEx.Message.ShouldContain("Unable to convert configured decimal? "); + inlineConfigEx.Message.ShouldContain("Unable to convert configured 'decimal?' "); } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs index 7dcd33e42..405697830 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs @@ -2,8 +2,6 @@ { using System.Linq; using Common; - using MoreTestClasses; - using NetStandardPolyfills; using TestClasses; #if !NET35 using Xunit; @@ -112,20 +110,5 @@ public void ShouldMapACustomTypePairInACollectionInline() mapper.InlineContexts().Count.ShouldBe(2); } } - - [Fact] - public void ShouldScanConfiguredAssembliesInline() - { - using (var mapper = Mapper.CreateNew()) - { - var result = mapper - .Map(new { NumberOfLegs = 100, SlitherNoise = "ththtth" }) - .Over(new Earthworm() as AnimalBase, cgf => cgf - .LookForDerivedTypesIn(typeof(Dog).GetAssembly(), typeof(Earthworm).GetAssembly())); - - result.NumberOfLegs.ShouldBe(100); - ((Earthworm)result).SlitherNoise.ShouldBe("ththtth"); - } - } } } diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs index 14df549cb..574987103 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs @@ -148,6 +148,92 @@ public void ShouldApplyMultipleConfiguredMembersBySourceType() } } + // See https://github.com/agileobjects/AgileMapper/issues/111 + [Fact] + public void ShouldConditionallyApplyAToTargetConfiguredSimpleTypeConstant() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().ToANew() + .If(ctx => string.IsNullOrEmpty(ctx.Source)) + .Map(default(string)).ToTarget(); + + var source = new Address { Line1 = "Here", Line2 = string.Empty }; + var result = mapper.Map(source).ToANew
(); + + result.Line1.ShouldBe("Here"); + result.Line2.ShouldBeNull(); + } + } + + [Fact] + public void ShouldApplyAToTargetConfiguredSimpleTypeConstant() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().ToANew() + .Map((s, t) => string.IsNullOrEmpty(s) ? null : s).ToTarget(); + + var source = new Address { Line1 = "There", Line2 = string.Empty }; + var result = mapper.Map(source).ToANew
(); + + result.Line1.ShouldBe("There"); + result.Line2.ShouldBeNull(); + } + } + + [Fact] + public void ShouldConditionallyApplyAToTargetConfiguredNestedSimpleTypeExpression() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().ToANew() + .If(ctx => ctx.Source % 2 == 0) + .Map(ctx => ctx.Source * 2).ToTarget(); + + var nonMatchingSource = new { ValueValue = 3 }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>>(); + + nonMatchingResult.Value.ShouldNotBeNull(); + nonMatchingResult.Value.Value.ShouldBe(3); + + var matchingSource = new { ValueValue = 4 }; + var matchingResult = mapper.Map(matchingSource).ToANew>>(); + + matchingResult.Value.ShouldNotBeNull(); + matchingResult.Value.Value.ShouldBe(8); + } + } + + [Fact] + public void ShouldConditionallyApplyAToTargetConfiguredSimpleTypeExpressionInAComplexTypeList() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().ToANew() + .If((s, t) => s % 2 == 0) + .Map(ctx => ctx.Source * 2).ToTarget(); + + var source = new PublicField>> + { + Value = new List> + { + new PublicField { Value = 1 }, + new PublicField { Value = 2 }, + new PublicField { Value = 3 } + } + }; + var result = mapper.Map(source).ToANew>>>(); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldBe(pf => pf.Value, 1, 4, 3); + } + } + [Fact] public void ShouldConditionallyApplyAConfiguredMember() { @@ -740,6 +826,62 @@ public void ShouldApplyAConfiguredComplexTypeEnumerableConditionally() } } + // See https://github.com/agileobjects/AgileMapper/issues/113 + [Fact] + public void ShouldApplyAConfiguredComplexToSimpleTypeEnumerableProjection() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From[]>>() + .To>() + .Map( + pfpfi => pfpfi.Value.Select(v => v.Value), + pfi => pfi.Value); + + var source = new PublicField[]> + { + Value = new[] + { + new PublicField { Value = 1 }, + new PublicField { Value = 2 }, + new PublicField { Value = 3 } + } + }; + + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldNotBeEmpty(); + result.Value.ShouldBe(1, 2, 3); + } + } + + // See https://github.com/agileobjects/AgileMapper/issues/113 + [Fact] + public void ShouldApplyAConfiguredComplexToSimpleTypeEnumerableProjectionToTheRootTarget() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From[]>() + .To() + .Map(ctx => ctx.Source.Select(v => v.Value)) + .ToTarget(); + + var source = new[] + { + new PublicField { Value = 1 }, + new PublicField { Value = 2 }, + new PublicField { Value = 3 } + }; + + var result = mapper.Map(source).ToANew(); + + result.ShouldNotBeEmpty(); + result.ShouldBe(1, 2, 3); + } + } + [Fact] public void ShouldApplyAConfiguredSourceAndTargetFunction() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs index 2eddf7b99..4cb2ce33a 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs @@ -300,6 +300,24 @@ public void ShouldErrorIfSimpleTypeConfiguredForEnumerableTarget() "PublicField.Value of type 'int' cannot be mapped to target type 'int[]'"); } + [Fact] + public void ShouldErrorIfUnconvertibleEnumerableElementTypeConfigured() + { + var configurationException = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From[]>>() + .To>() + .Map(s => s.Value, t => t.Value); + } + }); + + configurationException.Message.ShouldContain( + "Unable to convert configured 'PublicField' to target type 'int'"); + } + [Fact] public void ShouldErrorIfTargetParameterConfiguredAsTarget() { @@ -319,7 +337,7 @@ public void ShouldErrorIfTargetParameterConfiguredAsTarget() } [Fact] - public void ShouldErrorIfRootTargetSimpleTypeConstantDataSourceConfigured() + public void ShouldErrorIfSimpleTypeConstantConfiguredForRootTarget() { var configurationException = Should.Throw(() => { @@ -338,7 +356,7 @@ public void ShouldErrorIfRootTargetSimpleTypeConstantDataSourceConfigured() } [Fact] - public void ShouldErrorIfRootTargetSimpleTypeMemberDataSourceConfigured() + public void ShouldErrorIfSimpleTypeMemberConfiguredForRootTarget() { var configurationException = Should.Throw(() => { @@ -357,7 +375,7 @@ public void ShouldErrorIfRootTargetSimpleTypeMemberDataSourceConfigured() } [Fact] - public void ShouldErrorIfRootEnumerableTargetNonEnumerableTypeMemberDataSourceConfigured() + public void ShouldErrorIfNonEnumerableTypeMemberConfiguredForRootEnumerableTarget() { var configurationException = Should.Throw(() => { @@ -376,7 +394,7 @@ public void ShouldErrorIfRootEnumerableTargetNonEnumerableTypeMemberDataSourceCo } [Fact] - public void ShouldErrorIfRootNonEnumerableTargetEnumerableTypeMemberDataSourceConfigured() + public void ShouldErrorIfEnumerableTypeMemberConfiguredForRootNonEnumerableTarget() { var configurationException = Should.Throw(() => { @@ -394,6 +412,25 @@ public void ShouldErrorIfRootNonEnumerableTargetEnumerableTypeMemberDataSourceCo configurationException.Message.ShouldContain("cannot be mapped to non-enumerable target type 'PublicProperty'"); } + [Fact] + public void ShouldErrorIfUnconvertibleEnumerableElementTypeConfiguredForRootTarget() + { + var configurationException = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From[]>>() + .To() + .Map(ctx => ctx.Source.Value) + .ToTarget(); + } + }); + + configurationException.Message.ShouldContain( + "Unable to convert configured 'PublicField' to target type 'decimal'"); + } + [Fact] public void ShouldErrorIfConstantSpecifiedForTargetMember() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDerivedTypes.cs index 16bf2ebee..134745b89 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDerivedTypes.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDerivedTypes.cs @@ -2,8 +2,6 @@ { using AgileMapper.Extensions.Internal; using Common; - using MoreTestClasses; - using NetStandardPolyfills; using TestClasses; #if !NET35 using Xunit; @@ -14,52 +12,6 @@ #endif public class WhenConfiguringDerivedTypes { - [Fact] - public void ShouldScanConfiguredAssemblies() - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .LookForDerivedTypesIn(typeof(Dog).GetAssembly(), typeof(Earthworm).GetAssembly()); - - var result = mapper - .Map(new { NumberOfLegs = 1000, SlitherNoise = "thththtth" }) - .Over(new Earthworm() as AnimalBase); - - result.NumberOfLegs.ShouldBe(1000); - ((Earthworm)result).SlitherNoise.ShouldBe("thththtth"); - } - } - - [Fact] - public void ShouldSetAssembliesToScanGlobally() - { - using (var mapper1 = Mapper.CreateNew()) - using (var mapper2 = Mapper.CreateNew()) - { - // Set assembly scanning on mapper1... - mapper1.WhenMapping - .LookForDerivedTypesIn(typeof(Dog).GetAssembly(), typeof(Earthworm).GetAssembly()); - - // ...use mapper2 to cache the assembly scan results... - var result1 = mapper2 - .Map(new { NumberOfLegs = 4, WoofSound = "AWESOME" }) - .OnTo(new Dog() as AnimalBase); - - result1.NumberOfLegs.ShouldBe(4); - ((Dog)result1).WoofSound.ShouldBe("AWESOME"); - - // ...use mapper1 with a type outside the base type's assembly; - // assemblies are cached globally, so the scan settings should be too: - var result2 = mapper1 - .Map(new { NumberOfLegs = 100, SlitherNoise = "SLITHERRR" }) - .OnTo(new Earthworm() as AnimalBase); - - result2.NumberOfLegs.ShouldBe(100); - ((Earthworm)result2).SlitherNoise.ShouldBe("SLITHERRR"); - } - } - [Fact] public void ShouldMapACustomTypePair() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs index a6a1aefe7..8b88d5ee6 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs @@ -526,6 +526,24 @@ public void ShouldErrorIfConflictingFactoryConfigured() factoryEx.Message.ShouldContain("has already been configured"); } + // See https://github.com/agileobjects/AgileMapper/issues/114 + [Fact] + public void ShouldErrorIfPrimitiveTargetTypeSpecified() + { + var factoryEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To() + .CreateInstancesUsing(ctx => 123); + } + }); + + factoryEx.Message.ShouldContain("primitive type 'int'"); + } + #region Helper Classes private class CustomerCtor : Person diff --git a/AgileMapper.UnitTests/Configuration/WhenMappingToNull.cs b/AgileMapper.UnitTests/Configuration/WhenMappingToNull.cs index 2a8864e33..87087d4ff 100644 --- a/AgileMapper.UnitTests/Configuration/WhenMappingToNull.cs +++ b/AgileMapper.UnitTests/Configuration/WhenMappingToNull.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Linq; using AgileMapper.Configuration; using AgileMapper.Extensions.Internal; @@ -186,6 +187,43 @@ public void ShouldMapCollectionElementNestedPropertiesToNull() } } + [Fact] + public void ShouldMapCollectionsToNullIfConfiguredGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .MapNullCollectionsToNull(); + + var source = new PublicField> { Value = null }; + var result = mapper.Map(source).ToANew>>(); + + result.Value.ShouldBeNull(); + } + } + + [Fact] + public void ShouldMapCollectionsToNullIfConfiguredByType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>>() + .To>>() + .MapNullCollectionsToNull(); + + var matchingSource = new PublicProperty> { Value = null }; + var matchingResult = mapper.Map(matchingSource).ToANew>>(); + + matchingResult.Value.ShouldBeNull(); + + var nonMatchingSource = new PublicProperty> { Value = null }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>>(); + + nonMatchingResult.Value.ShouldBeEmpty(); + } + } + [Fact] public void ShouldErrorIfConditionsAreConfiguredForTheSameType() { diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs index 78e8b9664..591d2ce36 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using AgileMapper.Extensions; using Common; using TestClasses; #if !NET35 @@ -105,6 +106,7 @@ public void ShouldMapANestedEnumerableOfArraysToANestedEnumerableTypedDictionary Value = new[] { new [] { 1, 2, 3 }, + null, new [] { 4, 5, 6 } } }; @@ -113,7 +115,8 @@ public void ShouldMapANestedEnumerableOfArraysToANestedEnumerableTypedDictionary result.Value.ShouldNotContainKey("[0][0]"); result.Value["[0]"].ShouldBe("1", "2", "3"); - result.Value["[1]"].ShouldBe("4", "5", "6"); + result.Value["[1]"].ShouldBeDefault(); + result.Value["[2]"].ShouldBe("4", "5", "6"); } // See https://github.com/agileobjects/AgileMapper/issues/8 @@ -160,6 +163,34 @@ public void ShouldMapADictionaryMemberToANewDictionaryMember() ((CustomerViewModel)result.Value["Object"]).Name.ShouldBe("Mr Yo Yo"); } + // See https://github.com/agileobjects/AgileMapper/issues/110 + [Fact] + public void ShouldMapSimpleTypeObjectValuesToSimpleTypeObjectValues() + { + var source = new PublicField> + { + Value = new Dictionary + { + { "int", 1 }, + { "double", 1.0 }, + { "decimal", 1m }, + { "string", "hello" }, + { "bool", true } + } + }; + + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldNotBeSameAs(source.Value); + result.Value.Count.ShouldBe(source.Value.Count); + result.Value.ShouldContainKeyAndValue("int", 1); + result.Value.ShouldContainKeyAndValue("double", 1.0); + result.Value.ShouldContainKeyAndValue("decimal", 1m); + result.Value.ShouldContainKeyAndValue("string", "hello"); + result.Value.ShouldContainKeyAndValue("bool", true); + } + // See https://github.com/agileobjects/AgileMapper/issues/10 [Fact] public void ShouldMapADictionaryObjectValuesToNewDictionaryObjectValues() @@ -210,6 +241,40 @@ public void ShouldUseACloneConstructorToPopulateADictionaryConstructorParameter( result.Value["Test"].ShouldBe("Hello!"); } + // See https://github.com/agileobjects/AgileMapper/issues/110 + [Fact] + public void ShouldCloneSimpleTypeValuesInAnObjectDictionary() + { + var source = new PublicTwoFields> + { + Value1 = 6372, + Value2 = new Dictionary + { + ["QueryName"] = "References", + ["IsDefault"] = false, + ["QueryId"] = 155, + ["WorkspaceTypeId"] = 1, + ["IsUserDefined"] = true, + ["QueryTypeId"] = 2, + ["Test"] = default(int?) + } + }; + + var result = source.DeepClone(); + + result.Value1.ShouldBe(6372); + result.Value2.ShouldNotBeNull(); + result.Value2.ShouldNotBeSameAs(source.Value2); + result.Value2.Count.ShouldBe(source.Value2.Count); + result.Value2.ShouldContainKeyAndValue("QueryName", "References"); + result.Value2.ShouldContainKeyAndValue("IsDefault", false); + result.Value2.ShouldContainKeyAndValue("QueryId", 155); + result.Value2.ShouldContainKeyAndValue("WorkspaceTypeId", 1); + result.Value2.ShouldContainKeyAndValue("IsUserDefined", true); + result.Value2.ShouldContainKeyAndValue("QueryTypeId", 2); + result.Value2.ShouldContainKeyAndValue("Test", null); + } + [Fact] public void ShouldNotCreateDictionaryAsFallbackComplexType() { @@ -239,6 +304,7 @@ public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplemen Line2 = "That place", } }, + default(Customer), new MysteryCustomer { Id = Guid.NewGuid(), @@ -261,14 +327,14 @@ public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplemen result.Value["[0].Address.Line1"].ShouldBe("This place"); result.Value["[0].Address.Line2"].ShouldBe("That place"); - result.Value["[1].Id"].ShouldBe(source.Value.Second().Id); - result.Value["[1].Title"].ShouldBe(Title.Dr); - result.Value["[1].Name"].ShouldBe("Customer 2"); - result.Value["[1].Discount"].ShouldBe(0.3m); - result.Value["[1].Report"].ShouldBe("It was all a mystery :o"); - result.Value.ShouldNotContainKey("[1].Address"); - result.Value.ShouldNotContainKey("[1].Address.Line1"); - result.Value.ShouldNotContainKey("[1].Address.Line2"); + result.Value["[2].Id"].ShouldBe(source.Value.Third().Id); + result.Value["[2].Title"].ShouldBe(Title.Dr); + result.Value["[2].Name"].ShouldBe("Customer 2"); + result.Value["[2].Discount"].ShouldBe(0.3m); + result.Value["[2].Report"].ShouldBe("It was all a mystery :o"); + result.Value.ShouldNotContainKey("[2].Address"); + result.Value.ShouldNotContainKey("[2].Address.Line1"); + result.Value.ShouldNotContainKey("[2].Address.Line2"); } diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToInts.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToInts.cs index 0db0937bc..c287fc2e2 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToInts.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToInts.cs @@ -324,5 +324,43 @@ public void ShouldMapAStringEnumerableToAnIntEnumerable() result.ShouldBe(1, 2, 3); } + + // See https://github.com/agileobjects/AgileMapper/issues/114 + [Fact] + public void ShouldUseUserDefinedToIntOperators() + { + var source = new PublicField { Value = new UserId(123) }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(123); + } + + [Fact] + public void ShouldUseUserDefinedFromIntOperators() + { + var source = new PublicField { Value = 456 }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.Equals(new UserId(456)).ShouldBeTrue(); + } + + #region Helper Members + + private struct UserId + { + private readonly int _value; + + public UserId(int value) => _value = value; + + public static implicit operator UserId(int value) => new UserId(value); + public static explicit operator int(UserId userId) => userId._value; + + public override bool Equals(object obj) + => obj is UserId other && _value.Equals(other._value); + + public override int GetHashCode() => _value.GetHashCode(); + } + + #endregion } } diff --git a/AgileMapper.UnitTests/WhenMappingCircularReferences.cs b/AgileMapper.UnitTests/WhenMappingCircularReferences.cs index eca776380..530162687 100644 --- a/AgileMapper.UnitTests/WhenMappingCircularReferences.cs +++ b/AgileMapper.UnitTests/WhenMappingCircularReferences.cs @@ -772,6 +772,51 @@ public void ShouldNotMapAnEmptyNestedRecursiveParentMember() result.Value.Parent.ShouldBeNull(); } + [Fact] + public void ShouldPerformRepeatedComplexTypeMemberAndElementMappings() + { + var c = new Issue115.C { Id = 222 }; + + var source = new Issue115.A + { + Id = 1, + B = new Issue115.B + { + Id = 11, + Cs = new[] + { + new Issue115.C + { + Id = 111, + Parent = c + } + } + }, + C = new Issue115.C + { + Id = 12, + Parent = c + } + }; + + var result = Mapper.Map(source).ToANew(); + + result.Id.ShouldBe(1); + + result.B.ShouldNotBeNull(); + result.B.Id.ShouldBe(11); + result.B.Cs.ShouldHaveSingleItem(); + result.B.Cs[0].Parent.ShouldNotBeNull(); + result.B.Cs[0].Parent.Id.ShouldBe(222); + result.B.Cs[0].Parent.Parent.ShouldBeNull(); + + result.C.ShouldNotBeNull(); + result.C.Id.ShouldBe(12); + result.C.Parent.ShouldNotBeNull(); + result.C.Parent.Id.ShouldBe(222); + result.C.Parent.Parent.ShouldBeNull(); + } + [Fact] public void ShouldGenerateAMappingPlanForLinkRelationships() { @@ -1131,6 +1176,57 @@ internal class WarehouseProduct : EntityBase } } + public static class Issue115 + { + public class A + { + public int Id { get; set; } + + public B B { get; set; } + + public C C { get; set; } + } + + public class B + { + public int Id { get; set; } + + public C[] Cs { get; set; } + } + + public class C + { + public int Id { get; set; } + + public C Parent { get; set; } + } + + public class ADto + { + public int Id { get; set; } + + // ReSharper disable MemberHidesStaticFromOuterClass + public BDto B { get; set; } + + public CDto C { get; set; } + // ReSharper restore MemberHidesStaticFromOuterClass + } + + public class BDto + { + public int Id { get; set; } + + public CDto[] Cs { get; set; } + } + + public class CDto + { + public int Id { get; set; } + + public CDto Parent { get; set; } + } + } + #endregion } } diff --git a/AgileMapper.UnitTests/WhenMappingConcurrently.cs b/AgileMapper.UnitTests/WhenMappingConcurrently.cs index 99102c5d4..0851f5683 100644 --- a/AgileMapper.UnitTests/WhenMappingConcurrently.cs +++ b/AgileMapper.UnitTests/WhenMappingConcurrently.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests { using System; + using System.Linq; using System.Threading.Tasks; using Common; using static WhenMappingCircularReferences; @@ -54,6 +55,20 @@ public void ShouldConcurrentlyMapLargeObjectsUsingTheStaticApi() // ReSharper disable once PossibleNullReferenceException product.Warehouses.Count.ShouldBe(counts.Warehouses * counts.WarehouseProducts); }); + + var branchMapper = Mapper + .Default + .Context + .ObjectMapperFactory + .RootMappers + .FirstOrDefault(mapper => + (mapper.MapperData.SourceType == typeof(Issue77.Branch)) && + (mapper.MapperData.TargetType == typeof(Issue77.Branch))); + + branchMapper.ShouldNotBeNull(); + + // ReSharper disable once PossibleNullReferenceException + branchMapper.RepeatedMappingFuncs.Count().ShouldBe(11); } #region Helper Members diff --git a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs index f2f5b6d8b..f0ebd4534 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs @@ -497,41 +497,117 @@ public void ShouldCreateAnEmptyCollectionByDefault() result.Value.ShouldBeEmpty(); } + // See https://github.com/agileobjects/AgileMapper/issues/115 [Fact] - public void ShouldMapCollectionsToNullIfConfiguredGlobally() + public void ShouldMapNestedLists() { - using (var mapper = Mapper.CreateNew()) + var source = new Issue115.A2 { - mapper.WhenMapping - .MapNullCollectionsToNull(); + Id = 2, + BB = new Issue115.B + { + Id = 11, + CC = new List + { + new Issue115.C + { + Id = 111, + DD = new Issue115.D { Id = 111 } + } + } + }, + CC = new Issue115.C + { + Id = 111, + DD = new Issue115.D { Id = 112 } + } + }; - var source = new PublicField> { Value = null }; - var result = mapper.Map(source).ToANew>>(); + var result = Mapper.Map(source).ToANew(); + + result.Id.ShouldBe(2); + + result.BB.ShouldNotBeNull(); + result.BB.Id.ShouldBe(11); + result.BB.CC.ShouldHaveSingleItem(); + result.BB.CC[0].Id.ShouldBe(111); + result.BB.CC[0].DD.ShouldNotBeNull().Id.ShouldBe(111); + + result.CC.ShouldNotBeNull(); + result.CC.Id.ShouldBe(111); + result.CC.DD.ShouldNotBeNull().Id.ShouldBe(112); - result.Value.ShouldBeNull(); - } } - [Fact] - public void ShouldMapCollectionsToNullIfConfiguredByType() + #region Helper Members + + private static class Issue115 { - using (var mapper = Mapper.CreateNew()) + // ReSharper disable ClassNeverInstantiated.Local + // ReSharper disable InconsistentNaming + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable CollectionNeverUpdated.Local + public class A2 { - mapper.WhenMapping - .From>>() - .To>>() - .MapNullCollectionsToNull(); + public int Id { get; set; } + + public B BB { get; set; } + + public C CC { get; set; } + } + + public class B + { + public int Id { get; set; } + + public IList CC { get; set; } + } + + public class C + { + public int Id { get; set; } + + public D DD { get; set; } + } + + public class D + { + public int Id { get; set; } + } + + public class A2Dto + { + public int Id { get; set; } - var matchingSource = new PublicProperty> { Value = null }; - var matchingResult = mapper.Map(matchingSource).ToANew>>(); + public BDto BB { get; set; } - matchingResult.Value.ShouldBeNull(); + public CDto CC { get; set; } + } - var nonMatchingSource = new PublicProperty> { Value = null }; - var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>>(); + public class BDto + { + public int Id { get; set; } - nonMatchingResult.Value.ShouldBeEmpty(); + public IList CC { get; set; } } + + public class CDto + { + public int Id { get; set; } + + public DDto DD { get; set; } + } + + public class DDto + { + public int Id { get; set; } + } + // ReSharper restore CollectionNeverUpdated.Local + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore InconsistentNaming + // ReSharper restore ClassNeverInstantiated.Local } + + #endregion } } diff --git a/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs b/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs index 7b825f128..5ed6f0afb 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs @@ -152,6 +152,20 @@ public void ShouldMapToAnISet() result.ShouldBe(1L, 2L, 3L); } #endif + [Fact] + public void ShouldHandleAnUnconvertibleElementType() + { + var source = new[] + { + new PublicField { Value = 1 }, + new PublicField { Value = 2 } + }; + + var result = Mapper.Map(source).ToANew(); + + result.ShouldBeEmpty(); + } + [Fact] public void ShouldHandleANullComplexTypeElement() { @@ -169,6 +183,25 @@ public void ShouldHandleANullComplexTypeElement() result.Second().ShouldBeNull(); } + [Fact] + public void ShouldHandleANullObjectElement() + { + var source = new List + { + 123, + null, + new MegaProduct { ProductId = "Boomstick" } + }; + + var result = Mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.ShouldNotBe(source); + result.First().ShouldBe(123); + result.Second().ShouldBeNull(); + result.Third().ShouldBeOfType(); + } + [Fact] public void ShouldCreateAnEmptyListByDefault() { diff --git a/AgileMapper.UnitTests/packages.config b/AgileMapper.UnitTests/packages.config index f63a96f4c..1c42562b2 100644 --- a/AgileMapper.UnitTests/packages.config +++ b/AgileMapper.UnitTests/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper/AgileMapper.csproj b/AgileMapper/AgileMapper.csproj index 55f78ad6a..bb94f56f2 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -19,16 +19,18 @@ false AgileObjects.AgileMapper Copyright © AgileObjects Ltd 2018 - - Making entity key mapping behaviour configurable -- Support for auto-reversal of configured data sources -- Updating to use ReadableExpressions v2.0 -- Added shorthand .Map(s => s.Value, t => t.Value) API method -- Performance and memory use improvements + - Support for simple type .ToTarget() configuration +- Support for automatic use of user-defined operators +- Fixing object-to-object Dictionary mapping bug #110 +- Fixing repeated DTO mapping bug #115 +- Performance improvements +- Updating to ReadableExpressions v2.1 + 1.1.0-preview5 - + diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index 4139e37ba..7a919ae14 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -291,7 +291,7 @@ private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo par public IMappingConfigContinuation ToTarget() { - ThrowIfSimpleSource(typeof(TTarget)); + ThrowIfSimpleSourceForNonSimpleTargetMember(typeof(TTarget)); ThrowIfEnumerableSourceAndTargetMismatch(typeof(TTarget)); return RegisterDataSource(() => new ConfiguredDataSourceFactory( diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index ca6f07fa9..22b96aaf5 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -12,7 +12,9 @@ #endif using Extensions.Internal; using Members; + using NetStandardPolyfills; using Projection; + using ReadableExpressions.Extensions; using Validation; internal class MappingConfigurator : @@ -195,7 +197,15 @@ IProjectionFactorySpecifier IRootProjectionConfigurat => CreateFactorySpecifier(); private FactorySpecifier CreateFactorySpecifier() - => new FactorySpecifier(ConfigInfo); + { + if (typeof(TObject).IsPrimitive()) + { + throw new MappingConfigurationException( + $"Unable to configure the creation of primitive type '{typeof(TObject).GetFriendlyName()}'"); + } + + return new FactorySpecifier(ConfigInfo); + } #endregion @@ -233,7 +243,7 @@ private IFullMappingSettings SetEntityKeyMapping(bool mapKeys) return this; } - public IFullMappingSettings AutoReverseConfiguredDataSources() + public IFullMappingSettings AutoReverseConfiguredDataSources() => SetDataSourceReversal(reverse: true); public IFullMappingSettings DoNotAutoReverseConfiguredDataSources() diff --git a/AgileMapper/Caching/ArrayCache.cs b/AgileMapper/Caching/ArrayCache.cs index 32ebff700..2d0a77e95 100644 --- a/AgileMapper/Caching/ArrayCache.cs +++ b/AgileMapper/Caching/ArrayCache.cs @@ -42,9 +42,9 @@ public IEnumerable Values { get { - for (var i = 0; i < _length; ++i) + for (var i = 0; i < _length;) { - yield return _values[i]; + yield return _values[i++]; } } } diff --git a/AgileMapper/Configuration/ConfiguredItemExtensions.cs b/AgileMapper/Configuration/ConfiguredItemExtensions.cs index 4fcfb6ad5..2bbbb3758 100644 --- a/AgileMapper/Configuration/ConfiguredItemExtensions.cs +++ b/AgileMapper/Configuration/ConfiguredItemExtensions.cs @@ -1,7 +1,6 @@ namespace AgileObjects.AgileMapper.Configuration { using System.Collections.Generic; - using System.Linq; using Extensions; using Extensions.Internal; using Members; @@ -17,7 +16,7 @@ public static TItem FindMatch(this IList items, IBasicMapperData m public static IEnumerable FindMatches(this IEnumerable items, IBasicMapperData mapperData) where TItem : UserConfiguredItemBase { - return items?.Filter(item => item.AppliesTo(mapperData)).OrderBy(item => item) ?? Enumerable.Empty; + return items?.Filter(item => item.AppliesTo(mapperData)) ?? Enumerable.Empty; } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs b/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs index 607a5b23a..97ef75ad2 100644 --- a/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs +++ b/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using Api.Configuration.Dictionaries; + using Extensions; using Extensions.Internal; using Members; using ReadableExpressions.Extensions; diff --git a/AgileMapper/Configuration/ParametersSwapper.cs b/AgileMapper/Configuration/ParametersSwapper.cs index 4fe9c5107..88e1c8507 100644 --- a/AgileMapper/Configuration/ParametersSwapper.cs +++ b/AgileMapper/Configuration/ParametersSwapper.cs @@ -3,6 +3,7 @@ namespace AgileObjects.AgileMapper.Configuration using System; using System.Collections.Generic; using System.Linq; + using Extensions; using Extensions.Internal; using Members; using NetStandardPolyfills; @@ -92,7 +93,6 @@ private static Expression SwapForContextParameter(SwapArgs swapArgs) return swapArgs.Lambda.ReplaceParameterWith(swapArgs.MapperData.MappingDataObject); } - var contextTypes = contextType.GetGenericTypeArguments(); var contextInfo = GetAppropriateMappingContext(swapArgs); if (swapArgs.Lambda.Body.NodeType == ExpressionType.Invoke) @@ -100,6 +100,7 @@ private static Expression SwapForContextParameter(SwapArgs swapArgs) return GetInvocationContextArgument(contextInfo, swapArgs.Lambda); } + var contextTypes = contextType.GetGenericTypeArguments(); var memberContextType = IsCallbackContext(contextTypes) ? contextType : contextType.GetAllInterfaces().First(); var sourceProperty = memberContextType.GetPublicInstanceProperty(RootSourceMemberName); var targetProperty = memberContextType.GetPublicInstanceProperty(RootTargetMemberName); @@ -169,14 +170,56 @@ private static Expression ReplaceParameters( private static MappingContextInfo GetAppropriateMappingContext(SwapArgs swapArgs) { + if (swapArgs.ContextTypes.All(t => t.IsSimple())) + { + return GetSimpleTypesMappingContextInfo(swapArgs); + } + if (swapArgs.ContextTypesMatch()) { return new MappingContextInfo(swapArgs); } - var dataAccess = swapArgs.GetAppropriateMappingContextAccess(); + var contextAccess = swapArgs.GetAppropriateMappingContextAccess(); - return new MappingContextInfo(swapArgs, dataAccess); + return new MappingContextInfo(swapArgs, contextAccess); + } + + private static MappingContextInfo GetSimpleTypesMappingContextInfo(SwapArgs swapArgs) + { + var mapperData = swapArgs.MapperData; + IMemberMapperData contextMapperData = mapperData.Parent; + + IQualifiedMember targetMember; + + while (true) + { + if (contextMapperData.TargetMemberIsEnumerableElement()) + { + targetMember = mapperData.TargetMember.RelativeTo(contextMapperData.TargetMember); + break; + } + + if (!contextMapperData.IsRoot) + { + contextMapperData = contextMapperData.Parent; + continue; + } + + targetMember = mapperData.TargetMember; + + contextMapperData = mapperData.GetAppropriateMappingContext( + mapperData.SourceMember.RootType, + targetMember.RootType); + + break; + } + + return new MappingContextInfo( + swapArgs, + mapperData.MappingDataObject, + mapperData.SourceMember.GetQualifiedAccess(contextMapperData.SourceObject), + targetMember.GetQualifiedAccess(contextMapperData.TargetInstance)); } #endregion @@ -252,12 +295,25 @@ public MappingContextInfo(SwapArgs swapArgs) } public MappingContextInfo(SwapArgs swapArgs, Expression contextAccess) + : this( + swapArgs, + contextAccess, + GetValueAccess(swapArgs.GetSourceAccess(contextAccess), swapArgs.ContextTypes[0]), + GetValueAccess(swapArgs.GetTargetAccess(contextAccess), swapArgs.ContextTypes[1])) + { + } + + public MappingContextInfo( + SwapArgs swapArgs, + Expression contextAccess, + Expression sourceAccess, + Expression targetAccess) { _swapArgs = swapArgs; CreatedObject = GetCreatedObject(swapArgs); - SourceAccess = GetValueAccess(swapArgs.GetSourceAccess(contextAccess), ContextTypes[0]); - TargetAccess = GetValueAccess(swapArgs.GetTargetAccess(contextAccess), ContextTypes[1]); + SourceAccess = sourceAccess; + TargetAccess = targetAccess; MappingDataAccess = swapArgs.GetTypedContextAccess(contextAccess); } @@ -321,7 +377,10 @@ public SwapArgs( public bool ContextTypesMatch() => MapperData.TypesMatch(ContextTypes); public Expression GetAppropriateMappingContextAccess() - => MapperData.GetAppropriateMappingContextAccess(ContextTypes); + => GetAppropriateMappingContextAccess(ContextTypes); + + public Expression GetAppropriateMappingContextAccess(params Type[] contextTypes) + => MapperData.GetAppropriateMappingContextAccess(contextTypes); public Expression GetTypedContextAccess(Expression contextAccess) => MapperData.GetTypedContextAccess(contextAccess, ContextTypes); diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index c91b26a0b..e1265c403 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -358,10 +358,14 @@ public ConfiguredDataSourceFactory GetDataSourceFactoryFor(MappingConfigInfo con public bool HasConfiguredRootDataSources { get; private set; } public IList GetDataSources(IMemberMapperData mapperData) - => QueryDataSourceFactories(mapperData).Project(dsf => dsf.Create(mapperData)).ToArray(); + { + return (_dataSourceFactories != null) + ? QueryDataSourceFactories(mapperData).Project(dsf => dsf.Create(mapperData)).ToArray() + : Enumerable.EmptyArray; + } public IEnumerable QueryDataSourceFactories(IBasicMapperData mapperData) - => _dataSourceFactories?.FindMatches(mapperData) ?? Enumerable.Empty; + => _dataSourceFactories.FindMatches(mapperData); #endregion diff --git a/AgileMapper/Constants.cs b/AgileMapper/Constants.cs index 10f7e9bc8..ff0bc1fde 100644 --- a/AgileMapper/Constants.cs +++ b/AgileMapper/Constants.cs @@ -58,7 +58,7 @@ internal static class Constants }; public static readonly Type[] NumericTypes = WholeNumberNumericTypes - .Append(typeof(float), typeof(decimal), typeof(double)); + .Append(new[] { typeof(float), typeof(decimal), typeof(double) }); public static readonly IDictionary NumericTypeMaxValuesByType = GetValuesByType("MaxValue"); public static readonly IDictionary NumericTypeMinValuesByType = GetValuesByType("MinValue"); diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index 79e7b007e..bb226972a 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -13,7 +13,6 @@ namespace AgileObjects.AgileMapper.DataSources internal class DataSourceSet : IEnumerable { private readonly IList _dataSources; - private readonly IList _variables; private Expression _value; public DataSourceSet(IMemberMapperData mapperData, params IDataSource[] dataSources) @@ -24,13 +23,13 @@ public DataSourceSet(IMemberMapperData mapperData, params IDataSource[] dataSour if (None) { - _variables = Enumerable.EmptyArray; + Variables = Enumerable.EmptyArray; return; } var variables = new List(); - for (var i = 0; i < dataSources.Length; ) + for (var i = 0; i < dataSources.Length;) { var dataSource = dataSources[i++]; @@ -55,7 +54,7 @@ public DataSourceSet(IMemberMapperData mapperData, params IDataSource[] dataSour } } - _variables = variables; + Variables = variables; } public IMemberMapperData MapperData { get; } @@ -68,7 +67,7 @@ public DataSourceSet(IMemberMapperData mapperData, params IDataSource[] dataSour public Expression SourceMemberTypeTest { get; } - public ICollection Variables => _variables; + public IList Variables { get; } public IDataSource this[int index] => _dataSources[index]; @@ -78,9 +77,9 @@ private Expression BuildValueExpression() { var value = default(Expression); - for (var i = _dataSources.Count - 1; i >= 0; --i) + for (var i = _dataSources.Count - 1; i >= 0;) { - var dataSource = _dataSources[i]; + var dataSource = _dataSources[i--]; value = dataSource.AddPreConditionIfNecessary(value == default(Expression) ? dataSource.Value diff --git a/AgileMapper/DataSources/Finders/DataSourceFindContext.cs b/AgileMapper/DataSources/Finders/DataSourceFindContext.cs index e89bc6369..e718d6621 100644 --- a/AgileMapper/DataSources/Finders/DataSourceFindContext.cs +++ b/AgileMapper/DataSources/Finders/DataSourceFindContext.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.DataSources.Finders { using System.Collections.Generic; + using Extensions; using Extensions.Internal; using Members; @@ -68,6 +69,12 @@ private static bool UseComplexTypeDataSource(IDataSource dataSource, QualifiedMe return !dataSource.SourceMember.Type.IsSimple(); } + if ((dataSource.Value.Type == targetMember.Type) && + (dataSource.SourceMember.Type != targetMember.Type)) + { + return false; + } + return !targetMember.Type.IsFromBcl(); } } diff --git a/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs b/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs index 0130cbb38..5210da295 100644 --- a/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs +++ b/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using Extensions; using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs b/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs index 7a59d2630..27be4f7d3 100644 --- a/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs +++ b/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs @@ -1,8 +1,8 @@ namespace AgileObjects.AgileMapper.DataSources.Finders { - using System.Collections.Generic; using Extensions.Internal; using Members; + using System.Collections.Generic; internal struct SourceMemberDataSourceFinder : IDataSourceFinder { @@ -35,6 +35,26 @@ public IEnumerable FindFor(DataSourceFindContext context) yield break; } + if (matchingSourceMemberDataSource.SourceMember.IsSimple && + context.MapperData.MapperContext.UserConfigurations.HasConfiguredRootDataSources) + { + var updatedMapperData = new ChildMemberMapperData( + matchingSourceMemberDataSource.SourceMember, + targetMember, + context.MapperData.Parent); + + var configuredRootDataSources = context + .MapperData + .MapperContext + .UserConfigurations + .GetDataSources(updatedMapperData); + + foreach (var configuredRootDataSource in configuredRootDataSources) + { + yield return configuredRootDataSource; + } + } + yield return matchingSourceMemberDataSource; if (!targetMember.IsReadOnly && @@ -62,7 +82,7 @@ private static IDataSource GetSourceMemberDataSourceOrNull(DataSourceFindContext return context.GetFinalDataSource(sourceMemberDataSource, contextMappingData); } - private static bool UseFallbackComplexTypeMappingDataSource(QualifiedMember targetMember) + private static bool UseFallbackComplexTypeMappingDataSource(QualifiedMember targetMember) => targetMember.IsComplex && !targetMember.IsDictionary && (targetMember.Type != typeof(object)); } } \ No newline at end of file diff --git a/AgileMapper/Extensions/Internal/EnumerableExtensions.cs b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs index 3aaf10cc6..e8311344a 100644 --- a/AgileMapper/Extensions/Internal/EnumerableExtensions.cs +++ b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs @@ -232,9 +232,9 @@ public static T[] Prepend(this IList items, T initialItem) return newArray; } - public static T[] Append(this T[] array, T extraItem) + public static T[] Append(this IList array, T extraItem) { - switch (array.Length) + switch (array.Count) { case 0: return new[] { extraItem }; @@ -246,21 +246,28 @@ public static T[] Append(this T[] array, T extraItem) return new[] { array[0], array[1], extraItem }; default: - var newArray = new T[array.Length + 1]; + var newArray = new T[array.Count + 1]; newArray.CopyFrom(array); - newArray[array.Length] = extraItem; + newArray[array.Count] = extraItem; return newArray; } } - public static T[] Append(this IList array, params T[] extraItems) - => Append(array, (IList)extraItems); - public static T[] Append(this IList array, IList extraItems) { + if (array.Count == 1) + { + return Prepend(extraItems, array[0]); + } + + if (extraItems.Count == 1) + { + return Append(array, extraItems[0]); + } + var combinedArray = new T[array.Count + extraItems.Count]; combinedArray.CopyFrom(array); diff --git a/AgileMapper/Extensions/Internal/TypeExtensions.cs b/AgileMapper/Extensions/Internal/TypeExtensions.cs index 8f91cb17b..048d286ff 100644 --- a/AgileMapper/Extensions/Internal/TypeExtensions.cs +++ b/AgileMapper/Extensions/Internal/TypeExtensions.cs @@ -114,30 +114,6 @@ public static bool IsFromBcl(this Type type) public static bool IsComplex(this Type type) => !type.IsSimple() && !type.IsEnumerable(); - public static bool IsSimple(this Type type) - { - type = type.GetNonNullableType(); - - if (type == typeof(ValueType)) - { - return true; - } - - if (type.GetTypeCode() != NetStandardTypeCode.Object) - { - return true; - } - - if ((type == typeof(Guid)) || - (type == typeof(TimeSpan)) || - (type == typeof(DateTimeOffset))) - { - return true; - } - - return type.IsValueType() && type.IsFromBcl(); - } - public static Type[] GetCoercibleNumericTypes(this Type numericType) { var typeMaxValue = Constants.NumericTypeMaxValuesByType[numericType]; diff --git a/AgileMapper/Extensions/PublicTypeExtensions.cs b/AgileMapper/Extensions/PublicTypeExtensions.cs new file mode 100644 index 000000000..3cb7df8d1 --- /dev/null +++ b/AgileMapper/Extensions/PublicTypeExtensions.cs @@ -0,0 +1,47 @@ +namespace AgileObjects.AgileMapper.Extensions +{ + using System; + using System.Reflection; + using Internal; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; + + /// + /// Provides mapping-related extension methods for Types. These methods support mapping, and are + /// not intended to be used from your code. + /// + public static class PublicTypeExtensions + { + internal static readonly MethodInfo IsSimpleMethod = typeof(PublicTypeExtensions) + .GetPublicStaticMethod(nameof(IsSimple)); + + /// + /// Determines if this is simple. + /// + /// The Type for which to make the determination. + /// True if this is simple otherwise false. + public static bool IsSimple(this Type type) + { + type = type.GetNonNullableType(); + + if (type == typeof(ValueType)) + { + return true; + } + + if (type.GetTypeCode() != NetStandardTypeCode.Object) + { + return true; + } + + if ((type == typeof(Guid)) || + (type == typeof(TimeSpan)) || + (type == typeof(DateTimeOffset))) + { + return true; + } + + return type.IsValueType() && type.IsFromBcl(); + } + } +} diff --git a/AgileMapper/Members/ChildMemberMapperData.cs b/AgileMapper/Members/ChildMemberMapperData.cs index f1e8e6c5a..083fb5d88 100644 --- a/AgileMapper/Members/ChildMemberMapperData.cs +++ b/AgileMapper/Members/ChildMemberMapperData.cs @@ -9,21 +9,29 @@ internal class ChildMemberMapperData : BasicMapperData, IMemberMapperData { + private readonly bool _useParentForTypeCheck; private bool? _isRepeatMapping; public ChildMemberMapperData(QualifiedMember targetMember, ObjectMapperData parent) - : this( + : base( + parent.RuleSet, + parent.SourceType, + parent.TargetType, parent.SourceMember, targetMember, parent) { + _useParentForTypeCheck = true; + SourceMember = parent.SourceMember; + Parent = parent; + Context = new MapperDataContext(this); } public ChildMemberMapperData(IQualifiedMember sourceMember, QualifiedMember targetMember, ObjectMapperData parent) : base( parent.RuleSet, - parent.SourceType, - parent.TargetType, + sourceMember.Type, + targetMember.Type, sourceMember, targetMember, parent) @@ -37,7 +45,7 @@ public ChildMemberMapperData(IQualifiedMember sourceMember, QualifiedMember targ public bool IsEntryPoint => Context.IsStandalone || IsRepeatMapping; - public bool IsRepeatMapping => (_isRepeatMapping ?? (_isRepeatMapping = this.IsRepeatMapping())).Value; + private bool IsRepeatMapping => (_isRepeatMapping ?? (_isRepeatMapping = this.IsRepeatMapping())).Value; public ObjectMapperData Parent { get; } @@ -61,6 +69,11 @@ public ChildMemberMapperData(IQualifiedMember sourceMember, QualifiedMember targ public ExpressionInfoFinder ExpressionInfoFinder => Parent.ExpressionInfoFinder; - public override bool HasCompatibleTypes(ITypePair typePair) => Parent.HasCompatibleTypes(typePair); + public override bool HasCompatibleTypes(ITypePair typePair) + { + return _useParentForTypeCheck + ? Parent.HasCompatibleTypes(typePair) + : typePair.HasCompatibleTypes(this, SourceMember, TargetMember); + } } } \ No newline at end of file diff --git a/AgileMapper/Members/ConfiguredSourceMember.cs b/AgileMapper/Members/ConfiguredSourceMember.cs index 1c79cb42a..f9ec8dd93 100644 --- a/AgileMapper/Members/ConfiguredSourceMember.cs +++ b/AgileMapper/Members/ConfiguredSourceMember.cs @@ -4,6 +4,7 @@ namespace AgileObjects.AgileMapper.Members using System.Collections.Generic; using System.Linq; using Caching; + using Extensions; using Extensions.Internal; using ReadableExpressions; using ReadableExpressions.Extensions; @@ -81,6 +82,7 @@ private ConfiguredSourceMember( : type.GetEnumerableElementType(); } + // TODO: Lazy-load this: _childMemberCache = mapperContext.Cache.CreateNew( default(HashCodeComparer)); } @@ -89,6 +91,8 @@ private ConfiguredSourceMember( public Type Type { get; } + public Type RootType => _childMembers[0].Type; + public Type ElementType { get; } public string GetFriendlyTypeName() => Type.GetFriendlyName(); diff --git a/AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs b/AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs index 0da751b3d..ea632f24b 100644 --- a/AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs @@ -27,7 +27,7 @@ public DictionaryEntrySourceMember( { } - public DictionaryEntrySourceMember(DictionaryEntrySourceMember parent, Member childMember) + private DictionaryEntrySourceMember(DictionaryEntrySourceMember parent, Member childMember) : this( childMember.Type, () => parent.GetPath() + "." + childMember.Name, @@ -68,6 +68,8 @@ private DictionaryEntrySourceMember( public Type Type { get; } + public Type RootType => Parent.Type; + public string GetFriendlyTypeName() => Type.GetFriendlyName(); public Type ElementType => _childMembers.Last().ElementType; diff --git a/AgileMapper/Members/Dictionaries/DictionarySourceMember.cs b/AgileMapper/Members/Dictionaries/DictionarySourceMember.cs index 04cc166f8..569f72115 100644 --- a/AgileMapper/Members/Dictionaries/DictionarySourceMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionarySourceMember.cs @@ -66,6 +66,8 @@ private DictionarySourceMember( public Type Type { get; } + public Type RootType => _wrappedSourceMember.RootType; + public Type ElementType => ValueType; public string GetFriendlyTypeName() => _wrappedSourceMember.GetFriendlyTypeName(); diff --git a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs index ccbe384f8..26dca9c88 100644 --- a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs @@ -265,7 +265,7 @@ public override Expression GetPopulation(Expression value, IMemberMapperData map return flattening; } - var keyedAccess = this.GetAccess(mapperData); + var keyedAccess = GetKeyedAccess(mapperData); var convertedValue = GetCheckedValueOrNull(value, keyedAccess, mapperData) ?? @@ -313,8 +313,7 @@ private Expression GetCheckedValueOrNull(Expression value, Expression keyedAcces return GetCheckedTryCatch((TryExpression)value, keyedAccess, checkedAccess, existingValue); } - var replacements = new ExpressionReplacementDictionary(1) { [keyedAccess] = existingValue }; - var checkedValue = ((BlockExpression)value).Replace(replacements); + var checkedValue = ((BlockExpression)value).Replace(keyedAccess, existingValue); return checkedValue.Update( checkedValue.Variables.Append(existingValue), diff --git a/AgileMapper/Members/IMemberMapperData.cs b/AgileMapper/Members/IMemberMapperData.cs index 6cccddd2d..207e38684 100644 --- a/AgileMapper/Members/IMemberMapperData.cs +++ b/AgileMapper/Members/IMemberMapperData.cs @@ -13,8 +13,6 @@ internal interface IMemberMapperData : IBasicMapperData bool IsEntryPoint { get; } - bool IsRepeatMapping { get; } - new ObjectMapperData Parent { get; } MapperDataContext Context { get; } diff --git a/AgileMapper/Members/IQualifiedMember.cs b/AgileMapper/Members/IQualifiedMember.cs index b00ef482c..de7196f7a 100644 --- a/AgileMapper/Members/IQualifiedMember.cs +++ b/AgileMapper/Members/IQualifiedMember.cs @@ -13,6 +13,8 @@ internal interface IQualifiedMember Type Type { get; } + Type RootType { get; } + Type ElementType { get; } string GetFriendlyTypeName(); diff --git a/AgileMapper/Members/Member.cs b/AgileMapper/Members/Member.cs index 4477d177f..eb70b5d06 100644 --- a/AgileMapper/Members/Member.cs +++ b/AgileMapper/Members/Member.cs @@ -3,6 +3,7 @@ namespace AgileObjects.AgileMapper.Members using System; using System.Reflection; using Dictionaries; + using Extensions; using Extensions.Internal; using NetStandardPolyfills; using ObjectPopulation; diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 080b4a53e..53c0a5d77 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -251,25 +251,58 @@ public static bool IsRepeatMapping(this IBasicMapperData mapperData) return false; } - if (GetTargetMembers(mapperData.TargetType).All(tm => tm.IsSimple)) + if (TargetMemberHasRecursiveObjectGraph(mapperData.TargetMember) == false) { return false; } - // The target member we're mapping right now isn't recursive, but - // it might recurse elsewhere within the mapping graph. - // We therefore check if this member ever recurses; if so we'll - // map it by calling MapRepeated, and it'll be the entry point of - // the RepeatedMapperFunc which performs the repeated mapping: + // The target member we're mapping right now isn't recursive, but it has recursion + // within its child members, and its mapping might be repeated elsewhere within the + // mapping graph. We therefore check if this member ever repeats; if so we'll map it + // by calling MapRepeated, and it'll be the entry point of the RepeatedMapperFunc + // which performs the repeated mapping: var rootMember = mapperData.GetRootMapperData().TargetMember; - return TargetMemberEverRecursesWithin(rootMember, mapperData.TargetMember); + return TargetMemberEverRepeatsWithin(rootMember, mapperData.TargetMember); } - private static IList GetTargetMembers(Type targetType) + private static IEnumerable GetTargetMembers(Type targetType) => GlobalContext.Instance.MemberCache.GetTargetMembers(targetType); - private static bool TargetMemberEverRecursesWithin(QualifiedMember parentMember, QualifiedMember subjectMember) + private static bool TargetMemberHasRecursiveObjectGraph(QualifiedMember targetMember) + { + while (true) + { + var mappingType = targetMember.IsEnumerable ? targetMember.ElementType : targetMember.Type; + + var nonSimpleChildMembers = GetTargetMembers(mappingType) + .Filter(m => !m.IsSimple) + .Project(cm => GetNonEnumerableChildMember(targetMember, cm)) + .ToArray(); + + if (nonSimpleChildMembers.None()) + { + return false; + } + + if (nonSimpleChildMembers.Any(cm => cm.IsRecursion)) + { + return true; + } + + foreach (var childMember in nonSimpleChildMembers) + { + if (TargetMemberHasRecursiveObjectGraph(childMember)) + { + return true; + } + } + + return false; + } + } + + private static bool TargetMemberEverRepeatsWithin(QualifiedMember parentMember, IQualifiedMember subjectMember) { while (true) { @@ -302,7 +335,7 @@ private static bool TargetMemberEverRecursesWithin(QualifiedMember parentMember, continue; } - if (TargetMemberEverRecursesWithin(qualifiedChildMember, subjectMember)) + if (TargetMemberEverRepeatsWithin(qualifiedChildMember, subjectMember)) { return true; } @@ -427,7 +460,7 @@ public static Expression GetAppropriateMappingContextAccess(this IMemberMapperDa return dataAccess; } - public static IMemberMapperData GetAppropriateMappingContext(this IMemberMapperData mapperData, Type[] contextTypes) + public static IMemberMapperData GetAppropriateMappingContext(this IMemberMapperData mapperData, params Type[] contextTypes) { if (mapperData.TypesMatch(contextTypes)) { diff --git a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs index 8822c52be..1e4b35824 100644 --- a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs +++ b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.Members.Population { + using System.Collections.Generic; using Extensions.Internal; #if NET35 using Microsoft.Scripting.Ast; @@ -27,12 +28,29 @@ public Expression GetPopulation(IMemberPopulationContext context) if (context.DataSources.Variables.Any()) { - population = Expression.Block(context.DataSources.Variables, population); + population = GetPopulationWithVariables(population, context.DataSources.Variables); } return GetGuardedPopulation(population, populationGuard, useSingleExpression); } + private static Expression GetPopulationWithVariables(Expression population, IList variables) + { + if (population.NodeType != ExpressionType.Block) + { + return Expression.Block(variables, population); + } + + var populationBlock = (BlockExpression)population; + + if (populationBlock.Variables.Any()) + { + variables = variables.Append(populationBlock.Variables); + } + + return populationBlock.Update(variables, populationBlock.Expressions); + } + protected abstract Expression GetPopulationGuard(IMemberPopulationContext context); private Expression GetBinding(IMemberPopulationContext context, Expression populationGuard) diff --git a/AgileMapper/Members/QualifiedMember.cs b/AgileMapper/Members/QualifiedMember.cs index 301c6e09c..d819b891a 100644 --- a/AgileMapper/Members/QualifiedMember.cs +++ b/AgileMapper/Members/QualifiedMember.cs @@ -6,7 +6,6 @@ namespace AgileObjects.AgileMapper.Members using System.Linq; using Caching; using Dictionaries; - using Extensions; using Extensions.Internal; using NetStandardPolyfills; using ReadableExpressions.Extensions; @@ -109,8 +108,12 @@ private bool DetermineRecursion() for (var i = Depth - 2; i >= 0; --i) { - if (LeafMember.Type == MemberChain[i].Type) + if ((LeafMember.Type == MemberChain[i].Type) && + ((Depth - i > 2) || LeafMember.Equals(MemberChain[i]))) { + // Recursion if the types match and either: + // 1. It's via an intermediate object, e.g. Order.OrderItem.Order, or + // 2. It's the same member, e.g. root.Parent.Parent return true; } } @@ -147,6 +150,8 @@ public static QualifiedMember From(Member[] memberChain, MapperContext mapperCon public Type Type => LeafMember?.Type; + public Type RootType => MemberChain[0].Type; + public string GetFriendlyTypeName() => Type.GetFriendlyName(); public Type ElementType => LeafMember?.ElementType; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs index d86613b6c..58a5614cb 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -15,7 +15,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes internal abstract class PopulationExpressionFactoryBase { - public IEnumerable GetPopulation(MappingExpressionFactoryBase.MappingCreationContext context) + public IEnumerable GetPopulation(MappingCreationContext context) { var mappingData = context.MappingData; var mapperData = context.MapperData; @@ -46,7 +46,7 @@ public IEnumerable GetPopulation(MappingExpressionFactoryBase.Mappin } private static void GetCreationCallbacks( - MappingExpressionFactoryBase.MappingCreationContext context, + MappingCreationContext context, out Expression preCreationCallback, out Expression postCreationCallback) { diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs index 84f479c06..02488b623 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using DataSources; + using Extensions; using Extensions.Internal; using Members; #if NET35 diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index 8a1f4f7bf..55180fb5c 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -90,7 +90,16 @@ private Expression AssignDictionaryEntryFromKeyValuePair( var dictionaryEntryMember = _targetDictionaryMember.Append(keyVariable); var targetEntryAssignment = AssignDictionaryEntry(loopData, dictionaryEntryMember, mappingData); - return Expression.Block(new[] { keyVariable }, keyAssignment, targetEntryAssignment); + if (targetEntryAssignment.NodeType != ExpressionType.Block) + { + return Expression.Block(new[] { keyVariable }, keyAssignment, targetEntryAssignment); + } + + var targetEntryAssignmentBlock = (BlockExpression)targetEntryAssignment; + + return Expression.Block( + targetEntryAssignmentBlock.Variables.Prepend(keyVariable), + targetEntryAssignmentBlock.Expressions.Prepend(keyAssignment)); } private Expression AssignDictionaryEntry(IPopulationLoopData loopData, IObjectMappingData mappingData) @@ -119,12 +128,12 @@ private Expression AssignDictionaryEntry( return GetPopulation(loopData, dictionaryEntryMember, mappingData); } - var derivedSourceTypes = mappingData.MapperData.GetDerivedSourceTypes(); - var hasDerivedSourceTypes = derivedSourceTypes.Any(); - List typedVariables; List mappingExpressions; + var derivedSourceTypes = mappingData.MapperData.GetDerivedSourceTypes(); + var hasDerivedSourceTypes = derivedSourceTypes.Any(); + if (hasDerivedSourceTypes) { typedVariables = new List(derivedSourceTypes.Count); @@ -144,10 +153,14 @@ private Expression AssignDictionaryEntry( mappingExpressions = new List(2); } - InsertSourceElementNullCheck(loopData, mappingExpressions); - mappingExpressions.Add(GetPopulation(loopData, dictionaryEntryMember, mappingData)); + InsertSourceElementNullCheck( + loopData, + dictionaryEntryMember, + mappingData.MapperData, + mappingExpressions); + var mappingBlock = hasDerivedSourceTypes ? Expression.Block(typedVariables, mappingExpressions) : Expression.Block(mappingExpressions); @@ -187,7 +200,11 @@ private void AddDerivedSourceTypePopulations( } } - private static void InsertSourceElementNullCheck(IPopulationLoopData loopData, IList mappingExpressions) + private void InsertSourceElementNullCheck( + IPopulationLoopData loopData, + DictionaryTargetMember dictionaryEntryMember, + IMemberMapperData mapperData, + IList mappingExpressions) { var sourceElement = loopData.GetSourceElementValue(); @@ -199,8 +216,16 @@ private static void InsertSourceElementNullCheck(IPopulationLoopData loopData, I loopData.NeedsContinueTarget = true; var sourceElementIsNull = sourceElement.GetIsDefaultComparison(); + + var nullTargetValue = dictionaryEntryMember.ValueType.ToDefaultExpression(); + var addNullEntry = dictionaryEntryMember.GetPopulation(nullTargetValue, mapperData); + + var incrementCounter = _wrappedBuilder.GetCounterIncrement(); var continueLoop = Expression.Continue(loopData.ContinueLoopTarget); - var ifNullContinue = Expression.IfThen(sourceElementIsNull, continueLoop); + + var nullEntryActions = Expression.Block(addNullEntry, incrementCounter, continueLoop); + + var ifNullContinue = Expression.IfThen(sourceElementIsNull, nullEntryActions); mappingExpressions.Insert(0, ifNullContinue); } diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs index 7ab23d8b6..a63dd5295 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs @@ -3,6 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries using System; using System.Collections.Generic; using DataSources; + using Extensions; using Extensions.Internal; using NetStandardPolyfills; #if NET35 diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs index dad82911c..5ec29b736 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs @@ -19,12 +19,14 @@ public override bool IsFor(IObjectMappingData mappingData) protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) { - if (mappingData.MapperData.SourceMember.IsEnumerable) + var mapperData = mappingData.MapperData; + + if (HasCompatibleSourceMember(mapperData)) { return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } - if (HasConfiguredRootDataSources(mappingData.MapperData, out var configuredRootDataSources) && + if (HasConfiguredRootDataSources(mapperData, out var configuredRootDataSources) && configuredRootDataSources.Any(ds => ds.SourceMember.IsEnumerable)) { return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); @@ -32,14 +34,22 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out nullMappingBlock = Expression.Block( ReadableExpression.Comment("No source enumerable available"), - mappingData.MapperData.GetFallbackCollectionValue()); + mapperData.GetFallbackCollectionValue()); return true; } + private static bool HasCompatibleSourceMember(IMemberMapperData mapperData) + { + return mapperData.SourceMember.IsEnumerable && + mapperData.CanConvert( + mapperData.SourceMember.GetElementMember().Type, + mapperData.TargetMember.GetElementMember().Type); + } + protected override IEnumerable GetObjectPopulation(MappingCreationContext context) { - if (!context.MapperData.SourceMember.IsEnumerable) + if (!HasCompatibleSourceMember(context.MapperData)) { yield break; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index e8c210529..94960e288 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -522,7 +522,7 @@ public void AddNewItemsToTargetVariable(IObjectMappingData mappingData) return; } - BuildPopulationLoop((ld, md) => GetTargetMethodCall("Add", ld.GetElementMapping(md)), mappingData); + BuildPopulationLoop(GetElementPopulation, mappingData); } public void BuildPopulationLoop( @@ -539,6 +539,34 @@ public void BuildPopulationLoop( _populationExpressions.Add(populationLoop); } + private Expression GetElementPopulation(IPopulationLoopData loopData, IObjectMappingData mappingData) + { + var elementMapping = loopData.GetElementMapping(mappingData); + + if (InsertSourceObjectElementNullCheck(loopData, out var sourceElement)) + { + elementMapping = Expression.Condition( + sourceElement.GetIsDefaultComparison(), + typeof(object).ToDefaultExpression(), + elementMapping); + } + + return GetTargetMethodCall("Add", elementMapping); + } + + private bool InsertSourceObjectElementNullCheck(IPopulationLoopData loopData, out Expression sourceElement) + { + if (TargetTypeHelper.ElementType != typeof(object)) + { + sourceElement = null; + return false; + } + + sourceElement = loopData.GetSourceElementValue(); + + return sourceElement.Type == typeof(object); + } + public Expression GetElementConversion(Expression sourceElement, IObjectMappingData mappingData) { if (ElementTypesAreSimple) diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs index 1d690cd7d..b7cbfa686 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables { using System; + using Extensions; using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs b/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs index 4b3b08bc3..e479577a3 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs @@ -23,10 +23,7 @@ public static Expression BuildPopulationLoop( var elementPopulation = elementPopulationFactory.Invoke(loopData, mappingData); - var loopBody = Expression.Block( - Expression.IfThen(loopData.LoopExitCheck, breakLoop), - elementPopulation, - builder.GetCounterIncrement()); + var loopBody = GetLoopBody(loopData, builder, breakLoop, elementPopulation); var populationLoop = loopData.NeedsContinueTarget ? Expression.Loop(loopBody, breakLoop.Target, loopData.ContinueLoopTarget) @@ -41,5 +38,32 @@ public static Expression BuildPopulationLoop( return population; } + + private static BlockExpression GetLoopBody( + IPopulationLoopData loopData, + EnumerablePopulationBuilder builder, + Expression breakLoop, + Expression elementPopulation) + { + var ifExitCheckBreakLoop = Expression.IfThen(loopData.LoopExitCheck, breakLoop); + var counterIncrement = builder.GetCounterIncrement(); + + if (elementPopulation.NodeType != ExpressionType.Block) + { + return Expression.Block(ifExitCheckBreakLoop, elementPopulation, counterIncrement); + } + + var elementPopulationBlock = (BlockExpression)elementPopulation; + + var loopExpressions = new Expression[elementPopulationBlock.Expressions.Count + 2]; + + loopExpressions[0] = ifExitCheckBreakLoop; + loopExpressions.CopyFrom(elementPopulationBlock.Expressions, startIndex: 1); + loopExpressions[loopExpressions.Length - 1] = counterIncrement; + + return elementPopulationBlock.Variables.Any() + ? Expression.Block(elementPopulationBlock.Variables, loopExpressions) + : Expression.Block(loopExpressions); + } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs b/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs index 7e6fbb99d..103174808 100644 --- a/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs +++ b/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs @@ -2,6 +2,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.MapperKeys { internal interface IRootMapperKeyFactory { - ObjectMapperKeyBase CreateRootKeyFor(ObjectMappingData mappingData); + ObjectMapperKeyBase CreateRootKeyFor(IObjectMappingData mappingData); } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs b/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs index 670ae6f8b..18e778d77 100644 --- a/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs +++ b/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.MapperKeys { internal struct RootMapperKeyFactory : IRootMapperKeyFactory { - public ObjectMapperKeyBase CreateRootKeyFor(ObjectMappingData mappingData) + public ObjectMapperKeyBase CreateRootKeyFor(IObjectMappingData mappingData) { return new RootObjectMapperKey(mappingData.MappingTypes, mappingData.MappingContext) { diff --git a/AgileMapper/ObjectPopulation/MappingCreationContext.cs b/AgileMapper/ObjectPopulation/MappingCreationContext.cs new file mode 100644 index 000000000..997f55d12 --- /dev/null +++ b/AgileMapper/ObjectPopulation/MappingCreationContext.cs @@ -0,0 +1,105 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using DataSources; + using Members; + + internal class MappingCreationContext + { + private bool _mapperDataHasRootEnumerableVariables; + + public MappingCreationContext( + IObjectMappingData mappingData, + Expression mapToNullCondition = null, + List mappingExpressions = null) + : this(mappingData, null, null, mapToNullCondition, mappingExpressions) + { + } + + public MappingCreationContext( + IObjectMappingData mappingData, + Expression preMappingCallback, + Expression postMappingCallback, + Expression mapToNullCondition, + List mappingExpressions = null) + { + MappingData = mappingData; + PreMappingCallback = preMappingCallback; + PostMappingCallback = postMappingCallback; + MapToNullCondition = mapToNullCondition; + InstantiateLocalVariable = true; + MappingExpressions = mappingExpressions ?? new List(); + } + + public MapperContext MapperContext => MapperData.MapperContext; + + public MappingRuleSet RuleSet => MappingData.MappingContext.RuleSet; + + public ObjectMapperData MapperData => MappingData.MapperData; + + public QualifiedMember TargetMember => MapperData.TargetMember; + + public bool IsRoot => MappingData.IsRoot; + + public IObjectMappingData MappingData { get; } + + public Expression PreMappingCallback { get; } + + public Expression PostMappingCallback { get; } + + public Expression MapToNullCondition { get; } + + public List MappingExpressions { get; } + + public bool InstantiateLocalVariable { get; set; } + + public MappingCreationContext WithDataSource(IDataSource newDataSource) + { + var newSourceMappingData = MappingData.WithSource(newDataSource.SourceMember); + + var newContext = new MappingCreationContext(newSourceMappingData, mappingExpressions: MappingExpressions) + { + InstantiateLocalVariable = false + }; + + newContext.MapperData.SourceObject = newDataSource.Value; + newContext.MapperData.TargetObject = MapperData.TargetObject; + + if (TargetMember.IsComplex) + { + newContext.MapperData.TargetInstance = MapperData.TargetInstance; + } + else if (_mapperDataHasRootEnumerableVariables) + { + UpdateEnumerableVariables(MapperData, newContext.MapperData); + } + + return newContext; + } + + public void UpdateFrom(MappingCreationContext childSourceContext) + { + MappingData.MapperKey.AddSourceMemberTypeTesterIfRequired(childSourceContext.MappingData); + + if (TargetMember.IsComplex || _mapperDataHasRootEnumerableVariables) + { + return; + } + + _mapperDataHasRootEnumerableVariables = true; + + UpdateEnumerableVariables(childSourceContext.MapperData, MapperData); + } + + private static void UpdateEnumerableVariables(ObjectMapperData sourceMapperData, ObjectMapperData targetMapperData) + { + targetMapperData.LocalVariable = sourceMapperData.LocalVariable; + targetMapperData.EnumerablePopulationBuilder.TargetVariable = sourceMapperData.EnumerablePopulationBuilder.TargetVariable; + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 970bc8004..13771cecf 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -6,9 +6,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using DataSources; using Extensions; using Extensions.Internal; - using MapperKeys; using Members; - using Members.Sources; using NetStandardPolyfills; #if NET35 using Microsoft.Scripting.Ast; @@ -49,7 +47,7 @@ public Expression Create(IObjectMappingData mappingData) context.MappingExpressions.AddUnlessNullOrEmpty(derivedTypeMappings); context.MappingExpressions.AddUnlessNullOrEmpty(context.PreMappingCallback); - context.MappingExpressions.AddRange(GetObjectPopulation(context).WhereNotNull()); + context.MappingExpressions.AddRange(GetNonNullObjectPopulation(context)); context.MappingExpressions.AddRange(GetConfiguredRootDataSourcePopulations(context)); context.MappingExpressions.AddUnlessNullOrEmpty(context.PostMappingCallback); @@ -119,6 +117,9 @@ private static MappingCreationContext GetCreationContext(IObjectMappingData mapp private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperData) => mapperData.MapperContext.UserConfigurations.GetMapToNullConditionOrNull(mapperData); + private IEnumerable GetNonNullObjectPopulation(MappingCreationContext context) + => GetObjectPopulation(context).WhereNotNull(); + protected abstract IEnumerable GetObjectPopulation(MappingCreationContext context); private IEnumerable GetConfiguredRootDataSourcePopulations(MappingCreationContext context) @@ -133,7 +134,7 @@ private IEnumerable GetConfiguredRootDataSourcePopulations(MappingCr var configuredRootDataSource = configuredRootDataSources[i]; var newSourceContext = context.WithDataSource(configuredRootDataSource); - var memberPopulations = GetObjectPopulation(newSourceContext).WhereNotNull().ToArray(); + var memberPopulations = GetNonNullObjectPopulation(newSourceContext).ToArray(); if (memberPopulations.None()) { @@ -303,7 +304,7 @@ private Expression GetMappingBlock(MappingCreationContext context) return returnExpression; } - CreateFullMappingBlock: + CreateFullMappingBlock: returnExpression = GetReturnExpression(GetReturnValue(context.MapperData), context); @@ -388,156 +389,55 @@ private static Expression WrapInTryCatch(Expression mappingBlock, IMemberMapperD var configuredCallback = mapperData.MapperContext.UserConfigurations.GetExceptionCallbackOrNull(mapperData); var exceptionVariable = Parameters.Create("ex"); - Expression catchBody; - - if (configuredCallback != null) + if (configuredCallback == null) { - var callbackActionType = configuredCallback.Type.GetGenericTypeArguments()[0]; - - Type[] contextTypes; - Expression contextAccess; - - if (callbackActionType.IsGenericType()) - { - contextTypes = callbackActionType.GetGenericTypeArguments(); - contextAccess = mapperData.GetAppropriateTypedMappingContextAccess(contextTypes); - } - else - { - contextTypes = new[] { mapperData.SourceType, mapperData.TargetType }; - contextAccess = mapperData.MappingDataObject; - } - - var exceptionContextCreateMethod = ObjectMappingExceptionData - .CreateMethod - .MakeGenericMethod(contextTypes); - - var exceptionContextCreateCall = Expression.Call( - exceptionContextCreateMethod, - contextAccess, - exceptionVariable); - - var callbackInvocation = Expression.Invoke(configuredCallback, exceptionContextCreateCall); - var returnDefault = mappingBlock.Type.ToDefaultExpression(); - catchBody = Expression.Block(callbackInvocation, returnDefault); - } - else - { - catchBody = Expression.Throw( + var catchBody = Expression.Throw( MappingException.GetFactoryMethodCall(mapperData, exceptionVariable), mappingBlock.Type); - } - var catchBlock = Expression.Catch(exceptionVariable, catchBody); - - return Expression.TryCatch(mappingBlock, catchBlock); - } + return CreateTryCatch(mappingBlock, exceptionVariable, catchBody); + } - public virtual void Reset() - { - } + var callbackActionType = configuredCallback.Type.GetGenericTypeArguments()[0]; - #region Helper Class + Type[] contextTypes; + Expression contextAccess; - internal class MappingCreationContext - { - private bool _mapperDataHasRootEnumerableVariables; - - public MappingCreationContext( - IObjectMappingData mappingData, - Expression mapToNullCondition = null, - List mappingExpressions = null) - : this(mappingData, null, null, mapToNullCondition, mappingExpressions) + if (callbackActionType.IsGenericType()) { + contextTypes = callbackActionType.GetGenericTypeArguments(); + contextAccess = mapperData.GetAppropriateTypedMappingContextAccess(contextTypes); } - - public MappingCreationContext( - IObjectMappingData mappingData, - Expression preMappingCallback, - Expression postMappingCallback, - Expression mapToNullCondition, - List mappingExpressions = null) + else { - MappingData = mappingData; - PreMappingCallback = preMappingCallback; - PostMappingCallback = postMappingCallback; - MapToNullCondition = mapToNullCondition; - InstantiateLocalVariable = true; - MappingExpressions = mappingExpressions ?? new List(); + contextTypes = new[] { mapperData.SourceType, mapperData.TargetType }; + contextAccess = mapperData.MappingDataObject; } - public MapperContext MapperContext => MapperData.MapperContext; - - public MappingRuleSet RuleSet => MappingData.MappingContext.RuleSet; - - public ObjectMapperData MapperData => MappingData.MapperData; - - public QualifiedMember TargetMember => MapperData.TargetMember; - - public bool IsRoot => MappingData.IsRoot; + var exceptionContextCreateMethod = ObjectMappingExceptionData + .CreateMethod + .MakeGenericMethod(contextTypes); - public IObjectMappingData MappingData { get; } + var createExceptionContextCall = Expression.Call( + exceptionContextCreateMethod, + contextAccess, + exceptionVariable); - public Expression PreMappingCallback { get; } + var callbackInvocation = Expression.Invoke(configuredCallback, createExceptionContextCall); + var returnDefault = mappingBlock.Type.ToDefaultExpression(); + var configuredCatchBody = Expression.Block(callbackInvocation, returnDefault); - public Expression PostMappingCallback { get; } - - public Expression MapToNullCondition { get; } - - public List MappingExpressions { get; } - - public bool InstantiateLocalVariable { get; set; } - - public MappingCreationContext WithDataSource(IDataSource newDataSource) - { - var newSourceMappingData = MappingData.WithSource(newDataSource.SourceMember); - - newSourceMappingData.MapperKey = new RootObjectMapperKey( - RuleSet, - newSourceMappingData.MappingTypes, - new FixedMembersMembersSource(newDataSource.SourceMember, TargetMember)); - - var newContext = new MappingCreationContext(newSourceMappingData, mappingExpressions: MappingExpressions) - { - InstantiateLocalVariable = false - }; - - newContext.MapperData.SourceObject = newDataSource.Value; - newContext.MapperData.TargetObject = MapperData.TargetObject; - - if (TargetMember.IsComplex) - { - newContext.MapperData.TargetInstance = MapperData.TargetInstance; - } - else if (_mapperDataHasRootEnumerableVariables) - { - UpdateEnumerableVariables(MapperData, newContext.MapperData); - } - - return newContext; - } - - public void UpdateFrom(MappingCreationContext childSourceContext) - { - MappingData.MapperKey.AddSourceMemberTypeTesterIfRequired(childSourceContext.MappingData); - - if (TargetMember.IsComplex || _mapperDataHasRootEnumerableVariables) - { - return; - } - - _mapperDataHasRootEnumerableVariables = true; + return CreateTryCatch(mappingBlock, exceptionVariable, configuredCatchBody); + } - UpdateEnumerableVariables(childSourceContext.MapperData, MapperData); - } + private static Expression CreateTryCatch( + Expression mappingBlock, + ParameterExpression exceptionVariable, + Expression catchBody) + { + var catchBlock = Expression.Catch(exceptionVariable, catchBody); - private static void UpdateEnumerableVariables(ObjectMapperData sourceMapperData, ObjectMapperData targetMapperData) - { - targetMapperData.LocalVariable = sourceMapperData.LocalVariable; - targetMapperData.EnumerablePopulationBuilder.TargetVariable = sourceMapperData.EnumerablePopulationBuilder.TargetVariable; - } + return Expression.TryCatch(mappingBlock, catchBlock); } - - #endregion } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index 6c4583018..2a59b1bc6 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System; + using Extensions; using Extensions.Internal; using Members; #if NET35 diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index cfb1f5e3b..391845385 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -7,6 +7,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Reflection; using DataSources; using Enumerables; + using Extensions; using Extensions.Internal; using MapperKeys; using Members; @@ -509,7 +510,7 @@ public MethodCallExpression GetMapCall( return mapCall; } - public MethodCallExpression GetMapCall(Expression sourceElement, Expression targetElement) + public Expression GetMapCall(Expression sourceElement, Expression targetElement) { if (!TargetMember.IsEnumerable && this.TargetMemberIsEnumerableElement()) { @@ -526,7 +527,22 @@ public MethodCallExpression GetMapCall(Expression sourceElement, Expression targ targetElement, EnumerablePopulationBuilder.Counter); - return mapCall; + if ((sourceElement.Type != typeof(object)) || (targetElement.Type != typeof(object))) + { + return mapCall; + } + + var sourceObjectGetTypeMethod = typeof(object).GetPublicInstanceMethod("GetType"); + var sourceObjectGetTypeCall = Expression.Call(sourceElement, sourceObjectGetTypeMethod); + var isSimpleMethod = Extensions.PublicTypeExtensions.IsSimpleMethod; + var sourceObjectTypeIsSimpleCall = Expression.Call(isSimpleMethod, sourceObjectGetTypeCall); + + var simpleSourceTypeOrMapCall = Expression.Condition( + sourceObjectTypeIsSimpleCall, + sourceElement, + mapCall); + + return simpleSourceTypeOrMapCall; } private static MethodInfo GetMapMethod(Type mappingDataType, int numberOfArguments) diff --git a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs index f4986fc44..323b582d8 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs @@ -99,11 +99,6 @@ public ObjectMapper Create(ObjectMappingData public void Reset() { - foreach (var mappingExpressionFactory in _mappingExpressionFactories) - { - mappingExpressionFactory.Reset(); - } - foreach (var mapper in _rootMappersCache.Values) { mapper.Reset(); diff --git a/AgileMapper/ObjectPopulation/ObjectMappingData.cs b/AgileMapper/ObjectPopulation/ObjectMappingData.cs index 83c8f010f..2a55fede1 100644 --- a/AgileMapper/ObjectPopulation/ObjectMappingData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMappingData.cs @@ -302,10 +302,7 @@ TDeclaredTarget IObjectMappingDataUntyped.MapRepeated GetMappingDataCreator 3; diff --git a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs index c8e5719ca..16ea67e05 100644 --- a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Collections.Generic; - using Extensions.Internal; + using Extensions; using Members; #if NET35 using Microsoft.Scripting.Ast; diff --git a/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs b/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs index 68fa3311a..379d7f444 100644 --- a/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs +++ b/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs @@ -6,11 +6,11 @@ internal struct QueryProjectorMapperKeyFactory : IRootMapperKeyFactory { - public ObjectMapperKeyBase CreateRootKeyFor(ObjectMappingData mappingData) + public ObjectMapperKeyBase CreateRootKeyFor(IObjectMappingData mappingData) { - var providerType = ((IQueryable)mappingData.Source).Provider.GetType(); + var providerType = mappingData.GetSource().Provider.GetType(); - return new QueryProjectorKey(mappingData.MappingTypes, providerType, mappingData.MapperContext) + return new QueryProjectorKey(mappingData.MappingTypes, providerType, mappingData.MappingContext.MapperContext) { MappingData = mappingData }; diff --git a/AgileMapper/TypeConversion/ConverterSet.cs b/AgileMapper/TypeConversion/ConverterSet.cs index c31c4b50a..0010e26fa 100644 --- a/AgileMapper/TypeConversion/ConverterSet.cs +++ b/AgileMapper/TypeConversion/ConverterSet.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Configuration; + using Extensions; using Extensions.Internal; using NetStandardPolyfills; using ReadableExpressions.Extensions; @@ -38,6 +39,7 @@ public ConverterSet(UserConfigurationSet userConfigurations) ToNumericConverter.Instance, ToNumericConverter.Instance, TryParseConverter.Instance, + default(OperatorConverter), default(FallbackNonSimpleTypeValueConverter) }; } @@ -46,6 +48,12 @@ public ConverterSet(UserConfigurationSet userConfigurations) public void ThrowIfUnconvertible(Type sourceType, Type targetType) { + if (sourceType.IsEnumerable() && targetType.IsEnumerable()) + { + sourceType = sourceType.GetEnumerableElementType(); + targetType = targetType.GetEnumerableElementType(); + } + if (CanConvert(sourceType, targetType)) { return; @@ -55,7 +63,7 @@ public void ThrowIfUnconvertible(Type sourceType, Type targetType) var targetTypeName = targetType.GetFriendlyName(); throw new MappingConfigurationException( - $"Unable to convert configured {sourceTypeName} to target type {targetTypeName}"); + $"Unable to convert configured '{sourceTypeName}' to target type '{targetTypeName}'"); } public bool CanConvert(Type sourceType, Type targetType) diff --git a/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs b/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs index e93381748..216bf711e 100644 --- a/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs +++ b/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.TypeConversion { using System; - using Extensions.Internal; + using Extensions; using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; diff --git a/AgileMapper/TypeConversion/OperatorConverter.cs b/AgileMapper/TypeConversion/OperatorConverter.cs new file mode 100644 index 000000000..c73efdf06 --- /dev/null +++ b/AgileMapper/TypeConversion/OperatorConverter.cs @@ -0,0 +1,57 @@ +namespace AgileObjects.AgileMapper.TypeConversion +{ + using System; + using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using System.Reflection; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; + + internal struct OperatorConverter : IValueConverter + { + public bool CanConvert(Type nonNullableSourceType, Type nonNullableTargetType) + => GetOperatorOrNull(nonNullableSourceType, nonNullableTargetType) != null; + + public static MethodInfo GetOperatorOrNull( + Type nonNullableSourceType, + Type nonNullableTargetType) + { + if (!nonNullableSourceType.IsPrimitive()) + { + var operatorMethod = GetOperatorOrNull( + nonNullableSourceType, + o => o.ReturnType == nonNullableTargetType); + + if (operatorMethod != null) + { + return operatorMethod; + } + } + + if (nonNullableTargetType.IsPrimitive()) + { + return null; + } + + return GetOperatorOrNull( + nonNullableTargetType, + o => o.GetParameters()[0].ParameterType == nonNullableSourceType); + } + + private static MethodInfo GetOperatorOrNull(Type subjectType, Func matcher) + => subjectType.GetOperators().FirstOrDefault(matcher.Invoke); + + public Expression GetConversion(Expression sourceValue, Type targetType) + { + var nonNullableSourceType = sourceValue.Type.GetNonNullableType(); + var nonNullableTargetType = targetType.GetNonNullableType(); + var operatorMethod = GetOperatorOrNull(nonNullableSourceType, nonNullableTargetType); + + return Expression.Convert(sourceValue, nonNullableTargetType, operatorMethod); + } + } +} \ No newline at end of file diff --git a/AgileMapper/TypeConversion/ToNumericConverter.cs b/AgileMapper/TypeConversion/ToNumericConverter.cs index 07bf01f0c..785a27b80 100644 --- a/AgileMapper/TypeConversion/ToNumericConverter.cs +++ b/AgileMapper/TypeConversion/ToNumericConverter.cs @@ -15,7 +15,7 @@ internal class ToNumericConverter : TryParseConverter { #region Cached Items - public static new readonly ToNumericConverter Instance = new ToNumericConverter(); + public new static readonly ToNumericConverter Instance = new ToNumericConverter(); private static readonly Type[] _coercibleNumericTypes = typeof(TNumeric).GetCoercibleNumericTypes(); diff --git a/AgileMapper/TypeConversion/ToStringConverter.cs b/AgileMapper/TypeConversion/ToStringConverter.cs index 9ffd54efa..c5da2a12f 100644 --- a/AgileMapper/TypeConversion/ToStringConverter.cs +++ b/AgileMapper/TypeConversion/ToStringConverter.cs @@ -30,9 +30,8 @@ public static bool HasNativeStringRepresentation(Type nonNullableType) private static bool HasToStringOperator(Type nonNullableSourceType, out MethodInfo operatorMethod) { - operatorMethod = nonNullableSourceType - .GetOperators(o => o.To()) - .FirstOrDefault(); + operatorMethod = OperatorConverter + .GetOperatorOrNull(nonNullableSourceType, typeof(string)); return operatorMethod != null; } diff --git a/NuGet/AgileObjects.AgileMapper.1.1.0-preview.nupkg b/NuGet/AgileObjects.AgileMapper.1.1.0-preview.nupkg new file mode 100644 index 000000000..dec2dd899 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.1.0-preview.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.1.0-preview2.nupkg b/NuGet/AgileObjects.AgileMapper.1.1.0-preview2.nupkg new file mode 100644 index 000000000..25242ea6b Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.1.0-preview2.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.1.0-preview3.nupkg b/NuGet/AgileObjects.AgileMapper.1.1.0-preview3.nupkg new file mode 100644 index 000000000..9bbf4678b Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.1.0-preview3.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.1.0-preview4.nupkg b/NuGet/AgileObjects.AgileMapper.1.1.0-preview4.nupkg new file mode 100644 index 000000000..b782c4f59 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.1.0-preview4.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.1.0-preview5.nupkg b/NuGet/AgileObjects.AgileMapper.1.1.0-preview5.nupkg new file mode 100644 index 000000000..6ea13ce49 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.1.0-preview5.nupkg differ diff --git a/VersionInfo.cs b/VersionInfo.cs index c9d18c058..bf454fe34 100644 --- a/VersionInfo.cs +++ b/VersionInfo.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("1.0.0")] -[assembly: AssemblyFileVersion("1.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.1.0")] +[assembly: AssemblyFileVersion("1.1.0")] \ No newline at end of file diff --git a/common.props b/common.props index 3c5f18472..2e3c5cd3d 100644 --- a/common.props +++ b/common.props @@ -9,9 +9,9 @@ true git https://github.com/AgileObjects/AgileMapper - 1.0.0 - 1.0.0.0 - 1.0.0.0 + 1.1.0 + 1.1.0.0 + 1.1.0.0 \ No newline at end of file diff --git a/docs/src/Type-Conversion.md b/docs/src/Type-Conversion.md index c75618ced..2333f0009 100644 --- a/docs/src/Type-Conversion.md +++ b/docs/src/Type-Conversion.md @@ -1,11 +1,11 @@ Value conversion is performed according to the following: +- Implicit or explicit operators are used to convert values where they are available. + - Value types, nullable types and strings are all parsed and converted out of the box using the `TryParse` methods from the BCL. - `DateTime`s (and nullable `DateTime`s) are converted to strings using `value.ToString(CultureInfo.CurrentCulture.DateTimeFormat)`. Custom formatting strings [can be configured](/configuration/To-String-Formatting) for to-string conversions. -- Implicit or explicit to-String operators are used to convert values to strings where they are available. - - When parsing numerics, the default value is applied in the following circumstances: - If a value larger or smaller than the target type can contain is parsed - e.g. `double.MaxValue` being mapped to an int