diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index 9c8402f51..af79a4380 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -96,8 +96,30 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -180,7 +202,7 @@ - + diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs index aeab6c395..286193cde 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using AgileMapper.Configuration; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs index 5ed307c0a..ad98fa64e 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Configuration { using System; + using AgileMapper.Extensions.Internal; using TestClasses; using Xunit; @@ -331,8 +332,7 @@ public void ShouldIgnoreMembersBySourceTypeTargetTypeAndPathMatch() mapper.WhenMapping .From>() .To>() - .IgnoreTargetMembersWhere(member => - member.Path.Equals("Value.Line2", StringComparison.OrdinalIgnoreCase)); + .IgnoreTargetMembersWhere(member => member.Path.EqualsIgnoreCase("Value.Line2")); var matchingSource = new PublicField
{ Value = new Address { Line1 = "Here", Line2 = "Here!" } }; var nonMatchingSource = new { Value = new Address { Line1 = "There", Line2 = "There!" } }; diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByGlobalFilter.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByGlobalFilter.cs index 802192323..b0936b8e0 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByGlobalFilter.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByGlobalFilter.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Configuration { using System; - using System.Linq; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; @@ -209,8 +209,7 @@ public void ShouldIgnoreMembersByPathMatch() var source = new { Address = new Address { Line1 = "ONE!", Line2 = "TWO!" } }; mapper.WhenMapping - .IgnoreTargetMembersWhere(member => - member.Path.Equals("Value.Line1", StringComparison.OrdinalIgnoreCase)); + .IgnoreTargetMembersWhere(member => member.Path.EqualsIgnoreCase("Value.Line1")); mapper.WhenMapping .From(source) diff --git a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringDictionaryMappingIncorrectly.cs b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringDictionaryMappingIncorrectly.cs index 2bff412ef..182d2e3ef 100644 --- a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringDictionaryMappingIncorrectly.cs +++ b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringDictionaryMappingIncorrectly.cs @@ -14,7 +14,7 @@ public void ShouldErrorIfCustomMemberKeyIsNull() var configEx = Should.Throw(() => { Mapper.WhenMapping - .Dictionaries + .FromDictionaries .To>() .MapFullKey(null) .To(pf => pf.Value); @@ -29,7 +29,7 @@ public void ShouldErrorIfCustomMemberNameIsNull() var configEx = Should.Throw(() => { Mapper.WhenMapping - .Dictionaries + .FromDictionaries .To>() .MapMemberNameKey(null) .To(pf => pf.Value); @@ -46,12 +46,12 @@ public void ShouldErrorIfIgnoredMemberIsGivenCustomMemberKey() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() .Ignore(p => p.Id); mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() .MapFullKey("PersonId") .To(p => p.Id); @@ -69,12 +69,12 @@ public void ShouldErrorIfIgnoredMemberIsGivenCustomMemberName() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To>() .Ignore(pf => pf.Value); mapper.WhenMapping - .Dictionaries + .FromDictionaries .To>() .MapMemberNameKey("ValueValue") .To(pf => pf.Value); @@ -92,13 +92,13 @@ public void ShouldErrorIfCustomDataSourceMemberIsGivenCustomMemberKey() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() .Map((d, p) => d.Count) .To(p => p.Name); mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() .MapFullKey("PersonName") .To(p => p.Name); @@ -116,13 +116,13 @@ public void ShouldErrorIfCustomDataSourceMemberIsGivenCustomMemberName() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() .Map((d, p) => d.Count) .To(p => p.Name); mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() .MapMemberNameKey("PersonName") .To(p => p.Name); @@ -159,43 +159,43 @@ public void ShouldErrorIfAnUnreadableSourceMemberIsSpecified() } [Fact] - public void ShouldErrorIfMemberNamesAreFlattenedAndSeparatedGlobally() + public void ShouldErrorIfRedundantSourceSeparatorIsConfigured() { var configEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries - .UseFlattenedMemberNames() - .UseMemberNameSeparator("+"); + .FromDictionaries + .UseMemberNameSeparator("."); } }); + configEx.Message.ShouldContain("already"); configEx.Message.ShouldContain("global"); - configEx.Message.ShouldContain("flattened"); + configEx.Message.ShouldContain("'.'"); } [Fact] - public void ShouldErrorIfMemberNamesAreSeparatedAndFlattenedGlobally() + public void ShouldErrorIfMemberNamesAreFlattenedAndSeparatedGlobally() { var configEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries - .UseMemberNameSeparator("+") - .UseFlattenedMemberNames(); + .FromDictionaries + .UseFlattenedTargetMemberNames() + .UseMemberNameSeparator("+"); } }); configEx.Message.ShouldContain("global"); - configEx.Message.ShouldContain("separated with '+'"); + configEx.Message.ShouldContain("flattened"); } [Fact] - public void ShouldErrorIfMemberNamesAreFlattenedAndSeparatedForASpecificTargetType() + public void ShouldErrorIfMemberNamesAreSeparatedAndFlattenedGlobally() { var configEx = Should.Throw(() => { @@ -203,33 +203,32 @@ public void ShouldErrorIfMemberNamesAreFlattenedAndSeparatedForASpecificTargetTy { mapper.WhenMapping .Dictionaries - .To>>() - .UseFlattenedMemberNames() - .UseMemberNameSeparator("_"); + .UseMemberNameSeparator("+") + .UseFlattenedTargetMemberNames(); } }); - configEx.Message.ShouldContain("PublicField>"); - configEx.Message.ShouldContain("flattened"); + configEx.Message.ShouldContain("global"); + configEx.Message.ShouldContain("separated with '+'"); } [Fact] - public void ShouldErrorIfMemberNamesAreSeparatedAndFlattenedForASpecificTargetType() + public void ShouldErrorIfDifferentSeparatorsSpecifiedForASpecificTargetType() { var configEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries - .To>>() - .UseMemberNameSeparator("+") - .UseFlattenedMemberNames(); + .FromDictionaries + .To>>() + .UseMemberNameSeparator("-") + .UseMemberNameSeparator("_"); } }); - configEx.Message.ShouldContain("PublicProperty>"); - configEx.Message.ShouldContain("separated with '+'"); + configEx.Message.ShouldContain("PublicField>"); + configEx.Message.ShouldContain("separated with '-'"); } [Fact] @@ -280,6 +279,24 @@ public void ShouldErrorIfAnElementKeyPartHasMultipleIndexPlaceholders() "pattern must contain a single 'i' character as a placeholder for the enumerable index"); } + [Fact] + public void ShouldErrorIfRedundantGlobalElementKeyPartIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .Dictionaries + .UseElementKeyPattern("[i]"); + } + }); + + configEx.Message.ShouldContain("already"); + configEx.Message.ShouldContain("global"); + configEx.Message.ShouldContain("[i]"); + } + [Fact] public void ShouldErrorIfCustomTargetMemberKeyIsNotAConstant() { diff --git a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs index b7214e9f2..a29ba1fc4 100644 --- a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs +++ b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs @@ -14,7 +14,7 @@ public void ShouldUseACustomFullDictionaryKeyForARootMember() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To>() .MapFullKey("BoomDiddyBoom") .To(pf => pf.Value); @@ -32,7 +32,7 @@ public void ShouldUseACustomFullDictionaryKeyForANestedMember() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .OnTo>>() .MapFullKey("BoomDiddyMcBoom") .To(pf => pf.Value.Value); @@ -54,7 +54,7 @@ public void ShouldUseCustomMemberNameDictionaryKeysForRootMembers() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .Over
() .MapMemberNameKey("HouseNumber") .To(a => a.Line1) @@ -81,7 +81,7 @@ public void ShouldUseACustomMemberNameDictionaryKeyForANestedMember() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionariesWithValueType() .ToANew
() .MapMemberNameKey("HouseName") .To(a => a.Line1); @@ -99,7 +99,7 @@ public void ShouldUseACustomMemberNameDictionaryKeyForANestedArrayMember() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To>() .MapMemberNameKey("Strings") .To(pp => pp.Value); @@ -125,7 +125,7 @@ public void ShouldApplyNonDictionarySpecificConfiguration() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionariesWithValueType() .To() .MapFullKey("BlahBlahBlah") .To(p => p.ProductId) @@ -169,41 +169,14 @@ public void ShouldApplyNonDictionarySpecificConfiguration() } } - [Fact] - public void ShouldApplyFlattenedMemberNamesGlobally() - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .Dictionaries - .UseFlattenedMemberNames(); - - var source = new Dictionary - { - ["Name"] = "Bob", - ["Discount"] = "0.1", - ["AddressLine1"] = "Bob's House", - ["AddressLine2"] = "Bob's Street" - }; - var result = mapper.Map(source).ToANew(); - - result.Name.ShouldBe("Bob"); - result.Discount.ShouldBe(0.1); - result.Address.Line1.ShouldBe("Bob's House"); - result.Address.Line2.ShouldBe("Bob's Street"); - } - } - [Fact] public void ShouldApplyFlattenedMemberNamesToASpecificTargetType() { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .To() - .UseFlattenedMemberNames() - .And .MapMemberNameKey("OrderCode") .To(o => o.OrderId); @@ -257,7 +230,7 @@ public void ShouldApplyACustomSeparatorToASpecificTargetType() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .ToANew() .UseMemberNameSeparator("_") .And @@ -295,6 +268,7 @@ public void ShouldApplyGlobalThenSpecificCustomSeparators() .Dictionaries .UseMemberNameSeparator("+") .AndWhenMapping + .FromDictionaries .ToANew
() .UseMemberNameSeparator("-"); @@ -321,8 +295,7 @@ public void ShouldApplyACustomEnumerableElementPatternGlobally() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries - .UseFlattenedMemberNames() + .FromDictionaries .UseElementKeyPattern("_i_") .AndWhenMapping .To>() @@ -353,10 +326,11 @@ public void ShouldApplyACustomEnumerableElementPatternToASpecificTargetType() { mapper.WhenMapping .Dictionaries - .OnTo
() .UseMemberNameSeparator("-") .UseElementKeyPattern("i") - .And + .AndWhenMapping + .FromDictionariesWithValueType() + .OnTo
() .MapMemberNameKey("StreetName") .To(a => a.Line1) .And @@ -384,13 +358,31 @@ public void ShouldApplyACustomEnumerableElementPatternToASpecificTargetType() } } + [Fact] + public void ShouldApplyACustomConfiguredMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDictionaries + .ToANew>() + .Map(ctx => ctx.Source.Count) + .To(pf => pf.Value); + + var source = new Dictionary { ["One"] = 1, ["Two"] = 2 }; + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(2); + } + } + [Fact] public void ShouldConditionallyMapToDerivedTypes() { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .Dictionaries + .FromDictionaries .ToANew() .If(s => s.Source.ContainsKey("Discount")) .MapTo() @@ -424,7 +416,7 @@ public void ShouldConditionallyMapToDerivedTypesFromASpecificValueTypeDictionary using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .DictionariesWithValueType() + .FromDictionariesWithValueType() .ToANew() .If(s => s.Source["Report"].Length > 10) .MapTo(); @@ -447,5 +439,36 @@ public void ShouldConditionallyMapToDerivedTypesFromASpecificValueTypeDictionary ((MysteryCustomerViewModel)mysteryCustomerResult).Report.ShouldBe("Plenty long enough!"); } } + + [Fact] + public void ShouldRestrictCustomKeysByDictionaryValueType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDictionariesWithValueType() + .ToANew>() + .MapFullKey("LaLaLa") + .To(p => p.Value); + + var matchingSource = new Dictionary + { + ["LaLaLa"] = "1", + ["Value"] = "2" + }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + + matchingResult.Value.ShouldBe("1"); + + var nonMatchingSource = new Dictionary + { + ["LaLaLa"] = "20", + ["Value"] = "10" + }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + + nonMatchingResult.Value.ShouldBe("10"); + } + } } } diff --git a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringTargetDictionaryMapping.cs b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringTargetDictionaryMapping.cs index 610501e53..7f13c836e 100644 --- a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringTargetDictionaryMapping.cs +++ b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringTargetDictionaryMapping.cs @@ -17,7 +17,7 @@ public void ShouldApplyFlattenedMemberNamesGlobally() { mapper.WhenMapping .Dictionaries - .UseFlattenedMemberNames(); + .UseFlattenedTargetMemberNames(); var source = new MysteryCustomer { @@ -39,6 +39,28 @@ public void ShouldApplyFlattenedMemberNamesGlobally() } } + [Fact] + public void ShouldNotApplySourceOnlyConfigurationToTargetDictionaries() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDictionaries + .UseFlattenedTargetMemberNames(); + + var source = new Customer + { + Name = "Paul", + Address = new Address { Line1 = "Abbey Road", Line2 = "Penny Lane" } + }; + var result = mapper.Map(source).ToANew>(); + + result["Name"].ShouldBe("Paul"); + result["Address.Line1"].ShouldBe("Abbey Road"); + result["Address.Line2"].ShouldBe("Penny Lane"); + } + } + [Fact] public void ShouldApplyFlattenedMemberNamesToASpecificSourceType() { @@ -123,8 +145,8 @@ public void ShouldApplyACustomSeparatorToASpecificSourceType() matchingResult["StreetAddress"].ShouldBe("Paddy's"); matchingResult["Address!Line2"].ShouldBe("Philly"); - matchingResult.ContainsKey("Address.Line1").ShouldBeFalse(); - matchingResult.ContainsKey("Address!Line1").ShouldBeFalse(); + matchingResult.ShouldNotContainKey("Address.Line1"); + matchingResult.ShouldNotContainKey("Address!Line1"); var nonMatchingSource = new Customer { Address = address }; var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); @@ -163,7 +185,7 @@ public void ShouldApplyACustomEnumerableElementPatternGlobally() } [Fact] - public void ShouldApplyACustomEnumerableElementPatternToASpecificTargetType() + public void ShouldApplyACustomEnumerableElementPatternToASpecificSourceType() { using (var mapper = Mapper.CreateNew()) { @@ -241,7 +263,7 @@ public void ShouldAllowACustomTargetEntryKey() var noDiscountResult = mapper.Map(noDiscountSource).ToANew>(); noDiscountResult["CustomerName"].ShouldBe("Schumer"); - noDiscountResult.ContainsKey("Name").ShouldBeFalse(); + noDiscountResult.ShouldNotContainKey("Name"); var bigDiscountSource = new MysteryCustomerViewModel { Name = "Silverman", Discount = 0.6 }; var bigDiscountResult = mapper.Map(bigDiscountSource).ToANew>(); @@ -251,24 +273,6 @@ public void ShouldAllowACustomTargetEntryKey() } } - [Fact] - public void ShouldApplyACustomConfiguredMember() - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .Dictionaries - .ToANew>() - .Map(ctx => ctx.Source.Count) - .To(pf => pf.Value); - - var source = new Dictionary { ["One"] = 1, ["Two"] = 2 }; - var result = mapper.Map(source).ToANew>(); - - result.Value.ShouldBe(2); - } - } - [Fact] public void ShouldApplyACustomConfiguredMemberConditionally() { diff --git a/AgileMapper.UnitTests/Dictionaries/WhenCreatingRootDictionaryMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenCreatingRootDictionaryMembers.cs index c2034d3d1..e23137b83 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenCreatingRootDictionaryMembers.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenCreatingRootDictionaryMembers.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using AgileMapper.Members; + using AgileMapper.Members.Dictionaries; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOnToComplexTypes.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOnToComplexTypes.cs new file mode 100644 index 000000000..7a2c2e7bf --- /dev/null +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOnToComplexTypes.cs @@ -0,0 +1,22 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dictionaries +{ + using System; + using System.Collections.Generic; + using TestClasses; + using Xunit; + + public class WhenMappingFromDictionariesOnToComplexTypes + { + [Fact] + public void ShouldPopulateAStringMemberFromANullableTypedEntry() + { + var guid = Guid.NewGuid(); + + var source = new Dictionary { ["Value"] = guid }; + var target = new PublicProperty(); + var result = Mapper.Map(source).OnTo(target); + + result.Value.ShouldBe(guid.ToString()); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMergingObjectsFromDictionaries.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOnToEnumerableMembers.cs similarity index 82% rename from AgileMapper.UnitTests/Dictionaries/WhenMergingObjectsFromDictionaries.cs rename to AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOnToEnumerableMembers.cs index 5f10961f7..7d443330b 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMergingObjectsFromDictionaries.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOnToEnumerableMembers.cs @@ -1,26 +1,13 @@ namespace AgileObjects.AgileMapper.UnitTests.Dictionaries { - using System; using System.Collections.Generic; using System.Linq; using Shouldly; using TestClasses; using Xunit; - public class WhenMergingObjectsFromDictionaries + public class WhenMappingFromDictionariesOnToEnumerableMembers { - [Fact] - public void ShouldPopulateAStringMemberFromANullableTypedEntry() - { - var guid = Guid.NewGuid(); - - var source = new Dictionary { ["Value"] = guid }; - var target = new PublicProperty(); - var result = Mapper.Map(source).OnTo(target); - - result.Value.ShouldBe(guid.ToString()); - } - [Fact] public void ShouldMergeSimpleTypeListFromSimpleTypeDictionaryImplementationEntries() { @@ -59,7 +46,7 @@ public void ShouldMergeSimpleTypeCollectionFromSimpleTypeDictionaryImplementatio } [Fact] - public void ShouldMergeANestedComplexTypeArrayFromUntypedDictionaryImplementationEntries() + public void ShouldMergeAComplexTypeArrayFromUntypedDictionaryImplementationEntries() { var source = new StringKeyedDictionary { diff --git a/AgileMapper.UnitTests/Dictionaries/WhenOverwritingObjectsFromDictionaries.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOverComplexTypes.cs similarity index 97% rename from AgileMapper.UnitTests/Dictionaries/WhenOverwritingObjectsFromDictionaries.cs rename to AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOverComplexTypes.cs index dcd790f0a..4a24a4d89 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenOverwritingObjectsFromDictionaries.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesOverComplexTypes.cs @@ -6,7 +6,7 @@ using TestClasses; using Xunit; - public class WhenOverwritingObjectsFromDictionaries + public class WhenMappingFromDictionariesOverComplexTypes { [Fact] public void ShouldPopulateADateTimeMemberFromAnUntypedEntry() diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewComplexTypeMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewComplexTypeMembers.cs new file mode 100644 index 000000000..8c7057fad --- /dev/null +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewComplexTypeMembers.cs @@ -0,0 +1,117 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dictionaries +{ + using System.Collections.Generic; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDictionariesToNewComplexTypeMembers + { + [Fact] + public void ShouldMapToAStringMemberFromTypedDottedEntries() + { + var source = new Dictionary { ["Value.Value"] = "Over there!" }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.Value.ShouldBe("Over there!"); + } + + [Fact] + public void ShouldMapToABoolMemberFromUntypedDottedEntries() + { + var source = new Dictionary { ["Value.Value"] = "true" }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.Value.ShouldBeTrue(); + } + + [Fact] + public void ShouldMapToMemberFromFlattenedNameEntries() + { + var source = new Dictionary + { + ["Name"] = "Bob", + ["Discount"] = "0.1", + ["AddressLine1"] = "Bob's House", + ["AddressLine2"] = "Bob's Street" + }; + var result = Mapper.Map(source).ToANew(); + + result.Name.ShouldBe("Bob"); + result.Discount.ShouldBe(0.1); + result.Address.Line1.ShouldBe("Bob's House"); + result.Address.Line2.ShouldBe("Bob's Street"); + } + + [Fact] + public void ShouldMapToNestedMembersFromUntypedEntries() + { + var source = new Dictionary + { + ["Id"] = 123, + ["Name"] = "Captain Customer", + ["Address"] = new Address + { + Line1 = "Line 1", + Line2 = "Line 2" + } + }; + var result = Mapper.Map(source).ToANew(); + + result.Id.ShouldBeDefault(); + result.Name.ShouldBe("Captain Customer"); + result.Address.ShouldNotBeNull(); + result.Address.Line1.ShouldBe("Line 1"); + result.Address.Line2.ShouldBe("Line 2"); + } + + [Fact] + public void ShouldMapToDeepNestedComplexTypeMembersFromUntypedDottedEntries() + { + var source = new Dictionary + { + ["Value[0].Value.SetValue[0].Title"] = "Mr", + ["Value[0].Value.SetValue[0].Name"] = "Franks", + ["Value[0].Value.SetValue[0].Address.Line1"] = "Somewhere", + ["Value[0].Value.SetValue[0].Address.Line2"] = "Over the rainbow", + ["Value[0].Value.SetValue[1]"] = new PersonViewModel { Name = "Mike", AddressLine1 = "La la la" }, + ["Value[0].Value.SetValue[2].Title"] = 5, + ["Value[0].Value.SetValue[2].Name"] = "Wilkes", + ["Value[0].Value.SetValue[2].Address.Line1"] = "Over there", + ["Value[1].Value.SetValue[0].Title"] = 737328, + ["Value[1].Value.SetValue[0].Name"] = "Rob", + ["Value[1].Value.SetValue[0].Address.Line1"] = "Some place" + }; + + var result = Mapper + .Map(source) + .ToANew>>>>(); + + result.Value.Count.ShouldBe(2); + + result.Value.First().Value.Value.Length.ShouldBe(3); + result.Value.Second().Value.Value.Length.ShouldBe(1); + + result.Value.First().Value.Value.First().Title.ShouldBe(Title.Mr); + result.Value.First().Value.Value.First().Name.ShouldBe("Franks"); + result.Value.First().Value.Value.First().Address.Line1.ShouldBe("Somewhere"); + result.Value.First().Value.Value.First().Address.Line2.ShouldBe("Over the rainbow"); + + result.Value.First().Value.Value.Second().Title.ShouldBeDefault(); + result.Value.First().Value.Value.Second().Name.ShouldBe("Mike"); + result.Value.First().Value.Value.Second().Address.Line1.ShouldBe("La la la"); + result.Value.First().Value.Value.Second().Address.Line2.ShouldBeDefault(); + + result.Value.First().Value.Value.Third().Title.ShouldBe(Title.Mrs); + result.Value.First().Value.Value.Third().Name.ShouldBe("Wilkes"); + result.Value.First().Value.Value.Third().Address.Line1.ShouldBe("Over there"); + result.Value.First().Value.Value.Third().Address.Line2.ShouldBeDefault(); + + result.Value.Second().Value.Value.First().Title.ShouldBeDefault(); + result.Value.Second().Value.Value.First().Name.ShouldBe("Rob"); + result.Value.Second().Value.Value.First().Address.Line1.ShouldBe("Some place"); + result.Value.Second().Value.Value.First().Address.Line2.ShouldBeDefault(); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewComplexTypes.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewComplexTypes.cs new file mode 100644 index 000000000..c3dc6561f --- /dev/null +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewComplexTypes.cs @@ -0,0 +1,145 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dictionaries +{ + using System; + using System.Collections.Generic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDictionariesToNewComplexTypes + { + [Fact] + public void ShouldMapToAnIntMemberFromATypedEntry() + { + var source = new Dictionary { ["Value"] = 123 }; + var result = Mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBe(123); + } + + [Fact] + public void ShouldMapToAnIntMemberFromADictionaryImplementationTypedEntry() + { + var source = new StringKeyedDictionary { ["Value"] = 999 }; + var result = Mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBe(999L); + } + + [Fact] + public void ShouldMapToAStringMemberFromATypedEntryCaseInsensitively() + { + var source = new Dictionary { ["value"] = "Hello" }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe("Hello"); + } + + [Fact] + public void ShouldMapToAStringSetMethodFromATypedEntry() + { + var source = new Dictionary { ["SetValue"] = "Goodbye" }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe("Goodbye"); + } + + [Fact] + public void ShouldConvertASimpleTypeMemberValue() + { + var source = new Dictionary { ["setvalue"] = "123" }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(123); + } + + [Fact] + public void ShouldConvertASimpleTypeMemberValueFromObject() + { + var idGuid = Guid.NewGuid(); + var source = new Dictionary { ["Id"] = idGuid.ToString() }; + var result = Mapper.Map(source).ToANew(); + + result.ShouldNotBeNull(); + result.Id.ShouldBe(idGuid); + } + + [Fact] + public void ShouldMapToSimpleTypeConstructorParameterFromUntypedEntry() + { + var guid = Guid.NewGuid(); + var source = new Dictionary { ["Value"] = guid.ToString() }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(guid); + } + + [Fact] + public void ShouldIgnoreANonStringKeyedDictionary() + { + var source = new Dictionary { [123] = 456 }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldHandleAnUnparseableStringValue() + { + var source = new Dictionary { ["Value"] = "jkdekml" }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldHandleANullObjectValue() + { + var source = new Dictionary { ["Value"] = null }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldIgnoreADeclaredUnconvertibleValueType() + { + var source = new Dictionary { ["Value"] = new byte[0] }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldHandleAnUnconvertibleValueForASimpleType() + { + var source = new Dictionary { ["Value"] = new object() }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBeNull(); + } + + [Fact] + public void ShouldHandleAMappingException() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .To
() + .CreateInstancesUsing(ctx => new Address { Line1 = int.Parse("rstgerfed").ToString() }); + + var source = new Dictionary + { + ["Line1"] = "La la la", + ["Line2"] = "La la la" + }; + + var mappingEx = Should.Throw(() => mapper.Map(source).ToANew
()); + + mappingEx.Message.ShouldContain("Dictionary -> Address"); + } + } + } +} diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerableMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerableMembers.cs new file mode 100644 index 000000000..7c68efe73 --- /dev/null +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerableMembers.cs @@ -0,0 +1,125 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dictionaries +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDictionariesToNewEnumerableMembers + { + [Fact] + public void ShouldMapToASimpleTypeListFromTypedEntries() + { + var source = new Dictionary + { + ["Value[0]"] = 9, + ["Value[1]"] = 8, + ["Value[2]"] = 7 + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBe(9, 8, 7); + } + + [Fact] + public void ShouldMapToASimpleTypeCollectionFromConvertibleTypedEntries() + { + var now = DateTime.Now; + + var source = new Dictionary + { + ["Value[0]"] = now, + ["value[1]"] = now.AddHours(1), + ["Value[2]"] = now.AddHours(2) + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBe( + now.ToCurrentCultureString(), + now.AddHours(1).ToCurrentCultureString(), + now.AddHours(2).ToCurrentCultureString()); + } + + [Fact] + public void ShouldMapToASimpleTypeCollectionFromAnUntypedEntry() + { + var source = new Dictionary + { + ["Value"] = new[] { '1', '2', '3' } + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBe(1, 2, 3); + } + + [Fact] + public void ShouldMapToAComplexTypeArrayFromUntypedEntries() + { + var source = new Dictionary + { + ["Value[0]"] = new Customer { Name = "Mr Pants" }, + ["Value[1]"] = new Person { Name = "Ms Blouse" } + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.Length.ShouldBe(2); + result.Value.First().ShouldBeOfType(); + result.Value.First().Name.ShouldBe("Mr Pants"); + result.Value.Second().Name.ShouldBe("Ms Blouse"); + } + + [Fact] + public void ShouldMapToAComplexTypeCollectionFromTypedDottedEntries() + { + var source = new Dictionary + { + ["Value[0].ProductId"] = "Spade", + ["Value[0].Price"] = "100.00", + ["Value[0].HowMega"] = "1.01" + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldHaveSingleItem(); + result.Value.First().ProductId.ShouldBe("Spade"); + result.Value.First().Price.ShouldBe(100.00); + result.Value.First().HowMega.ShouldBe(1.01); + } + + [Fact] + public void ShouldMapToAComplexTypeArrayFromUntypedDottedEntries() + { + var source = new Dictionary + { + ["Value[0].ProductId"] = "Jay", + ["Value[0].Price"] = "100.00", + ["Value[0].HowMega"] = "1.01", + ["Value[1].ProductId"] = "Silent Bob", + ["Value[1].Price"] = "1000.00", + ["Value[1].HowMega"] = ".99" + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.Length.ShouldBe(2); + + result.Value.First().ProductId.ShouldBe("Jay"); + result.Value.First().Price.ShouldBe(100.00); + result.Value.First().HowMega.ShouldBe(1.01); + + result.Value.Second().ProductId.ShouldBe("Silent Bob"); + result.Value.Second().Price.ShouldBe(1000.00); + result.Value.Second().HowMega.ShouldBe(0.99); + } + + [Fact] + public void ShouldHandleAnUnconvertibleValueForACollection() + { + var source = new Dictionary { ["Value"] = new Person { Name = "Nope" } }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBeEmpty(); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerables.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerables.cs new file mode 100644 index 000000000..984e760e3 --- /dev/null +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerables.cs @@ -0,0 +1,213 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dictionaries +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDictionariesToNewEnumerables + { + [Fact] + public void ShouldMapToASimpleTypeCollectionFromATypedSourceArray() + { + var source = new Dictionary { ["Value"] = new long[] { 4, 5, 6 } }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBe(4, 5, 6); + } + + [Fact] + public void ShouldMapToASimpleTypeListFromNullableTypedSourceEntries() + { + var source = new Dictionary + { + ["Value[0]"] = 56, + ["Value[1]"] = null, + ["Value[2]"] = 27382 + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBe(56, null, default(byte?)); + } + + [Fact] + public void ShouldMapToASimpleTypeEnumerableFromAnUntypedSourceArray() + { + var source = new Dictionary { ["Value"] = new[] { 1, 2, 3 } }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldBe(1, 2, 3); + } + + [Fact] + public void ShouldMapToASimpleTypeArrayFromAConvertibleTypedSourceEnumerable() + { + var source = new Dictionary> { ["Value"] = new[] { 4, 5, 6 } }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe("4", "5", "6"); + } + + [Fact] + public void ShouldMapToAComplexTypeArrayFromAConvertibleTypedSourceEnumerable() + { + var source = new Dictionary> + { + ["Value"] = new[] + { + new Person { Name = "Mr Pants"}, + new Customer { Name = "Mrs Blouse" } + } + + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.Length.ShouldBe(2); + result.Value.First().Name.ShouldBe("Mr Pants"); + result.Value.Second().Name.ShouldBe("Mrs Blouse"); + } + + [Fact] + public void ShouldMapToASimpleTypeEnumerableFromTypedEntries() + { + var source = new Dictionary + { + ["[0]"] = 1, + ["[1]"] = 2, + ["[2]"] = 3 + }; + var result = Mapper.Map(source).ToANew>(); + + result.ShouldBe(1, 2, 3); + } + + [Fact] + public void ShouldMapToASimpleTypeArrayFromConvertibleTypedEntries() + { + var source = new Dictionary + { + ["[0]"] = 123, + ["[1]"] = long.MaxValue, + ["[2]"] = 789 + }; + var result = Mapper.Map(source).ToANew(); + + result.Length.ShouldBe(3); + result.First().ShouldBe(123); + result.Second().ShouldBeDefault(); + result.Third().ShouldBe(789); + } + + [Fact] + public void ShouldMapToAComplexTypeCollectionFromTypedEntries() + { + var source = new Dictionary + { + ["[0]"] = new MegaProduct { ProductId = "asdfasdf" }, + ["[1]"] = new MegaProduct { ProductId = "mnbvmnbv" } + }; + var result = Mapper.Map(source).ToANew>(); + + result.Count.ShouldBe(2); + result.First().ShouldNotBeSameAs(source.First()); + result.First().ProductId.ShouldBe("asdfasdf"); + result.Second().ShouldNotBeSameAs(source.Second()); + result.Second().ProductId.ShouldBe("mnbvmnbv"); + } + + [Fact] + public void ShouldMapToAComplexTypeListFromUntypedEntries() + { + var source = new Dictionary + { + ["[0]"] = new Product { ProductId = "Pants" }, + ["[1]"] = new MegaProduct { ProductId = "Blouse" } + }; + var result = Mapper.Map(source).ToANew>(); + + result.Count.ShouldBe(2); + result.First().ProductId.ShouldBe("Pants"); + result.Second().ShouldBeOfType(); + result.Second().ProductId.ShouldBe("Blouse"); + } + + [Fact] + public void ShouldMapToAComplexTypeEnumerableFromTypedDottedEntries() + { + var source = new Dictionary + { + ["[0].ProductId"] = "Hose", + ["[0].Price"] = "1.99" + }; + var result = Mapper.Map(source).ToANew>(); + + result.ShouldHaveSingleItem(); + result.First().ProductId.ShouldBe("Hose"); + result.First().Price.ShouldBe(1.99); + } + + [Fact] + public void ShouldMapToAParameterisedConstructorComplexTypeEnumerableFromTypedDottedEntries() + { + var source = new Dictionary + { + ["[0].Value"] = "123", + ["[1].Value"] = "456", + ["[2].value"] = "789" + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Count().ShouldBe(3); + result.First().Value.ShouldBe(123); + result.Second().Value.ShouldBe(456); + result.Third().Value.ShouldBe(789); + } + + [Fact] + public void ShouldMapToAComplexTypeCollectionFromUntypedDottedEntries() + { + var source = new Dictionary + { + ["[0].ProductId"] = "Blouse", + ["[0].Price"] = "10.99", + ["[1].ProductId"] = "Pants", + ["[1].Price"] = "7.99" + }; + var result = Mapper.Map(source).ToANew>(); + + result.Count.ShouldBe(2); + result.First().ProductId.ShouldBe("Blouse"); + result.First().Price.ShouldBe(10.99); + result.Second().ProductId.ShouldBe("Pants"); + result.Second().Price.ShouldBe(7.99); + } + + [Fact] + public void ShouldMapToComplexTypeAndSimpleTypeArrayConstructorParametersFromUntypedDottedEntries() + { + var now = DateTime.Now; + var nowString = now.ToCurrentCultureString(); + var inTenMinutes = now.AddMinutes(10).ToCurrentCultureString(); + var inTwentyMinutes = now.AddMinutes(20).ToCurrentCultureString(); + + var source = new Dictionary + { + ["Value1.ProductId"] = "Boom", + ["Value1.Price"] = "1.99", + ["Value1.HowMega"] = "1.00", + ["Value2[0]"] = nowString, + ["Value2[1]"] = inTenMinutes, + ["Value2[2]"] = inTwentyMinutes + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value1.ProductId.ShouldBe("Boom"); + result.Value1.Price.ShouldBe(1.99); + result.Value1.HowMega.ShouldBe(1.00); + + result.Value2.ShouldBe(d => d.ToCurrentCultureString(), nowString, inTenMinutes, inTwentyMinutes); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingNewObjectsFromDictionaries.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingNewObjectsFromDictionaries.cs deleted file mode 100644 index 02d691afc..000000000 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingNewObjectsFromDictionaries.cs +++ /dev/null @@ -1,504 +0,0 @@ -namespace AgileObjects.AgileMapper.UnitTests.Dictionaries -{ - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; - using Shouldly; - using TestClasses; - using Xunit; - - public class WhenMappingNewObjectsFromDictionaries - { - [Fact] - public void ShouldPopulateAnIntMemberFromATypedEntry() - { - var source = new Dictionary { ["Value"] = 123 }; - var result = Mapper.Map(source).ToANew>(); - - result.ShouldNotBeNull(); - result.Value.ShouldBe(123); - } - - [Fact] - public void ShouldPopulateAStringMemberFromATypedEntryCaseInsensitively() - { - var source = new Dictionary { ["value"] = "Hello" }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBe("Hello"); - } - - [Fact] - public void ShouldPopulateAStringSetMethodFromATypedEntry() - { - var source = new Dictionary { ["SetValue"] = "Goodbye" }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBe("Goodbye"); - } - - [Fact] - public void ShouldPopulateANestedStringMemberFromTypedDottedEntries() - { - var source = new Dictionary { ["Value.Value"] = "Over there!" }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.Value.ShouldBe("Over there!"); - } - - [Fact] - public void ShouldPopulateANestedBoolMemberFromUntypedDottedEntries() - { - var source = new Dictionary { ["Value.Value"] = "true" }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.Value.ShouldBeTrue(); - } - - [Fact] - public void ShouldConvertASimpleTypeMemberValue() - { - var source = new Dictionary { ["setvalue"] = "123" }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBe(123); - } - - [Fact] - public void ShouldConvertASimpleTypeMemberValueFromObject() - { - var idGuid = Guid.NewGuid(); - var source = new Dictionary { ["Id"] = idGuid.ToString() }; - var result = Mapper.Map(source).ToANew(); - - result.ShouldNotBeNull(); - result.Id.ShouldBe(idGuid); - } - - [Fact] - public void ShouldPopulateASimpleTypeCollectionFromATypedSourceArray() - { - var source = new Dictionary { ["Value"] = new long[] { 4, 5, 6 } }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldBe(4, 5, 6); - } - - [Fact] - public void ShouldPopulateASimpleTypeListFromNullableTypedSourceEntries() - { - var source = new Dictionary - { - ["Value[0]"] = 56, - ["Value[1]"] = null, - ["Value[2]"] = 27382 - }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldBe(56, null, default(byte?)); - } - - [Fact] - public void ShouldPopulateASimpleTypeEnumerableFromAnUntypedSourceArray() - { - var source = new Dictionary { ["Value"] = new[] { 1, 2, 3 } }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldBe(1, 2, 3); - } - - [Fact] - public void ShouldPopulateASimpleTypeArrayFromAConvertibleTypedSourceEnumerable() - { - var source = new Dictionary> { ["Value"] = new[] { 4, 5, 6 } }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBe("4", "5", "6"); - } - - [Fact] - public void ShouldPopulateAComplexTypeArrayFromAConvertibleTypedSourceEnumerable() - { - var source = new Dictionary> - { - ["Value"] = new[] - { - new Person { Name = "Mr Pants"}, - new Customer { Name = "Mrs Blouse" } - } - - }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.Length.ShouldBe(2); - result.Value.First().Name.ShouldBe("Mr Pants"); - result.Value.Second().Name.ShouldBe("Mrs Blouse"); - } - - [Fact] - public void ShouldPopulateARootSimpleTypeEnumerableFromTypedEntries() - { - var source = new Dictionary - { - ["[0]"] = 1, - ["[1]"] = 2, - ["[2]"] = 3 - }; - var result = Mapper.Map(source).ToANew>(); - - result.ShouldBe(1, 2, 3); - } - - [Fact] - public void ShouldPopulateANestedSimpleTypeListFromTypedEntries() - { - var source = new Dictionary - { - ["Value[0]"] = 9, - ["Value[1]"] = 8, - ["Value[2]"] = 7 - }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldBe(9, 8, 7); - } - - [Fact] - public void ShouldPopulateANestedSimpleTypeCollectionFromConvertibleTypedEntries() - { - var now = DateTime.Now; - - var source = new Dictionary - { - ["Value[0]"] = now, - ["value[1]"] = now.AddHours(1), - ["Value[2]"] = now.AddHours(2) - }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldBe( - now.ToCurrentCultureString(), - now.AddHours(1).ToCurrentCultureString(), - now.AddHours(2).ToCurrentCultureString()); - } - - [Fact] - public void ShouldPopulateARootSimpleTypeArrayFromConvertibleTypedEntries() - { - var source = new Dictionary - { - ["[0]"] = 123, - ["[1]"] = long.MaxValue, - ["[2]"] = 789 - }; - var result = Mapper.Map(source).ToANew(); - - result.Length.ShouldBe(3); - result.First().ShouldBe(123); - result.Second().ShouldBeDefault(); - result.Third().ShouldBe(789); - } - - [Fact] - public void ShouldPopulateARootComplexTypeCollectionFromTypedEntries() - { - var source = new Dictionary - { - ["[0]"] = new MegaProduct { ProductId = "asdfasdf" }, - ["[1]"] = new MegaProduct { ProductId = "mnbvmnbv" } - }; - var result = Mapper.Map(source).ToANew>(); - - result.Count.ShouldBe(2); - result.First().ShouldNotBeSameAs(source.First()); - result.First().ProductId.ShouldBe("asdfasdf"); - result.Second().ShouldNotBeSameAs(source.Second()); - result.Second().ProductId.ShouldBe("mnbvmnbv"); - } - - [Fact] - public void ShouldPopulateARootComplexTypeListFromUntypedEntries() - { - var source = new Dictionary - { - ["[0]"] = new Product { ProductId = "Pants" }, - ["[1]"] = new MegaProduct { ProductId = "Blouse" } - }; - var result = Mapper.Map(source).ToANew>(); - - result.Count.ShouldBe(2); - result.First().ProductId.ShouldBe("Pants"); - result.Second().ShouldBeOfType(); - result.Second().ProductId.ShouldBe("Blouse"); - } - - [Fact] - public void ShouldPopulateARootComplexTypeEnumerableFromTypedDottedEntries() - { - var source = new Dictionary - { - ["[0].ProductId"] = "Hose", - ["[0].Price"] = "1.99" - }; - var result = Mapper.Map(source).ToANew>(); - - result.ShouldHaveSingleItem(); - result.First().ProductId.ShouldBe("Hose"); - result.First().Price.ShouldBe(1.99); - } - - [Fact] - public void ShouldPopulateARootParameterisedConstructorComplexTypeEnumerableFromTypedDottedEntries() - { - var source = new Dictionary - { - ["[0].Value"] = "123", - ["[1].Value"] = "456", - ["[2].value"] = "789" - }; - var result = Mapper.Map(source).ToANew>>(); - - result.Count().ShouldBe(3); - result.First().Value.ShouldBe(123); - result.Second().Value.ShouldBe(456); - result.Third().Value.ShouldBe(789); - } - - [Fact] - public void ShouldPopulateARootComplexTypeCollectionFromUntypedDottedEntries() - { - var source = new Dictionary - { - ["[0].ProductId"] = "Blouse", - ["[0].Price"] = "10.99", - ["[1].ProductId"] = "Pants", - ["[1].Price"] = "7.99" - }; - var result = Mapper.Map(source).ToANew>(); - - result.Count.ShouldBe(2); - result.First().ProductId.ShouldBe("Blouse"); - result.First().Price.ShouldBe(10.99); - result.Second().ProductId.ShouldBe("Pants"); - result.Second().Price.ShouldBe(7.99); - } - - [Fact] - public void ShouldPopulateANestedComplexTypeArrayFromUntypedEntries() - { - var source = new Dictionary - { - ["Value[0]"] = new Customer { Name = "Mr Pants" }, - ["Value[1]"] = new Person { Name = "Ms Blouse" } - }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.Length.ShouldBe(2); - result.Value.First().ShouldBeOfType(); - result.Value.First().Name.ShouldBe("Mr Pants"); - result.Value.Second().Name.ShouldBe("Ms Blouse"); - } - - [Fact] - public void ShouldPopulateANestedComplexTypeCollectionFromTypedDottedEntries() - { - var source = new Dictionary - { - ["Value[0].ProductId"] = "Spade", - ["Value[0].Price"] = "100.00", - ["Value[0].HowMega"] = "1.01" - }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldHaveSingleItem(); - result.Value.First().ProductId.ShouldBe("Spade"); - result.Value.First().Price.ShouldBe(100.00); - result.Value.First().HowMega.ShouldBe(1.01); - } - - [Fact] - public void ShouldPopulateANestedComplexTypeArrayFromUntypedDottedEntries() - { - var source = new Dictionary - { - ["Value[0].ProductId"] = "Jay", - ["Value[0].Price"] = "100.00", - ["Value[0].HowMega"] = "1.01", - ["Value[1].ProductId"] = "Silent Bob", - ["Value[1].Price"] = "1000.00", - ["Value[1].HowMega"] = ".99" - }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.Length.ShouldBe(2); - - result.Value.First().ProductId.ShouldBe("Jay"); - result.Value.First().Price.ShouldBe(100.00); - result.Value.First().HowMega.ShouldBe(1.01); - - result.Value.Second().ProductId.ShouldBe("Silent Bob"); - result.Value.Second().Price.ShouldBe(1000.00); - result.Value.Second().HowMega.ShouldBe(0.99); - } - - [Fact] - public void ShouldPopulateSimpleTypeConstructorParameterFromUntypedEntry() - { - var guid = Guid.NewGuid(); - var source = new Dictionary { ["Value"] = guid.ToString() }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBe(guid); - } - - [Fact] - public void ShouldPopulateComplexTypeAndSimpleTypeArrayConstructorParametersFromUntypedDottedEntries() - { - var now = DateTime.Now; - var nowString = now.ToCurrentCultureString(); - var inTenMinutes = now.AddMinutes(10).ToCurrentCultureString(); - var inTwentyMinutes = now.AddMinutes(20).ToCurrentCultureString(); - - var source = new Dictionary - { - ["Value1.ProductId"] = "Boom", - ["Value1.Price"] = "1.99", - ["Value1.HowMega"] = "1.00", - ["Value2[0]"] = nowString, - ["Value2[1]"] = inTenMinutes, - ["Value2[2]"] = inTwentyMinutes - }; - var result = Mapper.Map(source).ToANew>(); - - result.Value1.ProductId.ShouldBe("Boom"); - result.Value1.Price.ShouldBe(1.99); - result.Value1.HowMega.ShouldBe(1.00); - - result.Value2.ShouldBe(d => d.ToCurrentCultureString(), nowString, inTenMinutes, inTwentyMinutes); - } - - [Fact] - public void ShouldPopulateDeepNestedComplexTypeMembersFromUntypedDottedEntries() - { - var source = new Dictionary - { - ["Value[0].Value.SetValue[0].Title"] = "Mr", - ["Value[0].Value.SetValue[0].Name"] = "Franks", - ["Value[0].Value.SetValue[0].Address.Line1"] = "Somewhere", - ["Value[0].Value.SetValue[0].Address.Line2"] = "Over the rainbow", - ["Value[0].Value.SetValue[1]"] = new PersonViewModel { Name = "Mike", AddressLine1 = "La la la" }, - ["Value[0].Value.SetValue[2].Title"] = 5, - ["Value[0].Value.SetValue[2].Name"] = "Wilkes", - ["Value[0].Value.SetValue[2].Address.Line1"] = "Over there", - ["Value[1].Value.SetValue[0].Title"] = 737328, - ["Value[1].Value.SetValue[0].Name"] = "Rob", - ["Value[1].Value.SetValue[0].Address.Line1"] = "Some place" - }; - - var result = Mapper - .Map(source) - .ToANew>>>>(); - - result.Value.Count.ShouldBe(2); - - result.Value.First().Value.Value.Length.ShouldBe(3); - result.Value.Second().Value.Value.Length.ShouldBe(1); - - result.Value.First().Value.Value.First().Title.ShouldBe(Title.Mr); - result.Value.First().Value.Value.First().Name.ShouldBe("Franks"); - result.Value.First().Value.Value.First().Address.Line1.ShouldBe("Somewhere"); - result.Value.First().Value.Value.First().Address.Line2.ShouldBe("Over the rainbow"); - - result.Value.First().Value.Value.Second().Title.ShouldBeDefault(); - result.Value.First().Value.Value.Second().Name.ShouldBe("Mike"); - result.Value.First().Value.Value.Second().Address.Line1.ShouldBe("La la la"); - result.Value.First().Value.Value.Second().Address.Line2.ShouldBeDefault(); - - result.Value.First().Value.Value.Third().Title.ShouldBe(Title.Mrs); - result.Value.First().Value.Value.Third().Name.ShouldBe("Wilkes"); - result.Value.First().Value.Value.Third().Address.Line1.ShouldBe("Over there"); - result.Value.First().Value.Value.Third().Address.Line2.ShouldBeDefault(); - - result.Value.Second().Value.Value.First().Title.ShouldBeDefault(); - result.Value.Second().Value.Value.First().Name.ShouldBe("Rob"); - result.Value.Second().Value.Value.First().Address.Line1.ShouldBe("Some place"); - result.Value.Second().Value.Value.First().Address.Line2.ShouldBeDefault(); - } - - [Fact] - public void ShouldIgnoreANonStringKeyedDictionary() - { - var source = new Dictionary { [123] = 456 }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBeDefault(); - } - - [Fact] - public void ShouldHandleAnUnparseableStringValue() - { - var source = new Dictionary { ["Value"] = "jkdekml" }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBeDefault(); - } - - [Fact] - public void ShouldHandleANullObjectValue() - { - var source = new Dictionary { ["Value"] = null }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBeDefault(); - } - - [Fact] - public void ShouldIgnoreADeclaredUnconvertibleValueType() - { - var source = new Dictionary { ["Value"] = new byte[0] }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBeDefault(); - } - - [Fact] - public void ShouldHandleAnUnconvertibleValueForASimpleType() - { - var source = new Dictionary { ["Value"] = new object() }; - var result = Mapper.Map(source).ToANew>(); - - result.Value.ShouldBeNull(); - } - - [Fact] - public void ShouldHandleAnUnconvertibleValueForACollection() - { - var source = new Dictionary { ["Value"] = new Person { Name = "Nope" } }; - var result = Mapper.Map(source).ToANew>>(); - - result.Value.ShouldBeEmpty(); - } - - [Fact] - public void ShouldHandleAMappingException() - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .To
() - .CreateInstancesUsing(ctx => new Address { Line1 = int.Parse("rstgerfed").ToString() }); - - var source = new Dictionary - { - ["Line1"] = "La la la", - ["Line2"] = "La la la" - }; - - var mappingEx = Should.Throw(() => mapper.Map(source).ToANew
()); - - mappingEx.Message.ShouldContain("Dictionary -> Address"); - } - } - } -} diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaries.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaries.cs index 5c5d76889..2072b35af 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaries.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaries.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaryMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaryMembers.cs index 6f2c251a9..779a707a8 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaryMembers.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingOverDictionaryMembers.cs @@ -8,7 +8,7 @@ public class WhenMappingOverDictionaryMembers { [Fact] - public void ShouldOverwriteANestedSimpleTypedIDictionary() + public void ShouldOverwriteASimpleTypedIDictionary() { var source = new PublicField
{ @@ -25,7 +25,7 @@ public void ShouldOverwriteANestedSimpleTypedIDictionary() } [Fact] - public void ShouldOverwriteAComplexTypeArrayToANestedSameComplexTypeDictionary() + public void ShouldOverwriteAComplexTypeArrayToASameComplexTypeDictionary() { var source = new PublicField { @@ -100,11 +100,11 @@ public void ShouldMapADictionaryMemberOverADictionaryMember() target.Value.ShouldBeSameAs(existingTarget); - target.Value.ContainsKey("One!").ShouldBeTrue(); + target.Value.ShouldContainKey("One!"); target.Value["One!"].ShouldBeOfType(); ((PersonViewModel)target.Value["One!"]).Name.ShouldBe("One!"); - target.Value.ContainsKey("Two!").ShouldBeTrue(); + target.Value.ShouldContainKey("Two!"); target.Value["Two!"].ShouldBeOfType(); ((PersonViewModel)target.Value["Two!"]).Name.ShouldBe("Two!"); } diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaries.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaries.cs index 5eb63865c..13abd7016 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaries.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaries.cs @@ -34,7 +34,7 @@ public void ShouldMapAComplexTypeMemberToATypedDictionary() var source = new PublicProperty { Value = new Product { ProductId = "xxx" } }; var result = Mapper.Map(source).ToANew>(); - result.ContainsKey("Value").ShouldBeTrue(); + result.ShouldContainKey("Value"); result["Value"].ShouldBeOfType(); } @@ -58,10 +58,24 @@ public void ShouldMapNestedSimpleTypeMembersToATypedDictionary() var result = Mapper.Map(source).ToANew>(); result["Name"].ShouldBe("Eddie"); + result.ShouldNotContainKey("Address"); result["Address.Line1"].ShouldBe("Customer house"); result["Address.Line2"].ShouldBeNull(); } + [Fact] + public void ShouldMapNestedSimpleTypeMembersToAnUntypedDictionary() + { + var source = new PublicProperty> + { + Value = new PublicField { Value = 12345 } + }; + var result = Mapper.Map(source).ToANew>(); + + result.ShouldNotContainKey("Value"); + result["Value.Value"].ShouldBe(12345); + } + [Fact] public void ShouldMapASimpleTypeArrayToAnUntypedDictionary() { @@ -98,7 +112,7 @@ public void ShouldMapAComplexTypeCollectionToAnUntypedDictionary() var result = Mapper.Map(source).ToANew>(); result.Count.ShouldBe(4); - result.ContainsKey("[0]").ShouldBeFalse(); + result.ShouldNotContainKey("[0]"); result["[0].Line1"].ShouldBe("LOL"); result["[0].Line2"].ShouldBeNull(); @@ -122,18 +136,18 @@ public void ShouldMapNestedComplexAndSimpleTypeEnumerablesToAnUntypedDictionary( }; var result = Mapper.Map(source).ToANew>(); - result.ContainsKey("Value1").ShouldBeFalse(); - result.ContainsKey("Value2").ShouldBeFalse(); + result.ShouldNotContainKey("Value1"); + result.ShouldNotContainKey("Value2"); result["Value1[0].Name"].ShouldBe("Clare"); - result.ContainsKey("Value1[0].Address").ShouldBeFalse(); + result.ShouldNotContainKey("Value1[0].Address"); result["Value1[0].Address.Line1"].ShouldBe("Nes"); result["Value1[0].Address.Line2"].ShouldBe("Ted"); result["Value1[1].Name"].ShouldBe("Jim"); - result.ContainsKey("Value1[1].Address").ShouldBeFalse(); - result.ContainsKey("Value1[1].Address.Line1").ShouldBeFalse(); - result.ContainsKey("Value1[1].Address.Line2").ShouldBeFalse(); + result.ShouldNotContainKey("Value1[1].Address"); + result.ShouldNotContainKey("Value1[1].Address.Line1"); + result.ShouldNotContainKey("Value1[1].Address.Line2"); result["Value2[0]"].ShouldBe(now.AddMinutes(1)); result["Value2[1]"].ShouldBe(now.AddMinutes(2)); @@ -250,11 +264,11 @@ public void ShouldMapToASimpleTypeDictionaryImplementation() result.Count.ShouldBe(3); - result.ContainsKey("[0]").ShouldBeTrue(); + result.ShouldContainKey("[0]"); result["[0]"].ShouldBe("Hello"); - result.ContainsKey("[1]").ShouldBeTrue(); + result.ShouldContainKey("[1]"); result["[1]"].ShouldBe("Goodbye"); - result.ContainsKey("[2]").ShouldBeTrue(); + result.ShouldContainKey("[2]"); result["[2]"].ShouldBe("See ya"); } @@ -271,13 +285,13 @@ public void ShouldMapFromASimpleTypeDictionaryImplementationToAnIDictionary() result.Count.ShouldBe(3); - result.ContainsKey("One").ShouldBeTrue(); + result.ShouldContainKey("One"); result["One"].ShouldBe("One!"); - result.ContainsKey("Two").ShouldBeTrue(); + result.ShouldContainKey("Two"); result["Two"].ShouldBe("Two!"); - result.ContainsKey("Three").ShouldBeTrue(); + result.ShouldContainKey("Three"); result["Three"].ShouldBe("Three!"); } @@ -293,10 +307,10 @@ public void ShouldMapBetweenSameDeclaredSimpleTypeIDictionaries() result.Count.ShouldBe(2); - result.ContainsKey("Hello").ShouldBeTrue(); + result.ShouldContainKey("Hello"); result["Hello"].ShouldBe("Bonjour"); - result.ContainsKey("Yes").ShouldBeTrue(); + result.ShouldContainKey("Yes"); result["Yes"].ShouldBe("Oui"); } @@ -313,18 +327,58 @@ public void ShouldMapBetweenSameComplexTypeDictionaryImplementations() result.Count.ShouldBe(3); - result.ContainsKey("One").ShouldBeTrue(); + result.ShouldContainKey("One"); result["One"].Line1.ShouldBe("1.1"); result["One"].Line2.ShouldBe("1.2"); - result.ContainsKey("Two").ShouldBeTrue(); + result.ShouldContainKey("Two"); result["Two"].Line1.ShouldBe("2.1"); result["Two"].Line2.ShouldBe("2.2"); - result.ContainsKey("Three").ShouldBeTrue(); + result.ShouldContainKey("Three"); result["Three"].ShouldBeNull(); } + [Fact] + public void ShouldFlattenToValueTypes() + { + var source = new + { + Name = "Fred", + Array = new[] { 1, 2, 3 }, + ComplexList = new List>> + { + new PublicTwoFields> + { + Value1 = new byte[] { 4, 8, 16 }, + Value2 = new PublicField { Value = 456 } + }, + new PublicTwoFields> + { + Value1 = default(byte[]), + Value2 = new PublicField { Value = 789 } + } + } + }; + + var anonResult = Mapper.Map(source).ToANew>(); + + // String members won't be mapped because they're not value types + anonResult.ShouldNotContainKey("Name"); + anonResult["Array[0]"].ShouldBe(1); + anonResult["Array[1]"].ShouldBe(2); + anonResult["Array[2]"].ShouldBe(3); + + anonResult["ComplexList[0].Value1[0]"].ShouldBe(4); + anonResult["ComplexList[0].Value1[1]"].ShouldBe(8); + anonResult["ComplexList[0].Value1[2]"].ShouldBe(16); + anonResult["ComplexList[0].Value2.Value"].ShouldBe(456); + + anonResult.ShouldNotContainKey("ComplexList[1].Value1"); + anonResult.ShouldNotContainKey("ComplexList[1].Value1[0]"); + anonResult["ComplexList[1].Value2.Value"].ShouldBe(789); + } + [Fact] public void ShouldHandleANullComplexTypeMember() { @@ -332,8 +386,8 @@ public void ShouldHandleANullComplexTypeMember() var result = Mapper.Map(source).ToANew>(); result["Name"].ShouldBe("Richie"); - result.ContainsKey("Address.Line1").ShouldBeFalse(); - result.ContainsKey("Address.Line2").ShouldBeFalse(); + result.ShouldNotContainKey("Address.Line1"); + result.ShouldNotContainKey("Address.Line2"); } [Fact] diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs index 32f16599d..caf86a7b5 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs @@ -25,7 +25,7 @@ public void ShouldMapNestedSimpleTypeMembersToANestedUntypedDictionary() result.Value.ShouldNotBeNull(); result.Value.ShouldNotBeEmpty(); result.Value["Name"].ShouldBe("Someone"); - result.Value.ContainsKey("Address").ShouldBeFalse(); + result.Value.ShouldNotContainKey("Address"); result.Value["Address.Line1"].ShouldBe("Some Place"); } @@ -55,15 +55,15 @@ public void ShouldMapANestedComplexTypeArrayToANestedTypedDictionary() }; var result = Mapper.Map(source).ToANew>>(); - result.Value.ContainsKey("[0]").ShouldBeFalse(); + result.Value.ShouldNotContainKey("[0]"); result.Value["[0].Name"].ShouldBeDefault(); - result.Value.ContainsKey("[0].Id").ShouldBeFalse(); // <- because id is a Guid, which can't be parsed to a decimal + result.Value.ShouldNotContainKey("[0].Id"); // <- because id is a Guid, which can't be parsed to a decimal result.Value["[0].AddressLine1"].ShouldBeDefault(); result.Value["[0].Discount"].ShouldBe(0.5); result.Value["[1].Name"].ShouldBeDefault(); - result.Value.ContainsKey("[1].Id").ShouldBeFalse(); + result.Value.ShouldNotContainKey("[1].Id"); result.Value["[1].AddressLine1"].ShouldBeDefault(); result.Value["[1].Discount"].ShouldBe(0.6); result.Value["[1].Report"].ShouldBe(0.075); @@ -82,7 +82,7 @@ public void ShouldFlattenANestedArrayOfArraysToANestedTypedDictionary() }; var result = Mapper.Map(source).ToANew>>(); - result.Value.ContainsKey("[0]").ShouldBeFalse(); + result.Value.ShouldNotContainKey("[0]"); result.Value["[0][0]"].ShouldBe(1.0); result.Value["[0][1]"].ShouldBe(2.0); @@ -105,7 +105,7 @@ public void ShouldMapANestedEnumerableOfArraysToANestedEnumerableTypedDictionary }; var result = Mapper.Map(source).ToANew>>>(); - result.Value.ContainsKey("[0][0]").ShouldBeFalse(); + result.Value.ShouldNotContainKey("[0][0]"); result.Value["[0]"].ShouldBe("1", "2", "3"); result.Value["[1]"].ShouldBe("4", "5", "6"); @@ -171,10 +171,10 @@ public void ShouldMapADictionaryObjectValuesToNewDictionaryObjectValues() var result = Mapper.Map(source).ToANew>>(); result.Value.ShouldNotBeNull(); - result.Value.ContainsKey("key1").ShouldBeTrue(); + result.Value.ShouldContainKey("key1"); result.Value["key1"].ShouldBeOfType(); result.Value["key1"].ShouldNotBeSameAs(source.Value["key1"]); - result.Value.ContainsKey("key2").ShouldBeTrue(); + result.Value.ShouldContainKey("key2"); result.Value["key2"].ShouldBeOfType(); result.Value["key2"].ShouldNotBeSameAs(source.Value["key2"]); } @@ -215,7 +215,7 @@ public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplemen result.Value["[0].Id"].ShouldBe(source.Value.First().Id); result.Value["[0].Title"].ShouldBe(Title.Count); result.Value["[0].Name"].ShouldBe("Customer 1"); - result.Value.ContainsKey("[0].Address").ShouldBeFalse(); + result.Value.ShouldNotContainKey("[0].Address"); result.Value["[0].Address.Line1"].ShouldBe("This place"); result.Value["[0].Address.Line2"].ShouldBe("That place"); @@ -224,9 +224,9 @@ public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplemen 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.ContainsKey("[1].Address").ShouldBeFalse(); - result.Value.ContainsKey("[1].Address.Line1").ShouldBeFalse(); - result.Value.ContainsKey("[1].Address.Line2").ShouldBeFalse(); + result.Value.ShouldNotContainKey("[1].Address"); + result.Value.ShouldNotContainKey("[1].Address.Line1"); + result.Value.ShouldNotContainKey("[1].Address.Line2"); } } diff --git a/AgileMapper.UnitTests/Dictionaries/WhenViewingDictionaryMappingPlans.cs b/AgileMapper.UnitTests/Dictionaries/WhenViewingDictionaryMappingPlans.cs index b75b50fe2..f6f91557b 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenViewingDictionaryMappingPlans.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenViewingDictionaryMappingPlans.cs @@ -16,7 +16,7 @@ public void ShouldShowATargetObjectMappingPlan() .ToANew(); plan.ShouldContain("Dictionary sourceDictionary_String_String"); - plan.ShouldContain("idKey = sourceDictionary_String_String.Keys.FirstOrDefault(key => string.Equals(key, \"Id\""); + plan.ShouldContain("idKey = sourceDictionary_String_String.Keys.FirstOrDefault(key => key.MatchesKey(\"Id\""); plan.ShouldContain("id = sourceDictionary_String_String[idKey]"); plan.ShouldContain("customerViewModel.Id ="); plan.ShouldContain("Guid.TryParse(id"); diff --git a/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringDynamicMappingIncorrectly.cs b/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringDynamicMappingIncorrectly.cs new file mode 100644 index 000000000..698813b70 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringDynamicMappingIncorrectly.cs @@ -0,0 +1,66 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics.Configuration +{ + using AgileMapper.Configuration; + using Shouldly; + using Xunit; + + public class WhenConfiguringDynamicMappingIncorrectly + { + [Fact] + public void ShouldErrorIfRedundantSourceSeparatorIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .Dynamics + .UseMemberNameSeparator("-") + .AndWhenMapping + .FromDynamics + .UseMemberNameSeparator("-"); + } + }); + + configEx.Message.ShouldContain("already"); + configEx.Message.ShouldContain("global"); + configEx.Message.ShouldContain("'-'"); + } + + [Fact] + public void ShouldErrorIfRedundantGlobalSeparatorIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .Dynamics + .UseMemberNameSeparator("_"); + } + }); + + configEx.Message.ShouldContain("already"); + configEx.Message.ShouldContain("global"); + configEx.Message.ShouldContain("'_'"); + } + + [Fact] + public void ShouldErrorIfRedundantSourceElementKeyPartIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .UseElementKeyPattern("_i_"); + } + }); + + configEx.Message.ShouldContain("already"); + configEx.Message.ShouldContain("global"); + configEx.Message.ShouldContain("_i_"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringSourceDynamicMapping.cs b/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringSourceDynamicMapping.cs new file mode 100644 index 000000000..1db11a87a --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringSourceDynamicMapping.cs @@ -0,0 +1,292 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics.Configuration +{ + using System.Collections.Generic; + using System.Dynamic; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenConfiguringSourceDynamicMapping + { + [Fact] + public void ShouldUseACustomDynamicSourceMemberName() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .ToANew>() + .MapFullMemberName("LaLaLa") + .To(pf => pf.Value); + + dynamic source = new ExpandoObject(); + + source.LaLaLa = 1; + source.Value = 2; + + var result = (PublicField)mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBe(1); + + mapper.Map(source).Over(result); + + result.Value.ShouldBe(2); + } + } + + [Fact] + public void ShouldUseCustomDynamicMemberNameForRootMembers() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .Over
() + .MapMemberName("HouseNumber") + .To(a => a.Line1) + .And + .MapMemberName("Street") + .To(a => a.Line2); + + dynamic source = new ExpandoObject(); + + source.HouseNumber = 10; + source.Street = "Street Road"; + + var target = new Address { Line1 = "??", Line2 = "??" }; + + mapper.Map(source).Over(target); + + target.Line1.ShouldBe("10"); + target.Line2.ShouldBe("Street Road"); + } + } + + [Fact] + public void ShouldApplyACustomMemberNamePartsToASpecificTargetType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .OnTo
() + .MapMemberName("StreetName") + .To(a => a.Line1) + .And + .MapMemberName("CityName") + .To(a => a.Line2); + + dynamic source = new ExpandoObject(); + + source.Value_0__StreetName = "Street Zero"; + source.Value_0__CityName = "City Zero"; + source.Value_1__StreetName = "Street One"; + source.Value_1__CityName = "City One"; + + var target = new PublicField> { Value = new List
() }; + + mapper.Map(source).OnTo(target); + + target.Value.Count.ShouldBe(2); + + target.Value.First().Line1.ShouldBe("Street Zero"); + target.Value.First().Line2.ShouldBe("City Zero"); + + target.Value.Second().Line1.ShouldBe("Street One"); + target.Value.Second().Line2.ShouldBe("City One"); + } + } + + [Fact] + public void ShouldApplyCustomSeparators() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .Dynamics + .UseMemberNameSeparator("-") + .AndWhenMapping + .FromDynamics + .UseMemberNameSeparator("+"); + + var source = new[] + { + new PublicProperty { Value = 10 }, + new PublicProperty { Value = 20 }, + new PublicProperty { Value = 30 }, + }; + + dynamic targetDynamic = new ExpandoObject(); + + ((IDictionary)targetDynamic)["_0_-Value"] = 1; + ((IDictionary)targetDynamic)["_1_-Value"] = 2; + + var targetResult = (IDictionary)mapper.Map(source).Over(targetDynamic); + + targetResult.Count.ShouldBe(3); + + targetResult["_0_-Value"].ShouldBe(10); + targetResult["_1_-Value"].ShouldBe(20); + targetResult["_2_-Value"].ShouldBe(30); + + dynamic sourceDynamic = new ExpandoObject(); + + ((IDictionary)sourceDynamic)["Value+Value"] = 123; + + var sourceResult = (PublicField>)mapper.Map(sourceDynamic).ToANew>>(); + + sourceResult.Value.Value.ShouldBe(123); + } + } + + [Fact] + public void ShouldApplyACustomConfiguredMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .Over>() + .Map((d, pf) => d.Count) + .To(pf => pf.Value); + + dynamic source = new ExpandoObject(); + + source.One = 1; + source.Two = 2; + + var target = new PublicField(); + + mapper.Map(source).Over(target); + + target.Value.ShouldBe(2); + } + } + + [Fact] + public void ShouldConditionallyMapToDerivedTypes() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .ToANew() + .If(s => s.Source.ContainsKey("Discount")) + .MapTo() + .And + .If(s => s.Source.ContainsKey("Report")) + .MapTo(); + + dynamic source = new ExpandoObject(); + + source.Name = "Person"; + + var personResult = (PersonViewModel)mapper.Map(source).ToANew(); + + personResult.ShouldBeOfType(); + personResult.Name.ShouldBe("Person"); + + source.Discount = 0.05; + + var customerResult = (PersonViewModel)mapper.Map(source).ToANew(); + + customerResult.ShouldBeOfType(); + ((CustomerViewModel)customerResult).Discount.ShouldBe(0.05); + + source.Report = "Very good!"; + + var mysteryCustomerResult = (PersonViewModel)mapper.Map(source).ToANew(); + + mysteryCustomerResult.ShouldBeOfType(); + ((MysteryCustomerViewModel)mysteryCustomerResult).Report.ShouldBe("Very good!"); + } + } + + [Fact] + public void ShouldNotApplyDictionaryConfigurationToDynamics() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDictionariesWithValueType() + .To>() + .MapFullKey("LaLaLa") + .To(pf => pf.Value); + + dynamic source = new ExpandoObject(); + + source.LaLaLa = 1; + source.Value = 2; + + var result = (PublicField)mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBe("2"); + } + } + + [Fact] + public void ShouldNotApplyDynamicConfigurationToDictionaries() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .To>() + .MapFullMemberName("LaLaLa") + .To(pf => pf.Value); + + var source = new Dictionary + { + ["LaLaLa"] = 1, + ["Value"] = 2 + }; + + var result = mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBe("2"); + } + } + + [Fact] + public void ShouldNotConflictDynamicAndDictionaryConfiguration() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDictionariesWithValueType() + .UseMemberNameSeparator("-"); + + mapper.WhenMapping + .FromDynamics + .UseMemberNameSeparator("+"); + + var dictionarySource = new Dictionary + { + ["Value-Line1"] = "Line 1!", + ["Value-Line2"] = "Line 2!" + }; + + var dictionaryResult = mapper.Map(dictionarySource).ToANew>(); + + dictionaryResult.Value.ShouldNotBeNull(); + dictionaryResult.Value.Line1.ShouldBe("Line 1!"); + dictionaryResult.Value.Line2.ShouldBe("Line 2!"); + + dynamic dynamicSource = new ExpandoObject(); + + ((IDictionary)dynamicSource)["Value+Line1"] = "Line 1?!"; + ((IDictionary)dynamicSource)["Value+Line2"] = "Line 2?!"; + + var dynamicResult = (PublicField
)mapper.Map(dynamicSource).ToANew>(); + + dynamicResult.Value.ShouldNotBeNull(); + dynamicResult.Value.Line1.ShouldBe("Line 1?!"); + dynamicResult.Value.Line2.ShouldBe("Line 2?!"); + } + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringTargetDynamicMapping.cs b/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringTargetDynamicMapping.cs new file mode 100644 index 000000000..2d0bff828 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/Configuration/WhenConfiguringTargetDynamicMapping.cs @@ -0,0 +1,349 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics.Configuration +{ + using System; + using System.Collections.Generic; + using System.Dynamic; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenConfiguringTargetDynamicMapping + { + [Fact] + public void ShouldApplyFlattenedMemberNamesGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .Dynamics + .UseFlattenedTargetMemberNames(); + + var source = new MysteryCustomer + { + Id = Guid.NewGuid(), + Title = Title.Mr, + Name = "Paul", + Discount = 0.25m, + Report = "Naah nah nah na-na-na NAAAAAAAHHHH", + Address = new Address { Line1 = "Abbey Road", Line2 = "Penny Lane" } + }; + var result = mapper.Map(source).ToANew(); + + ((Guid)result.Id).ShouldBe(source.Id); + ((Title)result.Title).ShouldBe(Title.Mr); + ((string)result.Name).ShouldBe("Paul"); + ((decimal)result.Discount).ShouldBe(0.25m); + ((string)result.AddressLine1).ShouldBe("Abbey Road"); + ((string)result.AddressLine2).ShouldBe("Penny Lane"); + } + } + + [Fact] + public void ShouldNotApplySourceOnlyConfigurationToTargetDynamics() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .FromDynamics + .UseFlattenedTargetMemberNames(); + + var source = new Customer + { + Name = "Paul", + Address = new Address { Line1 = "Abbey Road", Line2 = "Penny Lane" } + }; + + dynamic target = new ExpandoObject(); + + target.Name = "Ringo"; + + mapper.Map(source).OnTo(target); + + ((string)target.Name).ShouldBe("Ringo"); + ((string)target.Address_Line1).ShouldBe("Abbey Road"); + ((string)target.Address_Line2).ShouldBe("Penny Lane"); + } + } + + [Fact] + public void ShouldApplyFlattenedMemberNamesToASpecificSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToDynamics + .UseFlattenedMemberNames() + .And + .MapMember(pf => pf.Value) + .ToMemberName("Data"); + + var matchingSource = new PublicField
+ { + Value = new Address { Line1 = "As a pancake" } + }; + var matchingResult = mapper.Map(matchingSource).ToANew(); + + ((IDictionary)matchingResult).Keys.Any(k => k.StartsWith("Value")).ShouldBeFalse(); + ((string)matchingResult.DataLine1).ShouldBe("As a pancake"); + ((string)matchingResult.DataLine2).ShouldBeNull(); + + var nonMatchingSource = new PublicProperty
+ { + Value = new Address { Line1 = "Like a flatfish" } + }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew(); + + ((string)nonMatchingResult.Value_Line1).ShouldBe("Like a flatfish"); + ((string)nonMatchingResult.Value_Line2).ShouldBeNull(); + } + } + + [Fact] + public void ShouldApplyACustomSeparatorToASpecificSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToDynamics + .UseMemberNameSeparator("!") + .And + .MapMember(c => c.Address.Line1) + .ToFullMemberName("StreetAddress"); + + var address = new Address { Line1 = "Paddy's", Line2 = "Philly" }; + var matchingSource = new MysteryCustomer { Address = address }; + var matchingResult = (IDictionary)mapper.Map(matchingSource).ToANew(); + + matchingResult["StreetAddress"].ShouldBe("Paddy's"); + matchingResult["Address!Line2"].ShouldBe("Philly"); + matchingResult.ShouldNotContainKey("Address_Line1"); + matchingResult.ShouldNotContainKey("Address!Line1"); + + var nonMatchingSource = new Customer { Address = address }; + var nonMatchingSourceResult = (IDictionary)mapper.Map(nonMatchingSource).ToANew(); + + nonMatchingSourceResult["Address_Line1"].ShouldBe("Paddy's"); + + var nonMatchingTargetResult = (IDictionary)mapper.Map(nonMatchingSource).ToANew(); + + nonMatchingTargetResult["Address_Line1"].ShouldBe("Paddy's"); + } + } + + [Fact] + public void ShouldApplyACustomEnumerableElementPatternToASpecificDerivedSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToDynamics + .UseElementKeyPattern("(i)"); + + var source = new PublicProperty> + { + Value = new List + { + new Person { Name = "Sandra", Address = new Address { Line1 = "Home" } }, + new Customer { Name = "David", Address = new Address { Line1 = "Home!" } } + } + }; + var originalExpando = new ExpandoObject(); + var target = new PublicField { Value = originalExpando }; + + var result = (IDictionary)mapper.Map(source).OnTo(target).Value; + + result.ShouldBeSameAs(originalExpando); + + result["_0__Name"].ShouldBe("Sandra"); + result["_0__Address_Line1"].ShouldBe("Home"); + + result["(1)_Name"].ShouldBe("David"); + result["(1)_Address_Line1"].ShouldBe("Home!"); + } + } + + [Fact] + public void ShouldApplyAConfiguredConditionalTargetEntryValue() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToDynamics + .If((mcvm, d) => mcvm.Discount > 0.5) + .Map((mcvm, d) => mcvm.Name + " (Big discount!)") + .To(d => d["Name"]); + + var noDiscountSource = new MysteryCustomerViewModel { Name = "Schumer", Discount = 0.0 }; + var noDiscountResult = (IDictionary)mapper.Map(noDiscountSource).ToANew(); + + noDiscountResult["Name"].ShouldBe("Schumer"); + + var bigDiscountSource = new MysteryCustomerViewModel { Name = "Silverman", Discount = 0.6 }; + var bigDiscountResult = (IDictionary)mapper.Map(bigDiscountSource).ToANew(); + + bigDiscountResult["Name"].ShouldBe("Silverman (Big discount!)"); + } + } + + [Fact] + public void ShouldAllowACustomTargetEntryKey() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToDynamics + .MapMember(mcvm => mcvm.Name) + .ToFullMemberName("CustomerName") + .And + .If((mcvm, d) => mcvm.Discount > 0.5) + .Map((mcvm, d) => mcvm.Name + " (Big discount!)") + .To(d => d["Name"]); + + var noDiscountSource = new MysteryCustomerViewModel { Name = "Schumer", Discount = 0.0 }; + var noDiscountResult = (IDictionary)mapper.Map(noDiscountSource).ToANew(); + + noDiscountResult["CustomerName"].ShouldBe("Schumer"); + noDiscountResult.ShouldNotContainKey("Name"); + + var bigDiscountSource = new MysteryCustomerViewModel { Name = "Silverman", Discount = 0.6 }; + var bigDiscountResult = (IDictionary)mapper.Map(bigDiscountSource).ToANew(); + + bigDiscountResult["CustomerName"].ShouldBe("Silverman"); + bigDiscountResult["Name"].ShouldBe("Silverman (Big discount!)"); + } + } + + [Fact] + public void ShouldApplyACustomConfiguredMemberConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From
() + .ToDynamics + .If(ctx => !string.IsNullOrEmpty(ctx.Source.Line2)) + .Map(ctx => "Present") + .To(d => d["Line2State"]) + .But + .If(ctx => string.IsNullOrEmpty(ctx.Source.Line2)) + .Map(ctx => "Missing") + .To(d => d["Line2State"]); + + var line2Source = new Address { Line1 = "Line 1: Yes", Line2 = "Line 2: Yes!" }; + var line2Result = (IDictionary)mapper.Map(line2Source).ToANew(); + + line2Result["Line2State"].ShouldBe("Present"); + line2Result["Line2"].ShouldBe("Line 2: Yes!"); + + var noLine2Source = new Address { Line1 = "Line 1: Yes", Line2 = string.Empty }; + var noLine2Result = (IDictionary)mapper.Map(noLine2Source).ToANew(); + + noLine2Result["Line2State"].ShouldBe("Missing"); + } + } + + [Fact] + public void ShouldNotApplyTargetDictionaryConfigurationToDynamics() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToDictionaries + .Map((pf, d) => pf.Value) + .To(d => d["Name"]); + + var source = new PublicField { Value = "LaLaLa" }; + + dynamic target = new ExpandoObject(); + + target.Name = "Doodeedoo"; + + mapper.Map(source).Over(target); + + ((object)target).ShouldNotBeNull(); + ((string)target.Value).ShouldBe("LaLaLa"); + ((string)target.Name).ShouldBe("Doodeedoo"); + } + } + + [Fact] + public void ShouldNotApplyTargetDynamicConfigurationToDictionaries() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToDynamics + .Map(ctx => ctx.Source.Value) + .To(d => d["Name"]); + + var source = new PublicField + { + Value = 123 + }; + + var target = new Dictionary + { + ["Name"] = 1, + ["Value"] = 2 + }; + + mapper.Map(source).Over(target); + + target["Name"].ShouldBe(1); + target["Value"].ShouldBe(123); + } + } + + [Fact] + public void ShouldNotConflictTargetDynamicAndDictionaryConfiguration() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToDictionaries + .UseMemberNameSeparator("-"); + + mapper.WhenMapping + .From>() + .ToDynamics + .UseMemberNameSeparator("+"); + + var source = new PublicProperty
+ { + Value = new Address { Line1 = "L1", Line2 = "L2" } + }; + + var dictionaryTarget = new Dictionary + { + ["Value-Line1"] = "Line 1!", + ["Value-Line2"] = "Line 2!" + }; + + mapper.Map(source).Over(dictionaryTarget); + + dictionaryTarget["Value-Line1"].ShouldBe("L1"); + dictionaryTarget["Value-Line2"].ShouldBe("L2"); + + dynamic dynamicTarget = new ExpandoObject(); + var targetDynamicDictionary = (IDictionary)dynamicTarget; + + targetDynamicDictionary["Value+Line1"] = "Line 1?!"; + targetDynamicDictionary["Value+Line2"] = "Line 2?!"; + + mapper.Map(source).Over(dynamicTarget); + + targetDynamicDictionary["Value+Line1"].ShouldBe("L1"); + targetDynamicDictionary["Value+Line2"].ShouldBe("L2"); + } + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverComplexTypeMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverComplexTypeMembers.cs new file mode 100644 index 000000000..aa7021cf3 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverComplexTypeMembers.cs @@ -0,0 +1,72 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsOverComplexTypeMembers + { + [Fact] + public void ShouldMapFromANestedDynamicToANestedComplexType() + { + dynamic sourceDynamic = new ExpandoObject(); + + sourceDynamic.Line1 = "Over there"; + + var source = new PublicTwoFields + { + Value1 = sourceDynamic, + Value2 = "Good Googley Moogley!" + }; + + var target = new PublicTwoFields + { + Value1 = new Address { Line1 = "Over here", Line2 = "Somewhere else" }, + Value2 = "Nothing" + }; + + var preMappingAddress = target.Value1; + + Mapper.Map(source).Over(target); + + target.Value1.ShouldBeSameAs(preMappingAddress); + target.Value1.Line1.ShouldBe("Over there"); + target.Value1.Line2.ShouldBe("Somewhere else"); + target.Value2.ShouldBe("Good Googley Moogley!"); + } + + [Fact] + public void ShouldMapFlattenedMembersFromANestedDynamicToANestedComplexType() + { + dynamic sourceDynamic = new ExpandoObject(); + + sourceDynamic.Name = "Mystery :o"; + sourceDynamic.AddressLine1 = "Over here"; + sourceDynamic.AddressLine2 = "Over there"; + + var source = new PublicTwoFields + { + Value1 = sourceDynamic, + Value2 = "Blimey!!" + }; + + var target = new PublicTwoFields + { + Value1 = new MysteryCustomer { Name = "Mystery?!" }, + Value2 = "Nowt" + }; + + var preMappingCustomer = target.Value1; + + Mapper.Map(source).Over(target); + + target.Value1.ShouldBeSameAs(preMappingCustomer); + target.Value1.Name.ShouldBe("Mystery :o"); + target.Value1.Address.ShouldNotBeNull(); + target.Value1.Address.Line1.ShouldBe("Over here"); + target.Value1.Address.Line2.ShouldBe("Over there"); + target.Value2.ShouldBe("Blimey!!"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverComplexTypes.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverComplexTypes.cs new file mode 100644 index 000000000..fb9c74f29 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverComplexTypes.cs @@ -0,0 +1,26 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsOverComplexTypes + { + [Fact] + public void ShouldOverwriteComplexTypeMembers() + { + dynamic source = new ExpandoObject(); + + source.Line1 = "Up there!"; + source.Line2 = default(string); + + var target = new Address { Line2 = "Up where?!" }; + + Mapper.Map(source).Over(target); + + target.Line1.ShouldBe("Up there!"); + target.Line2.ShouldBeNull(); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverEnumerableMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverEnumerableMembers.cs new file mode 100644 index 000000000..c94e3e676 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverEnumerableMembers.cs @@ -0,0 +1,101 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Dynamic; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsOverEnumerableMembers + { + [Fact] + public void ShouldOverwriteASimpleTypeCollection() + { + dynamic source = new ExpandoObject(); + + source.Value = new List { 10, 20, 30 }; + + var target = new PublicField> + { + Value = new List { "40" } + }; + + Mapper.Map(source).Over(target); + + target.Value.ShouldBe("10", "20", "30"); + } + + [Fact] + public void ShouldOverwriteAComplexTypeCollectionToEmpty() + { + dynamic source = new ExpandoObject(); + + source.Value = default(List); + + var target = new PublicField> + { + Value = new List { new ProductDto { ProductId = "p-1" } } + }; + + Mapper.Map(source).Over(target); + + target.Value.ShouldBeEmpty(); + } + + [Fact] + public void ShouldOverwriteAnIndentifableComplexTypeCollection() + { + dynamic source = new ExpandoObject(); + + source.Value = new List + { + new ProductDto { ProductId = "prod-1", Price = 12.99m }, + new ProductDto { ProductId = "prod-2", Price = 15.00m }, + }; + + var target = new PublicField> + { + Value = new List + { + new Product { ProductId = "prod-2", Price = 20.00 }, + new Product { ProductId = "prod-3", Price = 1.99 }, + } + }; + + var preMappingProd2 = target.Value.First(); + + Mapper.Map(source).Over(target); + + target.Value.Count.ShouldBe(2); + + target.Value.First().ShouldBeSameAs(preMappingProd2); + target.Value.ShouldBe(p => p.ProductId, "prod-2", "prod-1"); + target.Value.ShouldBe(p => p.Price, 15.00, 12.99); + } + + [Fact] + public void ShouldOverwriteAComplexTypeCollectionFromElementEntries() + { + dynamic source = new ExpandoObject(); + + source.Value_0_ = new PublicField { Value = "Value 0" }; + source.Value_1_ = new PublicField { Value = "Value 1" }; + + var target = new PublicField>> + { + Value = new Collection> + { + new PublicField { Value = "Value 1" }, + new PublicField { Value = "Value 2" }, + } + }; + + Mapper.Map(source).Over(target); + + target.Value.Count.ShouldBe(2); + target.Value.ShouldBe(pf => pf.Value, "Value 0", "Value 1"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverEnumerables.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverEnumerables.cs new file mode 100644 index 000000000..bf64855e1 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsOverEnumerables.cs @@ -0,0 +1,67 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Collections.Generic; + using System.Dynamic; + using System.Linq; + using AgileMapper.Extensions.Internal; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsOverEnumerables + { + [Fact] + public void ShouldMapToASimpleTypeCollectionFromASourceArray() + { + dynamic source = new ExpandoObject(); + + source.Value = new long[] { 4, 5, 6 }; + + var target = new PublicProperty> + { + Value = new List { 2, 3 } + }; + + Mapper.Map(source).Over(target); + + target.Value.ShouldBe(4L, 5L, 6L); + } + + [Fact] + public void ShouldMapToAComplexTypeArrayFromAConvertibleTypedSourceEnumerable() + { + dynamic source = new ExpandoObject(); + + source.Value = new[] + { + new Person { Name = "Mr Pants"}, + new Customer { Name = "Mrs Blouse" } + }; + + var target = new PublicProperty(); + + Mapper.Map(source).Over(target); + + target.Value.Length.ShouldBe(2); + target.Value.First().Name.ShouldBe("Mr Pants"); + target.Value.Second().Name.ShouldBe("Mrs Blouse"); + } + + [Fact] + public void ShouldMapToAComplexTypeEnumerableFromFlattenedEntries() + { + dynamic source = new ExpandoObject(); + + source._0_ProductId = "Hose"; + source._0_Price = "1.99"; + + IEnumerable target = new List(); + + Mapper.Map(source).Over(target); + + target.ShouldHaveSingleItem(); + target.First().ProductId.ShouldBe("Hose"); + target.First().Price.ShouldBe(1.99); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewComplexTypeMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewComplexTypeMembers.cs new file mode 100644 index 000000000..3c410a19e --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewComplexTypeMembers.cs @@ -0,0 +1,63 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsToNewComplexTypeMembers + { + [Fact] + public void ShouldMapToNestedSimpleTypeMembers() + { + dynamic source = new ExpandoObject(); + source.Name = "Dynamic Customer!"; + source.Address = new Address + { + Line1 = "Dynamic Line 1!", + Line2 = "Dynamic Line 2!", + }; + + var result = (Customer)Mapper.Map(source).ToANew(); + + result.Name = "Dynamic Customer!"; + result.Address.ShouldNotBeNull(); + result.Address.Line1.ShouldBe("Dynamic Line 1!"); + result.Address.Line2.ShouldBe("Dynamic Line 2!"); + } + + [Fact] + public void ShouldMapFlattenedPropertiesToNestedSimpleTypeMembers() + { + dynamic source = new ExpandoObject(); + source.name = "Dynamic Person"; + source.addressLine1 = "Dynamic Line 1"; + source.addressLine2 = "Dynamic Line 2"; + + var result = (Person)Mapper.Map(source).ToANew(); + + result.Name = "Dynamic Person"; + result.Address.ShouldNotBeNull(); + result.Address.Line1.ShouldBe("Dynamic Line 1"); + result.Address.Line2.ShouldBe("Dynamic Line 2"); + } + + [Fact] + public void ShouldMapANestedDynamicToANestedComplexTypeMember() + { + dynamic source = new ExpandoObject(); + + source.Name = "Captain Dynamic"; + source.Address = new ExpandoObject(); + source.Address.Line1 = "Dynamic House"; + source.Address.Line2 = "Dynamic Street"; + + var result = (Customer)Mapper.Map(source).ToANew(); + + result.Name.ShouldBe("Captain Dynamic"); + result.Address.ShouldNotBeNull(); + result.Address.Line1.ShouldBe("Dynamic House"); + result.Address.Line2.ShouldBe("Dynamic Street"); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewComplexTypes.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewComplexTypes.cs new file mode 100644 index 000000000..e0f13f00c --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewComplexTypes.cs @@ -0,0 +1,67 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System; + using System.Dynamic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsToNewComplexTypes + { + [Fact] + public void ShouldMapToASimpleTypeMember() + { + dynamic source = new ExpandoObject(); + + source.value = 123; + + var result = (PublicField)Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(123); + } + + [Fact] + public void ShouldConvertASimpleTypeMemberValue() + { + dynamic source = new ExpandoObject(); + + source.Value = "728"; + + var result = (PublicField)Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(728L); + } + + [Fact] + public void ShouldHandleANullASimpleTypeMemberValue() + { + dynamic source = new ExpandoObject(); + + source.Value = default(string); + + var result = (PublicSetMethod)Mapper.Map(source).ToANew>(); + + result.Value.ShouldBeNull(); + } + + [Fact] + public void ShouldWrapAMappingException() + { + using (var mapper = Mapper.CreateNew()) + { + dynamic source = new ExpandoObject(); + + source.ValueLine1 = "1 Exception Road"; + + mapper.Before + .CreatingInstancesOf
() + .Call(ctx => throw new InvalidOperationException("I DON'T LIKE ADDRESSES")); + + var mappingEx = Should.Throw(() => + mapper.Map(source).ToANew>()); + + mappingEx.Message.ShouldContain(nameof(ExpandoObject)); + } + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewEnumerableMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewEnumerableMembers.cs new file mode 100644 index 000000000..c7322c573 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewEnumerableMembers.cs @@ -0,0 +1,72 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Collections.Generic; + using System.Dynamic; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsToNewEnumerableMembers + { + [Fact] + public void ShouldMapToASimpleTypeCollectionMember() + { + dynamic source = new ExpandoObject(); + source.Value = new[] { "a", "b", "c" }; + + var result = (PublicField)Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe('a', 'b', 'c'); + } + + [Fact] + public void ShouldMapToAComplexTypeEnumerableMember() + { + dynamic source = new ExpandoObject(); + + source.Value = new[] + { + new PublicField { Value = "1" }, + new PublicField { Value = "2" }, + new PublicField { Value = "3" } + }; + + var result = (PublicProperty>>)Mapper + .Map(source) + .ToANew>>>(); + + result.Value.ShouldBe(pf => pf.Value, 1, 2, 3); + } + + [Fact] + public void ShouldMapToAComplexTypeEnumerableMemberFromComplexTypeEntries() + { + dynamic source = new ExpandoObject(); + + source.Value_0_ = new PublicProperty { Value = '9' }; + source.Value_1_ = new PublicProperty { Value = '8' }; + source.Value_2_ = new PublicProperty { Value = '7' }; + + var result = (PublicField>>)Mapper + .Map(source) + .ToANew>>>(); + + result.Value.ShouldBe(pf => pf.Value, 9, 8, 7); + } + + [Fact] + public void ShouldMapToAComplexTypeEnumerableMemberFromFlattenedEntries() + { + dynamic source = new ExpandoObject(); + + source.Value_0_SetValue = '4'; + source.Value_1_SetValue = '5'; + source.Value_2_SetValue = '6'; + + var result = (PublicField>>)Mapper + .Map(source) + .ToANew>>>(); + + result.Value.ShouldBe(pf => pf.Value, 4, 5, 6); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewEnumerables.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewEnumerables.cs new file mode 100644 index 000000000..7af48ff15 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingFromDynamicsToNewEnumerables.cs @@ -0,0 +1,64 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Dynamic; + using TestClasses; + using Xunit; + + public class WhenMappingFromDynamicsToNewEnumerables + { + [Fact] + public void ShouldMapToASimpleTypeArray() + { + dynamic source = new ExpandoObject(); + + source._0_ = 'a'; + source._1_ = 'b'; + source._2_ = 'c'; + + var result = (string[])Mapper.Map(source).ToANew(); + + result.ShouldBe("a", "b", "c"); + } + + [Fact] + public void ShouldMapToAComplexTypeCollectionFromComplexTypeEntries() + { + dynamic source = new ExpandoObject(); + + source._0_ = new ProductDto { ProductId = "prod-one" }; + source._1_ = new ProductDto { ProductId = "prod-two" }; + source._2_ = new ProductDto { ProductId = "prod-three" }; + + var result = (Collection)Mapper.Map(source).ToANew>(); + + result.ShouldBe(p => p.ProductId, "prod-one", "prod-two", "prod-three"); + } + + [Fact] + public void ShouldMapToAComplexTypeListFromFlattenedEntries() + { + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + var guid3 = Guid.NewGuid(); + + dynamic source = new ExpandoObject(); + + source._0_Value1 = guid1; + source._0_Value2 = 123; + source._1_Value1 = guid2; + source._1_Value2 = 456; + source._2_Value1 = guid3; + source._2_Value2 = 789; + + var result = (IList>)Mapper + .Map(source) + .ToANew>>(); + + result.ShouldBe(p => p.Value1, guid1.ToString(), guid2.ToString(), guid3.ToString()); + result.ShouldBe(p => p.Value2, "123", "456", "789"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingOnToDynamicMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingOnToDynamicMembers.cs new file mode 100644 index 000000000..a38c20a0e --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingOnToDynamicMembers.cs @@ -0,0 +1,78 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System; + using System.Collections.Generic; + using System.Dynamic; + using Microsoft.CSharp.RuntimeBinder; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingOnToDynamicMembers + { + [Fact] + public void ShouldMergeFromANestedSimpleTypedDictionary() + { + var guidOne = Guid.NewGuid(); + var guidTwo = Guid.NewGuid(); + + var source = new PublicProperty> + { + Value = new Dictionary { ["ONEah-ah-ah"] = guidOne, ["TWOah-ah-ah"] = guidTwo } + }; + + dynamic targetDynamic = new ExpandoObject(); + + targetDynamic.ONEah_ah_ah = guidOne; + targetDynamic.TWOah_ah_ah = guidTwo; + targetDynamic.THREEah_ah_ah = "gibblets"; + + var target = new PublicField { Value = targetDynamic }; + + Mapper.Map(source).Over(target); + + ((Guid?)target.Value.ONEah_ah_ah).ShouldBe(guidOne); + ((Guid?)target.Value.TWOah_ah_ah).ShouldBe(guidTwo); + ((string)target.Value.THREEah_ah_ah).ShouldBe("gibblets"); + } + + [Fact] + public void ShouldMapFromANestedComplexTypeEnumerableOnToFlattenedMembers() + { + var source = new PublicField + { + Value = new[] + { + new ProductDto { ProductId = "p-1", Price = 10.00m }, + new ProductDtoMega { ProductId = "p-m", Price = 100.00m, HowMega = "OH SO" }, + new ProductDto { ProductId = "p-2", Price = 1.99m } + } + }; + + dynamic targetDynamic = new ExpandoObject(); + + targetDynamic._0__ProductId = default(string); + targetDynamic._0__Price = default(double?); + targetDynamic._0__HowMega = "UBER"; + + targetDynamic._1__ProductId = "p-m1"; + targetDynamic._1__Price = default(int?); + + var target = new PublicField { Value = targetDynamic }; + + Mapper.Map(source).OnTo(target); + + ((string)targetDynamic._0__ProductId).ShouldBe("p-1"); + ((decimal)targetDynamic._0__Price).ShouldBe(10.00m); + ((string)targetDynamic._0__HowMega).ShouldBe("UBER"); + + ((string)targetDynamic._1__ProductId).ShouldBe("p-m1"); + ((decimal)targetDynamic._1__Price).ShouldBe(100.00m); + ((string)targetDynamic._1__HowMega).ShouldBe("OH SO"); + + ((string)targetDynamic._2__ProductId).ShouldBe("p-2"); + ((decimal)targetDynamic._2__Price).ShouldBe(1.99m); + Should.Throw(() => targetDynamic.Value_2_HowMega); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingOnToDynamics.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingOnToDynamics.cs new file mode 100644 index 000000000..58bda2f47 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingOnToDynamics.cs @@ -0,0 +1,29 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using TestClasses; + using Xunit; + + public class WhenMappingOnToDynamics + { + [Fact] + public void ShouldUpdateANullMemberValue() + { + var source = new PublicTwoFieldsStruct + { + Value1 = "New value!", + Value2 = "Won't be a new value!" + }; + + dynamic target = new ExpandoObject(); + + target.Value1 = default(string); + target.Value2 = "Already populated!"; + + Mapper.Map(source).OnTo(target); + + ((string)target.Value1).ShouldBe("New value!"); + ((string)target.Value2).ShouldBe("Already populated!"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingOverDynamicMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingOverDynamicMembers.cs new file mode 100644 index 000000000..110c4d53a --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingOverDynamicMembers.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingOverDynamicMembers + { + [Fact] + public void ShouldOverwriteASimpleTypeMember() + { + var source = new PublicField
+ { + Value = new Address { Line1 = "Here", Line2 = "There" } + }; + + dynamic targetDynamic = new ExpandoObject(); + + targetDynamic.Line1 = "La la la"; + + var target = new PublicProperty { Value = targetDynamic }; + + var result = Mapper.Map(source).Over(target); + + ((ExpandoObject)result.Value).ShouldBeSameAs((ExpandoObject)targetDynamic); + ((string)result.Value.Line1).ShouldBe("Here"); + ((string)result.Value.Line2).ShouldBe("There"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingOverDynamics.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingOverDynamics.cs new file mode 100644 index 000000000..d9df33b5e --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingOverDynamics.cs @@ -0,0 +1,87 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Collections.Generic; + using System.Dynamic; + using TestClasses; + using Xunit; + + public class WhenMappingOverDynamics + { + [Fact] + public void ShouldOverwriteASimpleTypeProperty() + { + var source = new { Value = 123 }; + + dynamic target = new ExpandoObject(); + + target.Value = 456; + + Mapper.Map(source).Over(target); + + ((int)target.Value).ShouldBe(123); + } + + [Fact] + public void ShouldOverwriteAnEnumProperty() + { + var source = new PublicPropertyStruct + { + Value = TitleShortlist.Mrs + }; + + dynamic target = new ExpandoObject(); + + target.Value = Title.Mr; + + Mapper.Map(source).Over(target); + + ((TitleShortlist)target.Value).ShouldBe(TitleShortlist.Mrs); + } + + [Fact] + public void ShouldOverwriteFromAStructCollection() + { + var source = new[] + { + new PublicPropertyStruct { Value = 1 }, + new PublicPropertyStruct { Value = 2 }, + new PublicPropertyStruct { Value = 3 }, + }; + + dynamic target = new ExpandoObject(); + + target._0__Value = 10; + target._2__Value = 30; + + Mapper.Map(source).Over(target); + + ((IDictionary)target).Count.ShouldBe(3); + + ((int)target._0__Value).ShouldBe(1); + ((int)target._1__Value).ShouldBe(2); + ((int)target._2__Value).ShouldBe(3); + } + + [Fact] + public void ShouldHandleAnUnmappableStructCollection() + { + var source = new[] + { + new PublicPropertyStruct { Value = new ProductDto { ProductId = "1" } }, + new PublicPropertyStruct { Value = new ProductDto { ProductId = "2" } } + }; + + dynamic target = new ExpandoObject(); + + target._0__Value_ProductId = "0"; + target._1__Value_ProductId = "0"; + + Mapper.Map(source).Over(target); + + ((IDictionary)target).Count.ShouldBe(2); + + ((string)target._0__Value_ProductId).ShouldBe("0"); + ((string)target._1__Value_ProductId).ShouldBe("0"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingToNewDynamicMembers.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingToNewDynamicMembers.cs new file mode 100644 index 000000000..e0e9f8944 --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingToNewDynamicMembers.cs @@ -0,0 +1,49 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingToNewDynamicMembers + { + [Fact] + public void ShouldMapFromAFlattenedMember() + { + var source = new + { + ValueLine1 = "Over here!", + Value = new { Line2 = "Over there!" }, + Va = new { Lu = new { E = new { Line3 = "Over where?!" } } } + }; + + var result = Mapper.Map(source).ToANew>(); + + ((object)result.Value).ShouldNotBeNull(); + dynamic resultDynamic = result.Value; + ((string)resultDynamic.Line1).ShouldBe("Over here!"); + ((string)resultDynamic.Line2).ShouldBe("Over there!"); + ((string)resultDynamic.Line3).ShouldBe("Over where?!"); + } + + [Fact] + public void ShouldMapFromNestedMembers() + { + var source = new PublicField
+ { + Value = new Address + { + Line1 = "One One One", + Line2 = "Two Two Two" + } + }; + + var result = Mapper.Map(source).ToANew>(); + + ((object)result.Value).ShouldNotBeNull(); + dynamic resultDynamic = result.Value; + ((string)resultDynamic.Line1).ShouldBe("One One One"); + ((string)resultDynamic.Line2).ShouldBe("Two Two Two"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingToNewDynamics.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingToNewDynamics.cs new file mode 100644 index 000000000..8f6e7807f --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingToNewDynamics.cs @@ -0,0 +1,48 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Dynamic; + using Microsoft.CSharp.RuntimeBinder; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingToNewDynamics + { + [Fact] + public void ShouldMapToADynamicSimpleTypeMember() + { + var result = Mapper.Map(new { Value = 123 }).ToANew(); + + ((object)result).ShouldNotBeNull(); + ((int)result.Value).ShouldBe(123); + } + + [Fact] + public void ShouldMapToAnExpandoObjectSimpleTypeMember() + { + dynamic result = Mapper.Map(new { Value = "Oh so dynamic" }).ToANew(); + + ((object)result).ShouldNotBeNull(); + ((string)result.Value).ShouldBe("Oh so dynamic"); + } + + [Fact] + public void ShouldMapNestedMembersToAnExpandoObject() + { + var source = new Customer + { + Title = Title.Mrs, + Name = "Captain Customer", + Address = new Address { Line1 = "One!", Line2 = "Two!" } + }; + dynamic result = Mapper.Map(source).ToANew(); + + ((object)result).ShouldNotBeNull(); + ((Title)result.Title).ShouldBe(Title.Mrs); + ((string)result.Name).ShouldBe("Captain Customer"); + Should.Throw(() => result.Address); + ((string)result.Address_Line1).ShouldBe("One!"); + ((string)result.Address_Line2).ShouldBe("Two!"); + } + } +} diff --git a/AgileMapper.UnitTests/Dynamics/WhenMappingToNewEnumerablesOfDynamic.cs b/AgileMapper.UnitTests/Dynamics/WhenMappingToNewEnumerablesOfDynamic.cs new file mode 100644 index 000000000..10c84005b --- /dev/null +++ b/AgileMapper.UnitTests/Dynamics/WhenMappingToNewEnumerablesOfDynamic.cs @@ -0,0 +1,45 @@ +namespace AgileObjects.AgileMapper.UnitTests.Dynamics +{ + using System.Collections.Generic; + using System.Linq; + using TestClasses; + using Xunit; + + public class WhenMappingToNewEnumerablesOfDynamic + { + [Fact] + public void ShouldMapAComplexTypeArray() + { + var source = new[] + { + new PublicField { Value = '1' }, + new PublicField { Value = '2' }, + new PublicField { Value = '3' } + }; + var result = Mapper.Map(source).ToANew>(); + + result.Count.ShouldBe(3); + + ((char)result.First().Value).ShouldBe('1'); + ((char)result.Second().Value).ShouldBe('2'); + ((char)result.Third().Value).ShouldBe('3'); + } + + [Fact] + public void ShouldMapAComplexTypeCollectionByRuntimeType() + { + ICollection source = new List + { + new PersonViewModel { Name = "Person" }, + new CustomerViewModel { Name = "Customer", Discount = 0.2 } + }; + var result = Mapper.Map(source).ToANew(); + + result.Length.ShouldBe(2); + + ((string)result.First().Name).ShouldBe("Person"); + ((string)result.Second().Name).ShouldBe("Customer"); + ((double)result.Second().Discount).ShouldBe(0.2); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Extensions/WhenEquatingExpressions.cs b/AgileMapper.UnitTests/Extensions/WhenEquatingExpressions.cs index 14fda35d9..40305c0f0 100644 --- a/AgileMapper.UnitTests/Extensions/WhenEquatingExpressions.cs +++ b/AgileMapper.UnitTests/Extensions/WhenEquatingExpressions.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/Extensions/WhenGeneratingVariableNames.cs b/AgileMapper.UnitTests/Extensions/WhenGeneratingVariableNames.cs index cd2543c3e..be8f2bffe 100644 --- a/AgileMapper.UnitTests/Extensions/WhenGeneratingVariableNames.cs +++ b/AgileMapper.UnitTests/Extensions/WhenGeneratingVariableNames.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Extensions { using System.Collections.Generic; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/Members/MemberTestsBase.cs b/AgileMapper.UnitTests/Members/MemberTestsBase.cs index b726ebd3a..7e0c0dc61 100644 --- a/AgileMapper.UnitTests/Members/MemberTestsBase.cs +++ b/AgileMapper.UnitTests/Members/MemberTestsBase.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using System.Linq.Expressions; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using AgileMapper.Members; using NetStandardPolyfills; diff --git a/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs b/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs index b1180e8bc..4b9fb5ac0 100644 --- a/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs +++ b/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs @@ -1,5 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Members { + using System; + using System.Linq.Expressions; using AgileMapper.Members; using ObjectPopulation; using Shouldly; @@ -13,7 +15,31 @@ public void ShouldNotMatchSameNameIncompatibleTypeProperties() { var source = new TwoValues { Value = new int[5], value = string.Empty }; var target = new PublicProperty(); - var targetMember = TargetMemberFor>(x => x.Value); + + var matchingSourceMember = GetMatchingSourceMember(source, target, pp => pp.Value); + + matchingSourceMember.ShouldNotBeNull(); + matchingSourceMember.Name.ShouldBe("value"); + } + + [Fact] + public void ShouldUseBaseClassMembers() + { + var source = new Derived { Value = 123 }; + var target = new PublicProperty(); + + var matchingSourceMember = GetMatchingSourceMember(source, target, pp => pp.Value); + + matchingSourceMember.ShouldNotBeNull(); + matchingSourceMember.Name.ShouldBe("Value"); + } + + private IQualifiedMember GetMatchingSourceMember( + TSource source, + TTarget target, + Expression> childMemberExpression) + { + var targetMember = TargetMemberFor(childMemberExpression); var mappingContext = new SimpleMappingContext(DefaultMapperContext.RuleSets.CreateNew, DefaultMapperContext); var rootMappingData = ObjectMappingDataFactory.ForRoot(source, target, mappingContext); @@ -22,12 +48,12 @@ public void ShouldNotMatchSameNameIncompatibleTypeProperties() var childMapperData = new ChildMemberMapperData(targetMember, rootMapperData); var childMappingContext = rootMappingData.GetChildMappingData(childMapperData); - var matchingSourceMember = SourceMemberMatcher.GetMatchFor(childMappingContext); - - matchingSourceMember.ShouldNotBeNull(); - matchingSourceMember.Name.ShouldBe("value"); + var matchingSourceMember = SourceMemberMatcher.GetMatchFor(childMappingContext, out var _); + return matchingSourceMember; } + #region Helper Classes + private class TwoValues { // ReSharper disable once InconsistentNaming @@ -42,5 +68,16 @@ public TwoValues() // ReSharper disable once UnusedAutoPropertyAccessor.Local public int[] Value { get; set; } } + + private abstract class Base + { + public virtual int Value { get; set; } + } + + private class Derived : Base + { + } + + #endregion } } \ No newline at end of file diff --git a/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs b/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs index 0b25c2a51..1c02983f0 100644 --- a/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs +++ b/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs @@ -3,7 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs index 1965afc76..db1e65352 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -159,6 +159,15 @@ public void ShouldMapANonMatchingEnumToANullableEnum() result.Value.ShouldBeNull(); } + [Fact] + public void ShouldMapANullableEnumToAnEnum() + { + var source = new PublicProperty { Value = Title.Dr }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(Title.Dr); + } + [Fact] public void ShouldMapANullNullableEnumToAnEnum() { diff --git a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs index a55acb152..b0e03065c 100644 --- a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs +++ b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Structs.Configuration { using System; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs index 1f7133be2..893669441 100644 --- a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs +++ b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using AgileMapper.Configuration; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/WhenAnalysingCollections.cs b/AgileMapper.UnitTests/WhenAnalysingCollections.cs index f97b325e1..ed82ec355 100644 --- a/AgileMapper.UnitTests/WhenAnalysingCollections.cs +++ b/AgileMapper.UnitTests/WhenAnalysingCollections.cs @@ -2,7 +2,7 @@ { using System; using System.Linq; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/WhenFlatteningObjects.cs b/AgileMapper.UnitTests/WhenFlatteningObjects.cs index 5a69505fc..5e35311a7 100644 --- a/AgileMapper.UnitTests/WhenFlatteningObjects.cs +++ b/AgileMapper.UnitTests/WhenFlatteningObjects.cs @@ -19,13 +19,15 @@ public void ShouldIncludeASimpleTypeMember() } [Fact] - public void ShouldIncludeASimpleTypeArrayMember() + public void ShouldFlattenASimpleTypeArrayMember() { var source = new PublicProperty { Value = new[] { 1L, 2L, 3L } }; var result = Mapper.Flatten(source); ((object)result).ShouldNotBeNull(); - ((long[])result.Value).ShouldBe(1, 2, 3); + ((long)result.Value_0_).ShouldBe(1L); + ((long)result.Value_1_).ShouldBe(2L); + ((long)result.Value_2_).ShouldBe(3L); } [Fact] @@ -50,9 +52,10 @@ public void ShouldIncludeAComplexTypeMemberSimpleTypeMember() public void ShouldHandleANullComplexTypeMember() { var source = new PublicProperty> { Value = null }; - var result = Mapper.Flatten(source); + var result = (IDictionary)Mapper.Flatten(source); - ((int)result.Value_Value).ShouldBeDefault(); + result.ShouldNotContainKey("Value"); + result.ShouldNotContainKey("Value_Value"); } [Fact] @@ -82,7 +85,7 @@ public void ShouldIncludeAComplexTypeEnumerableMemberSimpleTypeMember() }; var result = Mapper.Flatten(source); - ((string)result.Value0_ProductId).ShouldBe("SumminElse"); + ((string)result.Value_0__ProductId).ShouldBe("SumminElse"); } [Fact] @@ -92,9 +95,9 @@ public void ShouldHandleANullComplexTypeEnumerableMemberElement() { Value = new Product[] { null } }; - var result = Mapper.Flatten(source); + var result = (IDictionary)Mapper.Flatten(source); - ((string)result.Value0_ProductId).ShouldBeNull(); + result.ShouldNotContainKey("Value_0__ProductId"); } } } diff --git a/AgileMapper.UnitTests/WhenMappingCircularReferences.cs b/AgileMapper.UnitTests/WhenMappingCircularReferences.cs index 72c885e67..dbd4b3c7e 100644 --- a/AgileMapper.UnitTests/WhenMappingCircularReferences.cs +++ b/AgileMapper.UnitTests/WhenMappingCircularReferences.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; - using AgileMapper.Extensions; + using AgileMapper.Extensions.Internal; using Shouldly; using TestClasses; using Xunit; diff --git a/AgileMapper.UnitTests/WhenUsingPartialTrust.cs b/AgileMapper.UnitTests/WhenUsingPartialTrust.cs index cb428ca80..e8bcb1132 100644 --- a/AgileMapper.UnitTests/WhenUsingPartialTrust.cs +++ b/AgileMapper.UnitTests/WhenUsingPartialTrust.cs @@ -53,10 +53,7 @@ public void ShouldHandleAMaptimeException() [Fact] public void ShouldCreateAMappingPlan() { - ExecuteInPartialTrust(helper => - { - helper.TestMappingPlan(); - }); + ExecuteInPartialTrust(helper => helper.TestMappingPlan()); } private static void ExecuteInPartialTrust( diff --git a/AgileMapper.sln.DotSettings b/AgileMapper.sln.DotSettings index 06f73f9ea..10b838ca2 100644 --- a/AgileMapper.sln.DotSettings +++ b/AgileMapper.sln.DotSettings @@ -1,3 +1,3 @@  <data><IncludeFilters /><ExcludeFilters /></data> - <data><AttributeFilter ClassMask="*.ExcludeFromCodeCoverageAttribute" IsEnabled="True" /></data> \ No newline at end of file + <data><AttributeFilter ClassMask="AgileObjects.AgileMapper.ExcludeFromCodeCoverageAttribute" IsEnabled="True" /></data> \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index dfe3d1df0..1017f20b6 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -1,14 +1,16 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using System; + using System.Dynamic; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; using DataSources; - using Extensions; + using Extensions.Internal; using Members; + using Members.Dictionaries; using NetStandardPolyfills; using ReadableExpressions.Extensions; @@ -109,9 +111,11 @@ private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out Dictiona var entryKey = (string)((ConstantExpression)entryKeyExpression).Value; - var rootMember = (DictionaryTargetMember)_configInfo.MapperContext - .QualifiedMemberFactory - .RootTarget(); + var memberFactory = _configInfo.MapperContext.QualifiedMemberFactory; + + var rootMember = (DictionaryTargetMember)(_configInfo.TargetType == typeof(ExpandoObject) + ? memberFactory.RootTarget() + : memberFactory.RootTarget()); entryMember = rootMember.Append(typeof(TSource), entryKey); return true; diff --git a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryKeySpecifierBase.cs b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryKeySpecifierBase.cs index 2c1986b29..784f673c8 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryKeySpecifierBase.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryKeySpecifierBase.cs @@ -2,6 +2,8 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { using System; using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; + using Members; /// /// Provides base dictionary key configuration functionality for customising mappings @@ -11,15 +13,29 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries /// The second type argument necessary in the dictionary configuration. public abstract class CustomDictionaryKeySpecifierBase { - internal CustomDictionaryKeySpecifierBase(MappingConfigInfo configInfo) + private readonly QualifiedMember _sourceMember; + + internal CustomDictionaryKeySpecifierBase( + MappingConfigInfo configInfo, + QualifiedMember sourceMember = null) { ConfigInfo = configInfo; + _sourceMember = sourceMember; } internal MappingConfigInfo ConfigInfo { get; } internal UserConfigurationSet UserConfigurations => ConfigInfo.MapperContext.UserConfigurations; + internal DictionaryMappingConfigContinuation RegisterCustomKey( + string key, + Action dictionarySettingsAction) + { + var configuredKey = CustomDictionaryKey.ForSourceMember(key, _sourceMember, ConfigInfo); + + return RegisterCustomKey(configuredKey, dictionarySettingsAction); + } + internal DictionaryMappingConfigContinuation RegisterCustomKey( CustomDictionaryKey configuredKey, Action dictionarySettingsAction) diff --git a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs index 84f770d2e..ea636d44b 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs @@ -3,7 +3,9 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries using System; using System.Linq.Expressions; using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; using DataSources; + using Dynamics; /// /// Provides options for specifying a target member to which a dictionary configuration should apply. @@ -12,8 +14,9 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries /// The type of values stored in the dictionary to which the configuration will apply. /// /// The target type to which the configuration should apply. - public class CustomDictionaryMappingTargetMemberSpecifier - : CustomDictionaryKeySpecifierBase + public class CustomDictionaryMappingTargetMemberSpecifier : + CustomDictionaryKeySpecifierBase, + ICustomDynamicMappingTargetMemberSpecifier { private readonly string _key; private readonly Action _dictionarySettingsAction; @@ -54,6 +57,10 @@ public ISourceDictionaryMappingConfigContinuation To>> targetSetMethod) => RegisterCustomKey(targetSetMethod); + ISourceDynamicMappingConfigContinuation ICustomDynamicMappingTargetMemberSpecifier.To( + Expression> targetMember) + => RegisterCustomKey(targetMember); + private DictionaryMappingConfigContinuation RegisterCustomKey(LambdaExpression targetMemberLambda) { var configuredKey = CustomDictionaryKey.ForTargetMember(_key, targetMemberLambda, ConfigInfo); diff --git a/AgileMapper/Api/Configuration/Dictionaries/CustomTargetDictionaryKeySpecifier.cs b/AgileMapper/Api/Configuration/Dictionaries/CustomTargetDictionaryKeySpecifier.cs index aafd75132..f9b10c6ab 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/CustomTargetDictionaryKeySpecifier.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/CustomTargetDictionaryKeySpecifier.cs @@ -2,62 +2,55 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { using System; using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; + using Dynamics; using Members; - /// - /// Provides options for specifying custom target dictionary keys to which configured - /// source members should be mapped. - /// - public class CustomTargetDictionaryKeySpecifier - : CustomDictionaryKeySpecifierBase + internal class CustomTargetDictionaryKeySpecifier : + CustomDictionaryKeySpecifierBase, + ICustomTargetDictionaryKeySpecifier, + ICustomTargetDynamicMemberNameSpecifier { - private readonly QualifiedMember _sourceMember; - internal CustomTargetDictionaryKeySpecifier(MappingConfigInfo configInfo, QualifiedMember sourceMember) - : base(configInfo) + : base(configInfo, sourceMember) { - _sourceMember = sourceMember; } - /// - /// Configure a custom full dictionary key to use in place of the configured source member's name - /// when constructing a target dictionary key. For example, calling - /// Map(address => address.Line1).ToFullKey("StreetName") will generate the key 'StreetName' - /// when mapping an Address.Line1 property to a dictionary, instead of the default 'Address.Line1'. - /// - /// - /// The dictionary key to which to map the value of the configured source member. - /// - /// - /// An ITargetDictionaryMappingConfigContinuation to enable further configuration of mappings between - /// the source and target dictionary types being configured. - /// + #region Full Keys + public ITargetDictionaryMappingConfigContinuation ToFullKey(string fullMemberNameKey) - => RegisterMemberKey(fullMemberNameKey, (settings, customKey) => settings.AddFullKey(customKey)); - - /// - /// Use the given in place of the configured source member's name - /// when constructing a target dictionary key. For example, calling - /// Map(address => address.Line1).ToMemberKey("StreetName") will generate the key 'Address.StreetName' - /// when mapping an Address.Line1 property to a dictionary, instead of the default 'Address.Line1'. - /// - /// - /// The member key part to use in place of the configured source member's name. - /// - /// - /// An ITargetDictionaryMappingConfigContinuation to enable further configuration of mappings between - /// the source and target dictionary types being configured. - /// + => RegisterFullMemberNameKey(fullMemberNameKey); + + public ITargetDynamicMappingConfigContinuation ToFullMemberName(string fullMemberName) + => RegisterFullMemberNameKey(fullMemberName); + + private DictionaryMappingConfigContinuation RegisterFullMemberNameKey(string fullMemberNameKey) + { + return RegisterMemberKey(fullMemberNameKey, (settings, customKey) => settings.AddFullKey(customKey)); + } + + #endregion + + #region Part Keys + public ITargetDictionaryMappingConfigContinuation ToMemberNameKey(string memberNameKeyPart) - => RegisterMemberKey(memberNameKeyPart, (settings, customKey) => settings.AddMemberKey(customKey)); + => RegisterMemberNamePartKey(memberNameKeyPart); + + public ITargetDynamicMappingConfigContinuation ToMemberName(string memberName) + => RegisterMemberNamePartKey(memberName); - private ITargetDictionaryMappingConfigContinuation RegisterMemberKey( + private DictionaryMappingConfigContinuation RegisterMemberNamePartKey(string memberNameKeyPart) + { + return RegisterMemberKey(memberNameKeyPart, (settings, customKey) => settings.AddMemberKey(customKey)); + } + + #endregion + + private DictionaryMappingConfigContinuation RegisterMemberKey( string key, Action dictionarySettingsAction) { - var configuredKey = CustomDictionaryKey.ForSourceMember(key, _sourceMember, ConfigInfo); - - return RegisterCustomKey(configuredKey, dictionarySettingsAction); + return RegisterCustomKey(key, dictionarySettingsAction); } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/DictionaryConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/DictionaryConfigurator.cs deleted file mode 100644 index b760f9946..000000000 --- a/AgileMapper/Api/Configuration/Dictionaries/DictionaryConfigurator.cs +++ /dev/null @@ -1,118 +0,0 @@ -namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries -{ - using AgileMapper.Configuration; - - /// - /// Provides options for configuring how a mapper performs mapping from or to Dictionary{string, TValue} - /// instances. - /// - /// - /// The type of values stored in the dictionary to which the configurations will apply. - /// - public class DictionaryConfigurator : IGlobalDictionarySettings - { - private readonly MappingConfigInfo _configInfo; - - internal DictionaryConfigurator(MappingConfigInfo configInfo) - { - _configInfo = configInfo.ForSourceValueType(); - } - - #region IGlobalDictionarySettings Members - - /// - /// Construct dictionary keys for nested members using flattened member names. For example, a - /// Person.Address.StreetName member would be populated using the dictionary entry with key - /// 'AddressStreetName' when mapping to a root Person object. - /// - public IGlobalDictionarySettings UseFlattenedMemberNames() - { - var flattenedJoiningNameFactory = JoiningNameFactory.Flattened(GlobalConfigInfo); - - _configInfo.MapperContext.UserConfigurations.Dictionaries.Add(flattenedJoiningNameFactory); - return this; - } - - /// - /// Use the given to separate member names when mapping to nested - /// complex type members of any target type. For example, calling UseMemberName("_") will require - /// a dictionary entry with the key 'Address_Line1' to map to an Address.Line1 member. - /// - /// - /// The separator to use to separate member names when constructing dictionary keys for nested - /// members. - /// - public IGlobalDictionarySettings UseMemberNameSeparator(string separator) - { - var joiningNameFactory = JoiningNameFactory.For(separator, GlobalConfigInfo); - - _configInfo.MapperContext.UserConfigurations.Dictionaries.Add(joiningNameFactory); - return this; - } - - /// - /// Use the given to create the part of a dictionary key representing an - /// enumerable element. The pattern must contain a single 'i' character as a placeholder for the - /// enmerable index. For example, calling UseElementKeyPattern("(i)") and mapping from a dictionary - /// to a collection of ints will generate searches for keys '(0)', '(1)', '(2)', etc. - /// - /// - /// The pattern to use to create a dictionary key part representing an enumerable element. - /// - public IGlobalDictionarySettings UseElementKeyPattern(string pattern) - { - var keyPartFactory = ElementKeyPartFactory.For(pattern, GlobalConfigInfo); - - _configInfo.MapperContext.UserConfigurations.Dictionaries.Add(keyPartFactory); - return this; - } - - DictionaryConfigurator IGlobalDictionarySettings.AndWhenMapping => this; - - #endregion - - private MappingConfigInfo GlobalConfigInfo => _configInfo.Clone().ForAllRuleSets().ForAllTargetTypes(); - - /// - /// Configure how this mapper performs mappings from dictionaries in all MappingRuleSets - /// (create new, overwrite, etc), to the target type specified by the type argument. - /// - /// The target type to which the configuration will apply. - /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. - public ISourceDictionaryMappingConfigurator To() where TTarget : class - => CreateConfigurator(_configInfo.ForAllRuleSets()); - - /// - /// Configure how this mapper performs object creation mappings from dictionaries to the target type - /// specified by the type argument. - /// - /// The target type to which the configuration will apply. - /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. - public ISourceDictionaryMappingConfigurator ToANew() where TTarget : class - => CreateConfigurator(Constants.CreateNew); - - /// - /// Configure how this mapper performs OnTo (merge) mappings from dictionaries to the target type - /// specified by the type argument. - /// - /// The target type to which the configuration will apply. - /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. - public ISourceDictionaryMappingConfigurator OnTo() where TTarget : class - => CreateConfigurator(Constants.Merge); - - /// - /// Configure how this mapper performs Over (overwrite) mappings from dictionaries to the target type - /// specified by the type argument. - /// - /// The target type to which the configuration will apply. - /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. - public ISourceDictionaryMappingConfigurator Over() where TTarget : class - => CreateConfigurator(Constants.Overwrite); - - private ISourceDictionaryMappingConfigurator CreateConfigurator(string ruleSetName) - => CreateConfigurator(_configInfo.ForRuleSet(ruleSetName)); - - private static ISourceDictionaryMappingConfigurator CreateConfigurator(MappingConfigInfo configInfo) - => new SourceDictionaryMappingConfigurator(configInfo); - } -} diff --git a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigContinuation.cs b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigContinuation.cs index 0601dac7d..9f4a22172 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigContinuation.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigContinuation.cs @@ -1,10 +1,13 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { using AgileMapper.Configuration; + using Dynamics; internal class DictionaryMappingConfigContinuation : ISourceDictionaryMappingConfigContinuation, - ITargetDictionaryMappingConfigContinuation + ITargetDictionaryMappingConfigContinuation, + ISourceDynamicMappingConfigContinuation, + ITargetDynamicMappingConfigContinuation { private readonly MappingConfigInfo _configInfo; @@ -18,5 +21,11 @@ ISourceDictionaryMappingConfigurator ISourceDictionaryMappingCo ITargetDictionaryMappingConfigurator ITargetDictionaryMappingConfigContinuation.And => new TargetDictionaryMappingConfigurator(_configInfo.Clone()); + + ISourceDynamicMappingConfigurator ISourceDynamicMappingConfigContinuation.And + => new SourceDynamicMappingConfigurator(_configInfo.Clone()); + + ITargetDynamicMappingConfigurator ITargetDynamicMappingConfigContinuation.And + => new TargetDynamicMappingConfigurator(_configInfo.Clone()); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs new file mode 100644 index 000000000..f864583c1 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs @@ -0,0 +1,170 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries +{ + using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; + using Dynamics; + using static AgileMapper.Configuration.Dictionaries.DictionaryContext; + + internal class DictionaryMappingConfigurator : + DictionaryMappingConfiguratorBase, + IGlobalDictionarySettings, + ISourceDictionaryTargetTypeSelector, + IGlobalDynamicSettings, + ISourceDynamicTargetTypeSelector + { + private readonly MappingConfigInfo _configInfo; + + internal DictionaryMappingConfigurator(MappingConfigInfo configInfo) + : base(configInfo) + { + _configInfo = configInfo; + } + + #region Mapping Settings + + #region UseFlattenedTargetMemberNames + + IGlobalDictionarySettings IGlobalDictionarySettings.UseFlattenedTargetMemberNames() + => RegisterFlattenedTargetMemberNames(GetGlobalConfigInfo(All)); + + public ISourceDictionarySettings UseFlattenedTargetMemberNames() + => RegisterFlattenedTargetMemberNames(GetConfigInfo(SourceOnly)); + + + IGlobalDynamicSettings IGlobalDynamicSettings.UseFlattenedTargetMemberNames() + => RegisterFlattenedTargetMemberNames(GetGlobalConfigInfo(All)); + + ISourceDynamicSettings ISourceDynamicSettings.UseFlattenedTargetMemberNames() + => RegisterFlattenedTargetMemberNames(GetConfigInfo(SourceOnly)); + + private DictionaryMappingConfigurator RegisterFlattenedTargetMemberNames(MappingConfigInfo configInfo) + { + SetupFlattenedTargetMemberNames(configInfo); + return this; + } + + #endregion + + #region UseMemberNameSeparator + + IGlobalDictionarySettings IGlobalDictionarySettings.UseMemberNameSeparator(string separator) + => RegisterMemberNameSeparator(separator, GetGlobalConfigInfo(All)); + + public ISourceDictionarySettings UseMemberNameSeparator(string separator) + => RegisterMemberNameSeparator(separator, GetConfigInfo(SourceOnly)); + + IGlobalDynamicSettings IGlobalDynamicSettings.UseMemberNameSeparator(string separator) + => RegisterMemberNameSeparator(separator, GetGlobalConfigInfo(All)); + + ISourceDynamicSettings ISourceDynamicSettings.UseMemberNameSeparator(string separator) + => RegisterMemberNameSeparator(separator, GetConfigInfo(SourceOnly)); + + private DictionaryMappingConfigurator RegisterMemberNameSeparator( + string separator, + MappingConfigInfo configInfo) + { + SetupMemberNameSeparator(separator, configInfo); + return this; + } + + #endregion + + #region UseElementKeyPattern + + IGlobalDictionarySettings IGlobalDictionarySettings.UseElementKeyPattern(string pattern) + => RegisterElementKeyPattern(pattern, GetGlobalConfigInfo(All)); + + public ISourceDictionarySettings UseElementKeyPattern(string pattern) + => RegisterElementKeyPattern(pattern, GetConfigInfo(SourceOnly)); + + IGlobalDynamicSettings IGlobalDynamicSettings.UseElementKeyPattern(string pattern) + => RegisterElementKeyPattern(pattern, GetGlobalConfigInfo(All)); + + ISourceDynamicSettings ISourceDynamicSettings.UseElementKeyPattern(string pattern) + => RegisterElementKeyPattern(pattern, GetConfigInfo(SourceOnly)); + + private DictionaryMappingConfigurator RegisterElementKeyPattern( + string pattern, + MappingConfigInfo configInfo) + { + SetupElementKeyPattern(pattern, configInfo); + return this; + } + + #endregion + + private MappingConfigInfo GetConfigInfo(DictionaryContext context) + { + return (_configInfo.TargetType != typeof(object)) + ? _configInfo.Clone().Set(context) + : GetGlobalConfigInfo(context); + } + + private MappingConfigInfo GetGlobalConfigInfo(DictionaryContext context) + => _configInfo.Clone().ForAllRuleSets().ForAllTargetTypes().Set(context); + + #region AndWhenMapping + + MappingConfigStartingPoint IGlobalDictionarySettings.AndWhenMapping + => new MappingConfigStartingPoint(_configInfo.MapperContext); + + public ISourceDictionaryTargetTypeSelector AndWhenMapping => this; + + MappingConfigStartingPoint IGlobalDynamicSettings.AndWhenMapping + => new MappingConfigStartingPoint(_configInfo.MapperContext); + + ISourceDynamicTargetTypeSelector ISourceDynamicSettings.AndWhenMapping => this; + + #endregion + + #endregion + + #region Dictionaries + + public ISourceDictionaryMappingConfigurator To() + => CreateDictionaryConfigurator(_configInfo.ForAllRuleSets()); + + public ISourceDictionaryMappingConfigurator ToANew() + => CreateDictionaryConfigurator(Constants.CreateNew); + + public ISourceDictionaryMappingConfigurator OnTo() + => CreateDictionaryConfigurator(Constants.Merge); + + public ISourceDictionaryMappingConfigurator Over() + => CreateDictionaryConfigurator(Constants.Overwrite); + + private SourceDictionaryMappingConfigurator CreateDictionaryConfigurator( + string ruleSetName) + => CreateDictionaryConfigurator(_configInfo.ForRuleSet(ruleSetName)); + + private static SourceDictionaryMappingConfigurator CreateDictionaryConfigurator( + MappingConfigInfo configInfo) + => new SourceDictionaryMappingConfigurator(configInfo); + + #endregion + + #region Dynamics + + ISourceDynamicMappingConfigurator ISourceDynamicTargetTypeSelector.To() + => CreateDynamicConfigurator(_configInfo.ForAllRuleSets()); + + ISourceDynamicMappingConfigurator ISourceDynamicTargetTypeSelector.ToANew() + => CreateDynamicConfigurator(Constants.CreateNew); + + ISourceDynamicMappingConfigurator ISourceDynamicTargetTypeSelector.OnTo() + => CreateDynamicConfigurator(Constants.Merge); + + ISourceDynamicMappingConfigurator ISourceDynamicTargetTypeSelector.Over() + => CreateDynamicConfigurator(Constants.Overwrite); + + private SourceDynamicMappingConfigurator CreateDynamicConfigurator( + string ruleSetName) + => CreateDynamicConfigurator(_configInfo.ForRuleSet(ruleSetName)); + + private static SourceDynamicMappingConfigurator CreateDynamicConfigurator( + MappingConfigInfo configInfo) + => new SourceDynamicMappingConfigurator(configInfo); + + #endregion + } +} diff --git a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfiguratorBase.cs b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfiguratorBase.cs index 4e55e54f5..3e06fb743 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfiguratorBase.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfiguratorBase.cs @@ -1,6 +1,8 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { + using System; using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; internal abstract class DictionaryMappingConfiguratorBase : MappingConfigurator @@ -10,25 +12,57 @@ protected DictionaryMappingConfiguratorBase(MappingConfigInfo configInfo) { } - protected void SetupFlattenedMemberNames() + protected void SetupFlattenedTargetMemberNames(MappingConfigInfo configInfo = null) { - var flattenedJoiningNameFactory = JoiningNameFactory.Flattened(ConfigInfo); + var flattenedJoiningNameFactory = JoiningNameFactory.Flattened(configInfo ?? ConfigInfo); ConfigInfo.MapperContext.UserConfigurations.Dictionaries.Add(flattenedJoiningNameFactory); } - protected void SetupMemberNameSeparator(string separator) + protected void SetupMemberNameSeparator(string separator, MappingConfigInfo configInfo = null) { - var joiningNameFactory = JoiningNameFactory.For(separator, ConfigInfo); + var joiningNameFactory = JoiningNameFactory.For(separator, configInfo ?? ConfigInfo); ConfigInfo.MapperContext.UserConfigurations.Dictionaries.Add(joiningNameFactory); } - protected void SetupElementKeyPattern(string pattern) + protected void SetupElementKeyPattern(string pattern, MappingConfigInfo configInfo = null) { - var keyPartFactory = ElementKeyPartFactory.For(pattern, ConfigInfo); + var keyPartFactory = ElementKeyPartFactory.For(pattern, configInfo ?? ConfigInfo); ConfigInfo.MapperContext.UserConfigurations.Dictionaries.Add(keyPartFactory); } + + protected CustomDictionaryMappingTargetMemberSpecifier MapFullKey(string fullMemberNameKey) + { + return CreateTargetMemberSpecifier( + fullMemberNameKey, + "keys", + (settings, customKey) => settings.AddFullKey(customKey)); + } + + protected CustomDictionaryMappingTargetMemberSpecifier MapMemberNameKey(string memberNameKeyPart) + { + return CreateTargetMemberSpecifier( + memberNameKeyPart, + "member name", + (settings, customKey) => settings.AddMemberKey(customKey)); + } + + protected CustomDictionaryMappingTargetMemberSpecifier CreateTargetMemberSpecifier( + string key, + string keyName, + Action dictionarySettingsAction) + { + if (key == null) + { + throw new MappingConfigurationException(keyName + " cannot be null"); + } + + return new CustomDictionaryMappingTargetMemberSpecifier( + ConfigInfo, + key, + dictionarySettingsAction); + } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/ICustomTargetDictionaryKeySpecifier.cs b/AgileMapper/Api/Configuration/Dictionaries/ICustomTargetDictionaryKeySpecifier.cs new file mode 100644 index 000000000..533bf5fd2 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dictionaries/ICustomTargetDictionaryKeySpecifier.cs @@ -0,0 +1,39 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries +{ + /// + /// Provides options for specifying custom target Dictionary keys to which configured + /// source members should be mapped. + /// + public interface ICustomTargetDictionaryKeySpecifier + { + /// + /// Configure a custom full Dictionary key to use in place of the configured source member's name + /// when constructing a target Dictionary key. For example, calling + /// Map(address => address.Line1).ToFullKey("StreetName") will generate the key 'StreetName' + /// when mapping an Address.Line1 property to a Dictionary, instead of the default 'Address.Line1'. + /// + /// + /// The Dictionary key to which to map the value of the configured source member. + /// + /// + /// An ITargetDictionaryMappingConfigContinuation to enable further configuration of mappings between + /// the source and target Dictionary types being configured. + /// + ITargetDictionaryMappingConfigContinuation ToFullKey(string fullMemberNameKey); + + /// + /// Use the given in place of the configured source member's name + /// when constructing a target Dictionary key. For example, calling + /// Map(address => address.Line1).ToMemberKey("StreetName") will generate the key 'Address.StreetName' + /// when mapping an Address.Line1 property to a Dictionary, instead of the default 'Address.Line1'. + /// + /// + /// The member key part to use in place of the configured source member's name. + /// + /// + /// An ITargetDictionaryMappingConfigContinuation to enable further configuration of mappings between + /// the source and target Dictionary types being configured. + /// + ITargetDictionaryMappingConfigContinuation ToMemberNameKey(string memberNameKeyPart); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/IGlobalDictionarySettings.cs b/AgileMapper/Api/Configuration/Dictionaries/IGlobalDictionarySettings.cs index 23e17a49a..9cbbb3134 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/IGlobalDictionarySettings.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/IGlobalDictionarySettings.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { /// - /// Provides options for globally configuring how all mappers will perform mappings from dictionaries. + /// Provides options for configuring how this mapper will perform mappings from and to dictionaries. /// /// /// The type of values stored in the dictionary to which the configurations will apply. @@ -9,49 +9,51 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries public interface IGlobalDictionarySettings { /// - /// Construct dictionary keys for nested members using flattened member names. For example, a - /// Person.Address.StreetName member would be populated using the dictionary entry with key - /// 'AddressStreetName' when mapping to a root Person object. + /// Construct keys for target Dictionary members using flattened member names. For example, + /// a Person.Address.StreetName member would be mapped to a Dictionary entry with the key + /// 'AddressStreetName'. /// /// /// An with which to globally configure other - /// dictionary mapping aspects. + /// Dictionary mapping aspects. /// - IGlobalDictionarySettings UseFlattenedMemberNames(); + IGlobalDictionarySettings UseFlattenedTargetMemberNames(); /// - /// Use the given to separate member names when mapping to nested - /// complex type members of any target type. For example, calling UseMemberName("_") will require - /// a dictionary entry with the key 'Address_Line1' to map to an Address.Line1 member. + /// Use the given to construct expected source and target Dictionary + /// keys, and to separate member names when mapping to nested complex type members of any target + /// type - the default is '.'. For example, calling UseMemberNameSeparator("_") will require a + /// Dictionary entry with the key 'Address_Line1' to map to an Address.Line1 member. /// /// - /// The separator to use to separate member names when constructing dictionary keys for nested - /// members. + /// The separator to use to separate member names when constructing expected Dictionary keys for + /// nested members. /// /// /// An with which to globally configure other - /// dictionary mapping aspects. + /// Dictionary mapping aspects. /// IGlobalDictionarySettings UseMemberNameSeparator(string separator); /// - /// Use the given to create the part of a dictionary key representing an - /// enumerable element. The pattern must contain a single 'i' character as a placeholder for the - /// enumerable index. For example, calling UseElementKeyPattern("(i)") and mapping from a dictionary - /// to a collection of ints will generate searches for keys '(0)', '(1)', '(2)', etc. + /// Use the given to create the part of an expected Dictionary key + /// representing an enumerable element - the default is '[i]'. The pattern must contain a single + /// 'i' character as a placeholder for the enumerable index. For example, calling + /// UseElementKeyPattern("(i)") and mapping from a Dictionary to a collection of ints will generate + /// searches for keys '(0)', '(1)', '(2)', etc. /// /// - /// The pattern to use to create a dictionary key part representing an enumerable element. + /// The pattern to use to create an expected Dictionary key part representing an enumerable element. /// /// /// An with which to globally configure other - /// dictionary mapping aspects. + /// Dictionary mapping aspects. /// IGlobalDictionarySettings UseElementKeyPattern(string pattern); /// - /// Gets a link back to the full , for api fluency. + /// Gets a link back to the full , for api fluency. /// - DictionaryConfigurator AndWhenMapping { get; } + MappingConfigStartingPoint AndWhenMapping { get; } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryConfigSettings.cs b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryConfigSettings.cs index f7a7f2231..51cbae0e1 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryConfigSettings.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryConfigSettings.cs @@ -1,57 +1,51 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { /// - /// Provides options for configuring how mappers will perform mappings from dictionaries. + /// Provides options for configuring how mappers will perform mappings from Dictionaries to the + /// given . /// /// - /// The type of values stored in the dictionary to which the configurations will apply. + /// The type of values stored in the Dictionary to which the configurations will apply. /// /// The target type to which the configuration should apply. public interface ISourceDictionaryConfigSettings { /// - /// Construct dictionary keys for nested members using flattened member names. For example, a - /// Person.Address.StreetName member would be populated using the dictionary entry with key - /// 'AddressStreetName' when mapping to a root Person object. - /// - /// - /// An ISourceDictionaryConfigSettings to enable further configuration of mappings from dictionaries - /// to the target type being configured. - /// - ISourceDictionaryConfigSettings UseFlattenedMemberNames(); - - /// - /// Use the given to separate member names when mapping to nested - /// complex type members. For example, calling UseMemberName("-") will require a dictionary entry - /// with the key 'Address-Line1' to map to an Address.Line1 member. + /// Use the given to construct expected source Dictionary keys, + /// and to separate member names when mapping to nested complex type members of any target type - + /// the default is '.'. For example, calling UseMemberNameSeparator("_") will require a source + /// Dictionary entry with the key 'Address_Line1' to map to an Address.Line1 member. /// /// - /// The separator to use to separate member names when constructing dictionary keys for nested - /// members. + /// The separator to use to separate member names when constructing expected source Dictionary + /// keys for nested members. /// /// - /// An ISourceDictionaryConfigSettings to enable further configuration of mappings from dictionaries - /// to the target type being configured. + /// The with which to configure + /// other aspects of source Dictionary mapping. /// ISourceDictionaryConfigSettings UseMemberNameSeparator(string separator); /// - /// Use the given to create the part of a dictionary key representing an - /// enumerable element. The pattern must contain a single 'i' character as a placeholder for the - /// enumerable index. For example, calling UseElementKeyPattern("(i)") and mapping from a dictionary - /// to a collection of ints will generate searches for keys '(0)', '(1)', '(2)', etc. + /// Use the given to create the part of an expected source Dictionary + /// key representing an enumerable element - the default is '[i]'. The pattern must contain a + /// single 'i' character as a placeholder for the enumerable index. For example, calling + /// UseElementKeyPattern("(i)") and mapping from a Dictionary to a collection of ints will generate + /// searches for keys '(0)', '(1)', '(2)', etc. /// /// - /// The pattern to use to create a dictionary key part representing an enumerable element. + /// The pattern to use to create an expected source Dictionary key part representing an enumerable + /// element. /// /// - /// An ISourceDictionaryConfigSettings to enable further configuration of mappings from dictionaries - /// to the target type being configured. + /// The with which to configure + /// other aspects of source Dictionary mapping. /// ISourceDictionaryConfigSettings UseElementKeyPattern(string pattern); /// - /// Gets a link back to the full ISourceDictionaryMappingConfigurator, for api fluency. + /// Gets a link back to the full , + /// for api fluency. /// ISourceDictionaryMappingConfigurator And { get; } } diff --git a/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionarySettings.cs b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionarySettings.cs new file mode 100644 index 000000000..24ceaf716 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionarySettings.cs @@ -0,0 +1,61 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries +{ + /// + /// Provides options for configuring how this mapper will perform mappings from Dictionaries. + /// + /// + /// The type of values stored in the dictionary to which the configurations will apply. + /// + public interface ISourceDictionarySettings + { + /// + /// Construct keys for target Dictionary members using flattened member names. For example, a + /// Person.Address.StreetName member would be mapped to a Dictionary entry with the key + /// 'AddressStreetName'. + /// + /// + /// The with which to configure other aspects + /// of source Dictionary mapping. + /// + ISourceDictionarySettings UseFlattenedTargetMemberNames(); + + /// + /// Use the given to construct expected source Dictionary keys, + /// and to separate member names when mapping to nested complex type members of any target type - + /// the default is '.'. For example, calling UseMemberNameSeparator("_") will require a source + /// Dictionary entry with the key 'Address_Line1' to map to an Address.Line1 member. + /// + /// + /// The separator to use to separate member names when constructing expected source Dictionary + /// keys for nested members. + /// + /// + /// The with which to configure other aspects + /// of source Dictionary mapping. + /// + ISourceDictionarySettings UseMemberNameSeparator(string separator); + + /// + /// Use the given to create the part of an expected source Dictionary + /// key representing an enumerable element - the default is '[i]'. The pattern must contain a + /// single 'i' character as a placeholder for the enumerable index. For example, calling + /// UseElementKeyPattern("(i)") and mapping from a Dictionary to a collection of ints will generate + /// searches for keys '(0)', '(1)', '(2)', etc. + /// + /// + /// The pattern to use to create an expected source Dictionary key part representing an enumerable + /// element. + /// + /// + /// The with which to configure other aspects + /// of source Dictionary mapping. + /// + ISourceDictionarySettings UseElementKeyPattern(string pattern); + + /// + /// Gets a link back to the full , + /// for api fluency. + /// + ISourceDictionaryTargetTypeSelector AndWhenMapping { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs new file mode 100644 index 000000000..2b4d0d185 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs @@ -0,0 +1,43 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries +{ + /// + /// Provides options for specifying the type of Dictionary mapping to perform. + /// + /// + /// The type of values stored in the Dictionary to which the configurations will apply. + /// + public interface ISourceDictionaryTargetTypeSelector : ISourceDictionarySettings + { + /// + /// Configure how this mapper performs mappings from Dictionaries in all MappingRuleSets + /// (create new, overwrite, etc), to the target type specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. + ISourceDictionaryMappingConfigurator To(); + + /// + /// Configure how this mapper performs object creation mappings from Dictionaries to the target type + /// specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. + ISourceDictionaryMappingConfigurator ToANew(); + + /// + /// Configure how this mapper performs OnTo (merge) mappings from Dictionaries to the target type + /// specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. + ISourceDictionaryMappingConfigurator OnTo(); + + /// + /// Configure how this mapper performs Over (overwrite) mappings from Dictionaries to the target type + /// specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDictionaryMappingConfigurator with which to complete the configuration. + ISourceDictionaryMappingConfigurator Over(); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryConfigSettings.cs b/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryConfigSettings.cs index a90bf9819..73d0a999b 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryConfigSettings.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryConfigSettings.cs @@ -1,52 +1,51 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { /// - /// Provides options for configuring how mappers will perform mappings to dictionaries. + /// Provides options for configuring how mappers will perform mappings to Dictionaries. /// /// The source type to which the configuration should apply. /// - /// The type of values stored in the dictionary to which the configurations will apply. + /// The type of values stored in the Dictionary to which the configurations will apply. /// public interface ITargetDictionaryConfigSettings { /// - /// Construct dictionary keys for nested members using flattened member names. For example, a - /// Person.Address.StreetName member would be mapped to a dictionary entry with key - /// 'AddressStreetName' when mapping from a root Person object. + /// Construct Dictionary keys for nested members using flattened member names - the default is to + /// separate member names with '.'. For example, a Person.Address.StreetName member would be mapped to + /// a Dictionary entry with key 'AddressStreetName' when mapping from a root Person object. /// /// /// An ITargetDictionaryConfigSettings to enable further configuration of mappings from the source type - /// being configured to dictionaries. + /// being configured to Dictionaries. /// ITargetDictionaryConfigSettings UseFlattenedMemberNames(); /// /// Use the given to separate member names when mapping from nested complex - /// type members to dictionaries. For example, calling UseMemberName("_") will create a dictionary entry - /// with the key 'Address_Line1' when mapped from an Address.Line1 member. + /// type members to Dictionaries - the default is '.'. For example, calling UseMemberNameSeparator("_") + /// will create a Dictionary entry with the key 'Address_Line1' when mapping from an Address.Line1 member. /// /// - /// The separator to use to separate member names when constructing dictionary keys for nested - /// members. + /// The separator to use to separate member names when constructing Dictionary keys for nested members. /// /// /// An ITargetDictionaryConfigSettings to enable further configuration of mappings from the source type - /// being configured to dictionaries. + /// being configured to Dictionaries. /// ITargetDictionaryConfigSettings UseMemberNameSeparator(string separator); /// - /// Use the given to create the part of a dictionary key representing an - /// enumerable element. The pattern must contain a single 'i' character as a placeholder for the - /// enumerable index. For example, calling UseElementKeyPattern("(i)") and mapping from a collection - /// of ints to a dictionary will generate keys '(0)', '(1)', '(2)', etc. + /// Use the given to create the part of a Dictionary key representing an + /// enumerable element - the default is '[i]. The pattern must contain a single 'i' character as a + /// placeholder for the enumerable index. For example, calling UseElementKeyPattern("(i)") and mapping + /// from a collection of ints to a Dictionary will generate keys '(0)', '(1)', '(2)', etc. /// /// - /// The pattern to use to create a dictionary key part representing an enumerable element. + /// The pattern to use to create a Dictionary key part representing an enumerable element. /// /// /// An ITargetDictionaryConfigSettings to enable further configuration of mappings from the source - /// type being configured to dictionaries. + /// type being configured to Dictionaries. /// ITargetDictionaryConfigSettings UseElementKeyPattern(string pattern); diff --git a/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryMappingConfigurator.cs index 79158c27a..3c6d0fa38 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/ITargetDictionaryMappingConfigurator.cs @@ -10,7 +10,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries /// /// The source type to which the configuration should apply. /// - /// The type of values stored in the dictionary to which the configurations will apply. + /// The type of values stored in the Dictionary to which the configurations will apply. /// // ReSharper disable once PossibleInterfaceMemberAmbiguity public interface ITargetDictionaryMappingConfigurator : @@ -18,15 +18,15 @@ public interface ITargetDictionaryMappingConfigurator : ITargetDictionaryConfigSettings { /// - /// Map the given member using a custom dictionary key. + /// Map the given member using a custom Dictionary key. /// /// The source member's type. /// The source member to which to apply the configuration. /// - /// A CustomTargetDictionaryKeySpecifier with which to specify the custom key to use when mapping + /// A ICustomTargetDictionaryKeySpecifier with which to specify the custom key to use when mapping /// the given . /// - CustomTargetDictionaryKeySpecifier MapMember( + ICustomTargetDictionaryKeySpecifier MapMember( Expression> sourceMember); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dictionaries/SourceDictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/SourceDictionaryMappingConfigurator.cs index 9e30121d2..bf754da33 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/SourceDictionaryMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/SourceDictionaryMappingConfigurator.cs @@ -1,6 +1,5 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries { - using System; using System.Collections.Generic; using AgileMapper.Configuration; @@ -15,12 +14,6 @@ public SourceDictionaryMappingConfigurator(MappingConfigInfo configInfo) #region ISourceDictionaryConfigSettings Members - public ISourceDictionaryConfigSettings UseFlattenedMemberNames() - { - SetupFlattenedMemberNames(); - return this; - } - public ISourceDictionaryConfigSettings UseMemberNameSeparator(string separator) { SetupMemberNameSeparator(separator); @@ -39,30 +32,9 @@ ISourceDictionaryMappingConfigurator ISourceDictionaryConfigSet #endregion public CustomDictionaryMappingTargetMemberSpecifier MapFullKey(string fullMemberNameKey) - => CreateTargetMemberSpecifier("keys", fullMemberNameKey, (settings, customKey) => settings.AddFullKey(customKey)); + => MapFullKey(fullMemberNameKey); public CustomDictionaryMappingTargetMemberSpecifier MapMemberNameKey(string memberNameKeyPart) - { - return CreateTargetMemberSpecifier( - "member names", - memberNameKeyPart, - (settings, customKey) => settings.AddMemberKey(customKey)); - } - - private CustomDictionaryMappingTargetMemberSpecifier CreateTargetMemberSpecifier( - string keyName, - string key, - Action dictionarySettingsAction) - { - if (key == null) - { - throw new MappingConfigurationException(keyName + " cannot be null"); - } - - return new CustomDictionaryMappingTargetMemberSpecifier( - ConfigInfo, - key, - dictionarySettingsAction); - } + => MapMemberNameKey(memberNameKeyPart); } } diff --git a/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs index 21444211d..94a267549 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs @@ -20,7 +20,7 @@ public TargetDictionaryMappingConfigurator(MappingConfigInfo configInfo) public ITargetDictionaryConfigSettings UseFlattenedMemberNames() { - SetupFlattenedMemberNames(); + SetupFlattenedTargetMemberNames(); return this; } @@ -41,7 +41,7 @@ ITargetDictionaryMappingConfigurator ITargetDictionaryConfigSet #endregion - public CustomTargetDictionaryKeySpecifier MapMember( + public ICustomTargetDictionaryKeySpecifier MapMember( Expression> sourceMember) { var sourceQualifiedMember = GetSourceMemberOrThrow(sourceMember); diff --git a/AgileMapper/Api/Configuration/Dynamics/CustomTargetDynamicMemberNameSpecifier.cs b/AgileMapper/Api/Configuration/Dynamics/CustomTargetDynamicMemberNameSpecifier.cs new file mode 100644 index 000000000..7538de22b --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/CustomTargetDynamicMemberNameSpecifier.cs @@ -0,0 +1,60 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + using System; + using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; + using Dictionaries; + using Members; + + /// + /// Provides options for specifying custom target ExpandoObject member names to which + /// configured source members should be mapped. + /// + public class CustomTargetDynamicMemberNameSpecifier : + CustomDictionaryKeySpecifierBase + { + internal CustomTargetDynamicMemberNameSpecifier(MappingConfigInfo configInfo, QualifiedMember sourceMember) + : base(configInfo, sourceMember) + { + } + + /// + /// Configure a custom full Dictionary key to use in place of the configured source member's name + /// when constructing a target Dictionary key. For example, calling + /// Map(address => address.Line1).ToFullKey("StreetName") will generate the key 'StreetName' + /// when mapping an Address.Line1 property to a Dictionary, instead of the default 'Address.Line1'. + /// + /// + /// The Dictionary key to which to map the value of the configured source member. + /// + /// + /// An ITargetDictionaryMappingConfigContinuation to enable further configuration of mappings between + /// the source and target Dictionary types being configured. + /// + public ITargetDynamicMappingConfigContinuation ToFullKey(string fullMemberNameKey) + => RegisterMemberKey(fullMemberNameKey, (settings, customKey) => settings.AddFullKey(customKey)); + + /// + /// Use the given in place of the configured source member's name + /// when constructing a target Dictionary key. For example, calling + /// Map(address => address.Line1).ToMemberKey("StreetName") will generate the key 'Address.StreetName' + /// when mapping an Address.Line1 property to a Dictionary, instead of the default 'Address.Line1'. + /// + /// + /// The member key part to use in place of the configured source member's name. + /// + /// + /// An ITargetDictionaryMappingConfigContinuation to enable further configuration of mappings between + /// the source and target Dictionary types being configured. + /// + public ITargetDynamicMappingConfigContinuation ToMemberNameKey(string memberNameKeyPart) + => RegisterMemberKey(memberNameKeyPart, (settings, customKey) => settings.AddMemberKey(customKey)); + + private ITargetDynamicMappingConfigContinuation RegisterMemberKey( + string key, + Action dictionarySettingsAction) + { + return RegisterCustomKey(key, dictionarySettingsAction); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ICustomDynamicMappingTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/Dynamics/ICustomDynamicMappingTargetMemberSpecifier.cs new file mode 100644 index 000000000..280fa4c93 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ICustomDynamicMappingTargetMemberSpecifier.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + using System; + using System.Linq.Expressions; + + /// + /// Provides options for specifying a target member to which an ExpandoObject configuration should apply. + /// + /// The target type to which the configuration should apply. + public interface ICustomDynamicMappingTargetMemberSpecifier + { + /// + /// Apply the configuration to the given . + /// + /// The target member's type. + /// The target member to which to apply the configuration. + /// + /// An ISourceDynamicMappingConfigContinuation to enable further configuration of mappings from + /// Dynamics to the target type being configured. + /// + ISourceDynamicMappingConfigContinuation To( + Expression> targetMember); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ICustomTargetDynamicMemberNameSpecifier.cs b/AgileMapper/Api/Configuration/Dynamics/ICustomTargetDynamicMemberNameSpecifier.cs new file mode 100644 index 000000000..f012c85a0 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ICustomTargetDynamicMemberNameSpecifier.cs @@ -0,0 +1,40 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Provides options for specifying custom target ExpandoObject member names to which configured + /// source members should be mapped. + /// + public interface ICustomTargetDynamicMemberNameSpecifier + { + /// + /// Configure a custom full ExpandoObject member name to use in place of the configured source + /// member's name when constructing a target ExpandoObject member name. For example, calling + /// Map(address => address.Line1).ToFullMemberName("StreetName") will generate the key 'StreetName' + /// when mapping an Address.Line1 property to an ExpandoObject, instead of the default 'Address_Line1'. + /// + /// + /// The member name to which to map the value of the configured source member. + /// + /// + /// An ITargetDynamicMappingConfigContinuation to enable further configuration of mappings between the + /// source and target types being configured. + /// + ITargetDynamicMappingConfigContinuation ToFullMemberName(string fullMemberName); + + /// + /// Use the given in place of the configured source member's name + /// when constructing a target ExpandoObject member name. For example, calling + /// Map(address => address.Line1).ToMemberName("StreetName") will generate the member name + /// 'Address_StreetName' when mapping an Address.Line1 property to an ExpandoObject, instead of + /// the default 'Address_Line1'. + /// + /// + /// The member name to use in place of the configured source member's name. + /// + /// + /// An ITargetDynamicMappingConfigContinuation to enable further configuration of mappings between the + /// source and target types being configured. + /// + ITargetDynamicMappingConfigContinuation ToMemberName(string memberName); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/IGlobalDynamicSettings.cs b/AgileMapper/Api/Configuration/Dynamics/IGlobalDynamicSettings.cs new file mode 100644 index 000000000..11ca0bcfd --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/IGlobalDynamicSettings.cs @@ -0,0 +1,61 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Provides options for configuring how this mapper will perform mappings from source ExpandoObjects. + /// + public interface IGlobalDynamicSettings + { + /// + /// Construct flattened member names for source and target Dynamic members. For example, + /// an ExpandoObject.Address.StreetName member would be mapped to a Dynamic member with the + /// name 'AddressStreetName'. + /// + /// + /// The with which to configure other global aspects of + /// ExpandoObject mapping. + /// + IGlobalDynamicSettings UseFlattenedTargetMemberNames(); + + /// + /// Use the given to construct source and target ExpandoObject + /// member names, and to separate member names when mapping to target ExpandoObject nested + /// complex type members - the default is '_'. For example, calling UseMemberNameSeparator("-") + /// will require a source ExpandoObject member with the name 'Address-Line1' to map to an + /// Address.Line1 member. Any string can be specified as a separator - even if it would create + /// illegal member names like 'Address-Line1' - because ExpandoObjects are mapped as + /// IDictionary{string, Object}s. + /// + /// + /// The separator to use to separate member names when constructing expected source Dynamic + /// member names for nested members. + /// + /// + /// The with which to configure other global aspects of + /// ExpandoObject mapping. + /// + IGlobalDynamicSettings UseMemberNameSeparator(string separator); + + /// + /// Use the given to create the part of a sourec or target Dynamic member + /// name representing an enumerable element - the default is '_i_'. The pattern must contain a single + /// 'i' character as a placeholder for the enumerable index. For example, calling UseElementKeyPattern("-i-") + /// and mapping from a Dynamic to a collection of ints will generate searches for member names '-0-', + /// '-1-', '-2-', etc. Any pattern can be specified as an element key - even if it would create illegal + /// member names like '-0-' - because ExpandoObjects are mapped as IDictionary{string, Object}s. + /// + /// + /// The pattern to use to create an expected source Dynamic member name part representing an enumerable + /// element. + /// + /// + /// The with which to configure other global aspects of ExpandoObject + /// mapping. + /// + IGlobalDynamicSettings UseElementKeyPattern(string pattern); + + /// + /// Gets a link back to the full , for api fluency. + /// + MappingConfigStartingPoint AndWhenMapping { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicConfigSettings.cs b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicConfigSettings.cs new file mode 100644 index 000000000..7faa92559 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicConfigSettings.cs @@ -0,0 +1,53 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Provides options for configuring how mappers will perform mappings from Dynamics to the given + /// . + /// + /// The target type to which the configuration should apply. + public interface ISourceDynamicConfigSettings + { + /// + /// Use the given to construct expected source ExpandoObject + /// member names, and to separate member names when mapping to nested complex type members of + /// any target type - the default is '_'. For example, calling UseMemberNameSeparator("-") + /// will require a source ExpandoObject member with the name 'Address-Line1' to map to an + /// Address.Line1 member. Any string can be specified as a separator - even if it would create + /// illegal member names like 'Address-Line1' - because ExpandoObjects are mapped as + /// IDictionary{string, object}s. + /// + /// + /// The separator to use to separate member names when constructing expected source Dynamic + /// member names for nested members. + /// + /// + /// The with which to configure other + /// aspects of source ExpandoObject mapping. + /// + ISourceDynamicConfigSettings UseMemberNameSeparator(string separator); + + /// + /// Use the given to create the part of an expected Dynamic member name + /// representing an enumerable element - the default is '_i_'. The pattern must contain a single 'i' + /// character as a placeholder for the enumerable index. Any pattern can be specified as an element + /// key - even if it would create illegal member names like '0-OrderItemId' - because ExpandoObjects + /// are mapped as IDictionary{string, Object}s. For example, calling UseElementKeyPattern("-i-") and + /// mapping from a Dynamic to a collection of ints will generate searches for member names '-0-', '-1-', + /// '-2-', etc. + /// + /// + /// The pattern to use to create an expected source Dynamic member name part representing an enumerable + /// element. + /// + /// + /// The with which to configure other + /// aspects of source ExpandoObject mapping. + /// + ISourceDynamicConfigSettings UseElementKeyPattern(string pattern); + + /// + /// Gets a link back to the full ISourceDynamicMappingConfigurator, for api fluency. + /// + ISourceDynamicMappingConfigurator And { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicMappingConfigContinuation.cs b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicMappingConfigContinuation.cs new file mode 100644 index 000000000..75f6e78dd --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicMappingConfigContinuation.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Enables chaining of configurations for an ExpandoObject to the same target type. + /// + /// The target type to which the configuration should apply. + public interface ISourceDynamicMappingConfigContinuation + { + /// + /// Perform another configuration of how this mapper maps from an ExpandoObject to the target type + /// being configured. This property exists purely to provide a more fluent configuration interface. + /// + ISourceDynamicMappingConfigurator And { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicMappingConfigurator.cs new file mode 100644 index 000000000..9359a0319 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicMappingConfigurator.cs @@ -0,0 +1,41 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + using System.Collections.Generic; + + /// + /// Provides options for configuring mappings from an ExpandoObject to a given . + /// + /// The target type to which the configuration should apply. + // ReSharper disable once PossibleInterfaceMemberAmbiguity + public interface ISourceDynamicMappingConfigurator : + IFullMappingConfigurator, TTarget>, + ISourceDynamicConfigSettings + { + /// + /// Configure a custom source member for a particular target member when mapping from an ExpandoObject + /// to the target type being configured. + /// + /// + /// The name of the source member from which to retrieve the value to map to the configured target member. + /// + /// + /// An ICustomDynamicMappingTargetMemberSpecifier with which to specify the target member for which the + /// member with the given should be used. + /// + ICustomDynamicMappingTargetMemberSpecifier MapFullMemberName(string sourceMemberName); + + /// + /// Configure a custom member name to use in a key for a particular target member when mapping from an + /// ExpandoObject to the target type being configured. For example, to map the member "Address.HouseName" + /// to a 'Line1' member of an 'Address' member, use MapMemberName("HouseName").To(a => a.Line1). + /// + /// + /// The custom member name to use in a key with which to retrieve the value to map to the configured target member. + /// + /// + /// A CustomDictionaryMappingTargetMemberSpecifier with which to specify the target member for which the custom + /// member name should be used. + /// + ICustomDynamicMappingTargetMemberSpecifier MapMemberName(string memberNamePart); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicSettings.cs b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicSettings.cs new file mode 100644 index 000000000..bcd99c5da --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicSettings.cs @@ -0,0 +1,62 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Provides options for configuring how this mapper will perform mappings from source ExpandoObjects. + /// + public interface ISourceDynamicSettings + { + /// + /// Construct flattened member names for target Dynamic members. For example, an + /// ExpandoObject.Address.StreetName member would be mapped to a Dynamic member with the + /// name 'AddressStreetName'. + /// + /// + /// The with which to configure other aspects of source + /// ExpandoObject mapping. + /// + ISourceDynamicSettings UseFlattenedTargetMemberNames(); + + /// + /// Use the given to construct expected source ExpandoObject + /// member names, and to separate member names when mapping to nested complex type members of + /// any target type - the default is '_'. For example, calling UseMemberNameSeparator("-") + /// will require a source ExpandoObject member with the name 'Address-Line1' to map to an + /// Address.Line1 member. Any string can be specified as a separator - even if it would create + /// illegal member names like 'Address-Line1' - because ExpandoObjects are mapped as + /// IDictionary{string, object}s. + /// + /// + /// The separator to use to separate member names when constructing expected source Dynamic + /// member names for nested members. + /// + /// + /// The with which to configure other aspects of source + /// ExpandoObject mapping. + /// + ISourceDynamicSettings UseMemberNameSeparator(string separator); + + /// + /// Use the given to create the part of an expected Dynamic member name + /// representing an enumerable element - the default is '_i_'. The pattern must contain a single 'i' + /// character as a placeholder for the enumerable index. For example, calling UseElementKeyPattern("-i-") + /// and mapping from a Dynamic to a collection of ints will generate searches for member names '-0-', '-1-', + /// '-2-', etc. Any pattern can be specified as an element key - even if it would create illegal member + /// names like '-0-' - because ExpandoObjects are mapped as IDictionary{string, Object}s. + /// + /// + /// The pattern to use to create an expected source Dynamic member name part representing an enumerable + /// element. + /// + /// + /// The with which to configure other aspects of source ExpandoObject + /// mapping. + /// + ISourceDynamicSettings UseElementKeyPattern(string pattern); + + /// + /// Gets a link back to the full , + /// for api fluency. + /// + ISourceDynamicTargetTypeSelector AndWhenMapping { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs new file mode 100644 index 000000000..ce0dadb62 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs @@ -0,0 +1,40 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Provides options for specifying the type of ExpandoObject mapping to perform. + /// + public interface ISourceDynamicTargetTypeSelector : ISourceDynamicSettings + { + /// + /// Configure how this mapper performs mappings from ExpandoObjects in all MappingRuleSets + /// (create new, overwrite, etc), to the target type specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDynamicMappingConfigurator with which to complete the configuration. + ISourceDynamicMappingConfigurator To(); + + /// + /// Configure how this mapper performs object creation mappings from ExpandoObjects to the target type + /// specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDynamicMappingConfigurator with which to complete the configuration. + ISourceDynamicMappingConfigurator ToANew(); + + /// + /// Configure how this mapper performs OnTo (merge) mappings from ExpandoObjects to the target + /// type specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDynamicMappingConfigurator with which to complete the configuration. + ISourceDynamicMappingConfigurator OnTo(); + + /// + /// Configure how this mapper performs Over (overwrite) mappings from ExpandoObjects to the target + /// type specified by the type argument. + /// + /// The target type to which the configuration will apply. + /// An ISourceDynamicMappingConfigurator with which to complete the configuration. + ISourceDynamicMappingConfigurator Over(); + } +} diff --git a/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicConfigSettings.cs b/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicConfigSettings.cs new file mode 100644 index 000000000..0dc0c1c43 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicConfigSettings.cs @@ -0,0 +1,59 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Provides options for configuring how mappers will perform mappings to dictionaries. + /// + /// The source type to which the configuration should apply. + public interface ITargetDynamicConfigSettings + { + /// + /// Construct ExpandoObject member names for nested members using flattened member names - the default is + /// to separate member names with '_'. For example, a Person.Address.StreetName member would be mapped to + /// an ExpandoObject member with name 'AddressStreetName' when mapping from a root Person object. + /// + /// + /// An ITargetDynamicConfigSettings to enable further configuration of mappings from the source type + /// being configured to ExpandoObjects. + /// + ITargetDynamicConfigSettings UseFlattenedMemberNames(); + + /// + /// Use the given to separate member names when mapping from nested complex + /// type members to ExpandoObjects - the default is '_'. For example, calling UseMemberNameSeparator("-") + /// will create an ExpandoObject member with the name 'Address-Line1' when mapping from an Address.Line1 + /// member. Any string can be specified as a separator - even if it would create illegal member names like + /// 'Address-Line1' - because ExpandoObjects are mapped as IDictionary{string, object}s. + /// + /// + /// The separator to use to separate member names when constructing ExpandoObject member names for nested + /// members. + /// + /// + /// An ITargetDynamicConfigSettings to enable further configuration of mappings from the source type + /// being configured to ExpandoObjects. + /// + ITargetDynamicConfigSettings UseMemberNameSeparator(string separator); + + /// + /// Use the given to create the part of an ExpandoObject member name + /// representing an enumerable element - the default is '_i_. The pattern must contain a single 'i' + /// character as a placeholder for the enumerable index. For example, calling UseElementKeyPattern("(i)") + /// and mapping from a collection of ints to a Dictionary will generate keys '(0)', '(1)', '(2)', + /// etc. Any pattern can be specified as an element key - even if it would create illegal member names + /// like '(0)' - because ExpandoObjects are mapped as IDictionary{string, Object}s. + /// + /// + /// The pattern to use to create a Dictionary key part representing an enumerable element. + /// + /// + /// An ITargetDynamicConfigSettings to enable further configuration of mappings from the source type + /// being configured to ExpandoObjects. + /// + ITargetDynamicConfigSettings UseElementKeyPattern(string pattern); + + /// + /// Gets a link back to the full ITargetDictionaryMappingConfigurator, for api fluency. + /// + ITargetDynamicMappingConfigurator And { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicMappingConfigContinuation.cs b/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicMappingConfigContinuation.cs new file mode 100644 index 000000000..e498f786f --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicMappingConfigContinuation.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + /// + /// Enables chaining of configurations for the same source and target type. + /// + /// The source type to which the configuration should apply. + public interface ITargetDynamicMappingConfigContinuation + { + /// + /// Perform another configuration of how this mapper maps to and from the source and target types + /// being configured. This property exists purely to provide a more fluent configuration interface. + /// + ITargetDynamicMappingConfigurator And { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicMappingConfigurator.cs new file mode 100644 index 000000000..892fe8938 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/ITargetDynamicMappingConfigurator.cs @@ -0,0 +1,28 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + + /// + /// Provides options for configuring mappings from a to an ExpandoObject. + /// + /// The source type to which the configuration should apply. + // ReSharper disable once PossibleInterfaceMemberAmbiguity + public interface ITargetDynamicMappingConfigurator : + IFullMappingConfigurator>, + ITargetDynamicConfigSettings + { + /// + /// Map the given member using a custom ExpandoObject member name. + /// + /// The source member's type. + /// The source member to which to apply the configuration. + /// + /// A CustomTargetDictionaryKeySpecifier with which to specify the custom key to use when mapping + /// the given . + /// + ICustomTargetDynamicMemberNameSpecifier MapMember( + Expression> sourceMember); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/SourceDynamicMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dynamics/SourceDynamicMappingConfigurator.cs new file mode 100644 index 000000000..22e311607 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/SourceDynamicMappingConfigurator.cs @@ -0,0 +1,40 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + using System.Collections.Generic; + using AgileMapper.Configuration; + using Dictionaries; + + internal class SourceDynamicMappingConfigurator : + DictionaryMappingConfiguratorBase, TTarget>, + ISourceDynamicMappingConfigurator + { + public SourceDynamicMappingConfigurator(MappingConfigInfo configInfo) + : base(configInfo) + { + } + + #region ISourceDynamicConfigSettings Members + + public ISourceDynamicConfigSettings UseMemberNameSeparator(string separator) + { + SetupMemberNameSeparator(separator); + return this; + } + + public ISourceDynamicConfigSettings UseElementKeyPattern(string pattern) + { + SetupElementKeyPattern(pattern); + return this; + } + + ISourceDynamicMappingConfigurator ISourceDynamicConfigSettings.And => this; + + #endregion + + public ICustomDynamicMappingTargetMemberSpecifier MapFullMemberName(string sourceMemberName) + => MapFullKey(sourceMemberName); + + public ICustomDynamicMappingTargetMemberSpecifier MapMemberName(string memberNamePart) + => MapMemberNameKey(memberNamePart); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs new file mode 100644 index 000000000..9343e05f5 --- /dev/null +++ b/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs @@ -0,0 +1,66 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics +{ + using System; + using System.Collections.Generic; + using System.Dynamic; + using System.Linq.Expressions; + using AgileMapper.Configuration; + using Dictionaries; + using Members; + using ReadableExpressions; + + internal class TargetDynamicMappingConfigurator : + DictionaryMappingConfiguratorBase>, + ITargetDynamicMappingConfigurator + { + public TargetDynamicMappingConfigurator(MappingConfigInfo configInfo) + : base(configInfo.ForTargetType()) + { + } + + #region ITargetDynamicConfigSettings Members + + public ITargetDynamicConfigSettings UseFlattenedMemberNames() + { + SetupFlattenedTargetMemberNames(); + return this; + } + + public ITargetDynamicConfigSettings UseMemberNameSeparator(string separator) + { + SetupMemberNameSeparator(separator); + return this; + } + + public ITargetDynamicConfigSettings UseElementKeyPattern(string pattern) + { + SetupElementKeyPattern(pattern); + return this; + } + + ITargetDynamicMappingConfigurator ITargetDynamicConfigSettings.And => this; + + #endregion + + public ICustomTargetDynamicMemberNameSpecifier MapMember( + Expression> sourceMember) + { + var sourceQualifiedMember = GetSourceMemberOrThrow(sourceMember); + + return new CustomTargetDictionaryKeySpecifier(ConfigInfo, sourceQualifiedMember); + } + + private QualifiedMember GetSourceMemberOrThrow(LambdaExpression lambda) + { + var sourceMember = lambda.Body.ToSourceMember(ConfigInfo.MapperContext); + + if (sourceMember != null) + { + return sourceMember; + } + + throw new MappingConfigurationException( + $"Source member {lambda.Body.ToReadableString()} is not readable."); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/EnumPairSpecifier.cs b/AgileMapper/Api/Configuration/EnumPairSpecifier.cs index 354554185..9e8af13bc 100644 --- a/AgileMapper/Api/Configuration/EnumPairSpecifier.cs +++ b/AgileMapper/Api/Configuration/EnumPairSpecifier.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Linq; using AgileMapper.Configuration; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; using ReadableExpressions.Extensions; diff --git a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs index bdfd63124..fa4e9cc41 100644 --- a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs @@ -1,13 +1,18 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using System; + using System.Dynamic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; using Dictionaries; - using Extensions; + using Dynamics; + using Extensions.Internal; using Members; + using static Constants; + using static AgileMapper.Configuration.Dictionaries.DictionaryType; /// /// Provides options for configuring how a mapper performs a mapping. @@ -17,13 +22,8 @@ public class MappingConfigStartingPoint : IGlobalConfigSettings private readonly MappingConfigInfo _configInfo; internal MappingConfigStartingPoint(MapperContext mapperContext) - : this(new MappingConfigInfo(mapperContext)) { - } - - internal MappingConfigStartingPoint(MappingConfigInfo configInfo) - { - _configInfo = configInfo; + _configInfo = new MappingConfigInfo(mapperContext); } private MapperContext MapperContext => _configInfo.MapperContext; @@ -343,10 +343,33 @@ public InstanceConfigurator InstancesOf(TObject exampleInstanc public InstanceConfigurator InstancesOf() where TObject : class => new InstanceConfigurator(GlobalConfigInfo); + #region Dictionaries + + /// + /// Configure how this mapper performs mappings from or to source Dictionary instances + /// with any Dictionary value type. + /// + public IGlobalDictionarySettings Dictionaries + => CreateDictionaryConfigurator(Dictionary, sourceValueType: AllTypes); + + /// + /// Configure how this mapper performs mappings from or to source Dictionary{string, TValue} instances. + /// + /// + /// The type of values contained in the Dictionary to which the configuration will apply. + /// + /// + /// An IGlobalDictionarySettings with which to continue other global aspects of Dictionary mapping. + /// + public IGlobalDictionarySettings DictionariesWithValueType() + => CreateDictionaryConfigurator(Dictionary); + /// - /// Configure how this mapper performs mappings from source Dictionary{string, T} instances. + /// Configure how this mapper performs mappings from source Dictionary instances with + /// any Dictionary value type. /// - public DictionaryConfigurator Dictionaries => DictionariesWithValueType(); + public ISourceDictionaryTargetTypeSelector FromDictionaries + => CreateDictionaryConfigurator(Dictionary, sourceValueType: AllTypes); /// /// Configure how this mapper performs mappings from source Dictionary{string, TValue} instances. @@ -354,9 +377,39 @@ public InstanceConfigurator InstancesOf() where TObject : clas /// /// The type of values contained in the Dictionary to which the configuration will apply. /// - /// A DictionaryConfigurator with which to continue the configuration. - public DictionaryConfigurator DictionariesWithValueType() - => new DictionaryConfigurator(_configInfo.ForAllSourceTypes()); + /// + /// An ISourceDictionaryTargetTypeSelector with which to specify to which target type the + /// configuration will apply. + /// + public ISourceDictionaryTargetTypeSelector FromDictionariesWithValueType() + => CreateDictionaryConfigurator(Dictionary); + + /// + /// Configure how this mapper performs mappings from or to ExpandoObject instances. + /// + public IGlobalDynamicSettings Dynamics + => CreateDictionaryConfigurator(Expando, sourceValueType: AllTypes); + + /// + /// Configure how this mapper performs mappings from source ExpandoObject instances. + /// + public ISourceDynamicTargetTypeSelector FromDynamics + => CreateDictionaryConfigurator(Expando, typeof(ExpandoObject), sourceValueType: AllTypes); + + private DictionaryMappingConfigurator CreateDictionaryConfigurator( + DictionaryType dictionaryType, + Type sourceType = null, + Type sourceValueType = null) + { + var configInfo = _configInfo + .ForSourceType(sourceType ?? AllTypes) + .ForSourceValueType(sourceValueType ?? typeof(TValue)) + .Set(dictionaryType); + + return new DictionaryMappingConfigurator(configInfo); + } + + #endregion /// /// Configure how this mapper performs mappings from the source type specified by the given @@ -391,7 +444,7 @@ public IFullMappingConfigurator To() /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. public IFullMappingConfigurator ToANew() - => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Constants.CreateNew)).ToANew(); + => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(CreateNew)).ToANew(); /// /// Configure how this mapper performs OnTo (merge) mappings from any source type to the target type @@ -400,7 +453,7 @@ public IFullMappingConfigurator ToANew() /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. public IFullMappingConfigurator OnTo() - => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Constants.Merge)).OnTo(); + => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Merge)).OnTo(); /// /// Configure how this mapper performs Over (overwrite) mappings from any source type to the target type @@ -409,7 +462,7 @@ public IFullMappingConfigurator OnTo() /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. public IFullMappingConfigurator Over() - => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Constants.Overwrite)).Over(); + => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Overwrite)).Over(); private TargetTypeSpecifier GetAllSourcesTargetTypeSpecifier( Func configInfoConfigurator) diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index 68af19692..a6a3101b2 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; - using Extensions; + using Extensions.Internal; using Members; using Validation; @@ -14,7 +14,12 @@ internal class MappingConfigurator : { public MappingConfigurator(MappingConfigInfo configInfo) { - ConfigInfo = configInfo.ForTargetType(); + ConfigInfo = configInfo; + + if ((ConfigInfo.TargetType ?? typeof(object)) == typeof(object)) + { + ConfigInfo.ForTargetType(); + } } protected MappingConfigInfo ConfigInfo { get; } diff --git a/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs b/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs index 70340ec11..b616241dc 100644 --- a/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs +++ b/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs @@ -1,7 +1,9 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using AgileMapper.Configuration; + using AgileMapper.Configuration.Dictionaries; using Dictionaries; + using Dynamics; /// /// Provides options for specifying the target type and mapping rule set to which the configuration should @@ -58,7 +60,7 @@ private MappingConfigurator UsingRuleSet(string name) /// /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets - /// (create new, overwrite, etc), to target dictionaries. + /// (create new, overwrite, etc), to target Dictionaries. /// public ITargetDictionaryMappingConfigurator ToDictionaries => ToDictionariesWithValueType(); @@ -71,6 +73,13 @@ private MappingConfigurator UsingRuleSet(string name) /// /// An ITargetDictionaryConfigSettings with which to continue the configuration. public ITargetDictionaryMappingConfigurator ToDictionariesWithValueType() - => new TargetDictionaryMappingConfigurator(_configInfo.ForAllRuleSets()); + => new TargetDictionaryMappingConfigurator(_configInfo.ForAllRuleSets().Set(DictionaryType.Dictionary)); + + /// + /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets + /// (create new, overwrite, etc), to target ExpandoObjects. + /// + public ITargetDynamicMappingConfigurator ToDynamics + => new TargetDynamicMappingConfigurator(_configInfo.ForAllRuleSets().Set(DictionaryType.Expando)); } } \ No newline at end of file diff --git a/AgileMapper/Configuration/ConfiguredItemExtensions.cs b/AgileMapper/Configuration/ConfiguredItemExtensions.cs index 34c993eaa..6618bb59b 100644 --- a/AgileMapper/Configuration/ConfiguredItemExtensions.cs +++ b/AgileMapper/Configuration/ConfiguredItemExtensions.cs @@ -9,13 +9,13 @@ internal static class ConfiguredItemExtensions public static TItem FindMatch(this IEnumerable items, IBasicMapperData mapperData) where TItem : UserConfiguredItemBase { - return items?.FirstOrDefault(im => im.AppliesTo(mapperData)); + return items?.FirstOrDefault(item => item.AppliesTo(mapperData)); } public static IEnumerable FindMatches(this IEnumerable items, IBasicMapperData mapperData) where TItem : UserConfiguredItemBase { - return items?.Where(item => item.AppliesTo(mapperData)).OrderBy(im => im) ?? Enumerable.Empty; + return items?.Where(item => item.AppliesTo(mapperData)).OrderBy(item => item) ?? Enumerable.Empty; } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/ConfiguredLambdaInfo.cs b/AgileMapper/Configuration/ConfiguredLambdaInfo.cs index 3da954940..0db220cdb 100644 --- a/AgileMapper/Configuration/ConfiguredLambdaInfo.cs +++ b/AgileMapper/Configuration/ConfiguredLambdaInfo.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; using ObjectPopulation; diff --git a/AgileMapper/Configuration/DerivedTypePairSet.cs b/AgileMapper/Configuration/DerivedTypePairSet.cs index 14fa48708..98e61552e 100644 --- a/AgileMapper/Configuration/DerivedTypePairSet.cs +++ b/AgileMapper/Configuration/DerivedTypePairSet.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/Configuration/CustomDictionaryKey.cs b/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs similarity index 61% rename from AgileMapper/Configuration/CustomDictionaryKey.cs rename to AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs index 7fb57ff66..0770d890d 100644 --- a/AgileMapper/Configuration/CustomDictionaryKey.cs +++ b/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs @@ -1,8 +1,10 @@ -namespace AgileObjects.AgileMapper.Configuration +namespace AgileObjects.AgileMapper.Configuration.Dictionaries { using System; + using System.Dynamic; using System.Linq.Expressions; using DataSources; + using Extensions.Internal; using Members; internal class CustomDictionaryKey : UserConfiguredItemBase @@ -47,17 +49,35 @@ public static CustomDictionaryKey ForTargetMember( public string Key { get; } public string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) - { - return $"Configured dictionary key member {TargetMember.GetPath()} has a configured data source"; - } + => $"Configured dictionary key member {TargetMember.GetPath()} has a configured data source"; - public bool AppliesTo(Member member, IBasicMapperData mapperData) + public bool AppliesTo(Member member, IMemberMapperData mapperData) { if (!base.AppliesTo(mapperData)) { return false; } + if (((ConfigInfo.SourceValueType ?? Constants.AllTypes) != Constants.AllTypes) && + (mapperData.SourceType.GetDictionaryTypes().Value != ConfigInfo.SourceValueType)) + { + return false; + } + + var applicableDictionaryType = ConfigInfo.Get(); + + if ((applicableDictionaryType != DictionaryType.Expando) && + IsPartOfExpandoObjectMapping(mapperData)) + { + return false; + } + + if ((applicableDictionaryType == DictionaryType.Expando) && + !IsPartOfExpandoObjectMapping(mapperData)) + { + return false; + } + if (_sourceMember == null) { return true; @@ -68,6 +88,22 @@ public bool AppliesTo(Member member, IBasicMapperData mapperData) return _sourceMember.Matches(targetMember); } + private static bool IsPartOfExpandoObjectMapping(IMemberMapperData mapperData) + { + while (mapperData != null) + { + if ((mapperData.SourceMember.GetFriendlyTypeName() == nameof(ExpandoObject)) || + (mapperData.TargetMember.GetFriendlyTypeName() == nameof(ExpandoObject))) + { + return true; + } + + mapperData = mapperData.Parent; + } + + return false; + } + private QualifiedMember GetTargetMember(Member member, IBasicMapperData mapperData) { if (mapperData.TargetMember.LeafMember == member) diff --git a/AgileMapper/Configuration/Dictionaries/DictionaryContext.cs b/AgileMapper/Configuration/Dictionaries/DictionaryContext.cs new file mode 100644 index 000000000..c71d3dc63 --- /dev/null +++ b/AgileMapper/Configuration/Dictionaries/DictionaryContext.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.Configuration.Dictionaries +{ + internal enum DictionaryContext + { + All, + SourceOnly + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/Dictionaries/DictionaryKeyPartFactoryBase.cs b/AgileMapper/Configuration/Dictionaries/DictionaryKeyPartFactoryBase.cs new file mode 100644 index 000000000..7574364eb --- /dev/null +++ b/AgileMapper/Configuration/Dictionaries/DictionaryKeyPartFactoryBase.cs @@ -0,0 +1,21 @@ +namespace AgileObjects.AgileMapper.Configuration.Dictionaries +{ + using ReadableExpressions.Extensions; + + internal abstract class DictionaryKeyPartFactoryBase : UserConfiguredItemBase + { + + protected DictionaryKeyPartFactoryBase(MappingConfigInfo configInfo) + : base(configInfo) + { + IsForAllTargetTypes = configInfo.TargetType == typeof(object); + } + + protected bool IsForAllTargetTypes { get; } + + public abstract string GetConflictMessage(); + + protected string TargetScopeDescription + => IsForAllTargetTypes ? "globally" : "for target type " + ConfigInfo.TargetType.GetFriendlyName(); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/DictionarySettings.cs b/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs similarity index 64% rename from AgileMapper/Configuration/DictionarySettings.cs rename to AgileMapper/Configuration/Dictionaries/DictionarySettings.cs index 6a646ffd1..f7f4e4b83 100644 --- a/AgileMapper/Configuration/DictionarySettings.cs +++ b/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs @@ -1,10 +1,9 @@ -namespace AgileObjects.AgileMapper.Configuration +namespace AgileObjects.AgileMapper.Configuration.Dictionaries { using System.Collections.Generic; - using System.Globalization; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal class DictionarySettings @@ -21,11 +20,15 @@ public DictionarySettings(MapperContext mapperContext) _joiningNameFactories = new List { + JoiningNameFactory.UnderscoredForSourceDynamics(mapperContext), + JoiningNameFactory.UnderscoredForTargetDynamics(mapperContext), JoiningNameFactory.Dotted(mapperContext) }; _elementKeyPartFactories = new List { + ElementKeyPartFactory.UnderscoredIndexForSourceDynamics(mapperContext), + ElementKeyPartFactory.UnderscoredIndexForTargetDynamics(mapperContext), ElementKeyPartFactory.SquareBracketedIndex(mapperContext) }; } @@ -35,10 +38,10 @@ public void AddFullKey(CustomDictionaryKey configuredKey) _configuredFullKeys.Add(configuredKey); } - public Expression GetFullKeyOrNull(IBasicMapperData mapperData) + public Expression GetFullKeyOrNull(IMemberMapperData mapperData) => GetFullKeyValueOrNull(mapperData)?.ToConstantExpression(); - public string GetFullKeyValueOrNull(IBasicMapperData mapperData) + public string GetFullKeyValueOrNull(IMemberMapperData mapperData) { if (mapperData.TargetMember.IsCustom) { @@ -58,60 +61,67 @@ public void AddMemberKey(CustomDictionaryKey customKey) _configuredMemberKeys.Add(customKey); } - public string GetMemberKeyOrNull(IBasicMapperData mapperData) + public string GetMemberKeyOrNull(IMemberMapperData mapperData) => GetMemberKeyOrNull(mapperData.TargetMember.LeafMember, mapperData); - public string GetMemberKeyOrNull(Member member, IBasicMapperData mapperData) + public string GetMemberKeyOrNull(Member member, IMemberMapperData mapperData) => FindKeyOrNull(_configuredMemberKeys, member, mapperData)?.Key; private static CustomDictionaryKey FindKeyOrNull( IEnumerable keys, Member member, - IBasicMapperData mapperData) + IMemberMapperData mapperData) => keys.FirstOrDefault(k => k.AppliesTo(member, mapperData)); public void Add(JoiningNameFactory joiningNameFactory) { - ThrowIfConflictingJoiningNameFactoryExists(joiningNameFactory); + ThrowIfConflictingKeyPartFactoryExists(joiningNameFactory, _joiningNameFactories); _joiningNameFactories.Insert(0, joiningNameFactory); } - private void ThrowIfConflictingJoiningNameFactoryExists(JoiningNameFactory joiningNameFactory) + public Expression GetSeparator(IMemberMapperData mapperData) + => _joiningNameFactories.FindMatch(mapperData).Separator; + + public Expression GetJoiningName(Member member, IMemberMapperData mapperData) + => _joiningNameFactories.FindMatch(mapperData).GetJoiningName(member, mapperData); + + public void Add(ElementKeyPartFactory keyPartFactory) { - if (_joiningNameFactories.HasOne()) + ThrowIfConflictingKeyPartFactoryExists(keyPartFactory, _elementKeyPartFactories); + + _elementKeyPartFactories.Insert(0, keyPartFactory); + } + + private static void ThrowIfConflictingKeyPartFactoryExists( + TKeyPartFactory factory, + ICollection existingFactories) + where TKeyPartFactory : DictionaryKeyPartFactoryBase + { + if (existingFactories.HasOne()) { return; } - var conflictingJoiningName = _joiningNameFactories - .FirstOrDefault(jnf => jnf.ConflictsWith(joiningNameFactory)); + var conflictingFactory = existingFactories + .FirstOrDefault(kpf => kpf.ConflictsWith(factory)); - if (conflictingJoiningName == null) + if (conflictingFactory == null) { return; } - throw new MappingConfigurationException(string.Format( - CultureInfo.InvariantCulture, - "Member names are already configured {0} to be {1}", - conflictingJoiningName.TargetScopeDescription, - conflictingJoiningName.SeparatorDescription)); + throw new MappingConfigurationException(conflictingFactory.GetConflictMessage()); } - public Expression GetJoiningName(Member member, IMemberMapperData mapperData) - => _joiningNameFactories.FindMatch(mapperData).GetJoiningName(member, mapperData); - - public void Add(ElementKeyPartFactory keyPartFactory) - { - _elementKeyPartFactories.Insert(0, keyPartFactory); - } + public Expression GetElementKeyPartMatcher(IBasicMapperData mapperData) + => _elementKeyPartFactories.FindMatch(mapperData).GetElementKeyPartMatcher(); public Expression GetElementKeyPrefixOrNull(IBasicMapperData mapperData) => _elementKeyPartFactories.FindMatch(mapperData).GetElementKeyPrefixOrNull(); - public IEnumerable GetElementKeyParts(Expression index, IBasicMapperData mapperData) - => _elementKeyPartFactories.FindMatch(mapperData).GetElementKeyParts(index); + public IList GetElementKeyParts(Expression index, IBasicMapperData mapperData) + => _elementKeyPartFactories.FindMatch(mapperData).GetElementKeyParts(index).ToArray(); public void CloneTo(DictionarySettings dictionaries) { diff --git a/AgileMapper/Configuration/Dictionaries/DictionaryType.cs b/AgileMapper/Configuration/Dictionaries/DictionaryType.cs new file mode 100644 index 000000000..b5a179ede --- /dev/null +++ b/AgileMapper/Configuration/Dictionaries/DictionaryType.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.Configuration.Dictionaries +{ + internal enum DictionaryType + { + Dictionary, + Expando + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/Dictionaries/ElementKeyPartFactory.cs b/AgileMapper/Configuration/Dictionaries/ElementKeyPartFactory.cs new file mode 100644 index 000000000..39e77c80c --- /dev/null +++ b/AgileMapper/Configuration/Dictionaries/ElementKeyPartFactory.cs @@ -0,0 +1,181 @@ +namespace AgileObjects.AgileMapper.Configuration.Dictionaries +{ + using System.Collections.Generic; + using System.Dynamic; + using System.Linq.Expressions; + using System.Text.RegularExpressions; + using Extensions.Internal; + using Members; + using ReadableExpressions.Extensions; + + internal class ElementKeyPartFactory : DictionaryKeyPartFactoryBase + { + private readonly string _prefixString; + private readonly ConstantExpression _prefix; + private readonly string _suffixString; + private readonly ConstantExpression _suffix; + private Expression _keyPartMatcher; + + private ElementKeyPartFactory( + string prefix, + string suffix, + MappingConfigInfo configInfo) + : base(configInfo) + { + if (!string.IsNullOrEmpty(prefix)) + { + _prefixString = prefix; + _prefix = prefix.ToConstantExpression(); + } + + if (!string.IsNullOrEmpty(suffix)) + { + _suffixString = suffix; + _suffix = suffix.ToConstantExpression(); + } + } + + #region Factory Methods + + public static ElementKeyPartFactory UnderscoredIndexForSourceDynamics(MapperContext mapperContext) + { + var sourceExpandoObject = new MappingConfigInfo(mapperContext) + .ForAllRuleSets() + .ForAllSourceTypes() + .ForTargetType(); + + return new ElementKeyPartFactory("_", "_", sourceExpandoObject); + } + + public static ElementKeyPartFactory UnderscoredIndexForTargetDynamics(MapperContext mapperContext) + { + var sourceExpandoObject = new MappingConfigInfo(mapperContext) + .ForAllRuleSets() + .ForSourceType() + .ForAllTargetTypes(); + + return new ElementKeyPartFactory("_", "_", sourceExpandoObject); + } + + public static ElementKeyPartFactory SquareBracketedIndex(MapperContext mapperContext) + => new ElementKeyPartFactory("[", "]", MappingConfigInfo.AllRuleSetsSourceTypesAndTargetTypes(mapperContext)); + + private static readonly Regex _patternMatcher = new Regex("^(?[^i]*)i{1}(?[^i]*)$" +#if !NET_STANDARD + , RegexOptions.Compiled +#endif + ); + + public static ElementKeyPartFactory For(string pattern, MappingConfigInfo configInfo) + { + if (string.IsNullOrWhiteSpace(pattern)) + { + throw InvalidPattern(); + } + + var patternMatch = _patternMatcher.Match(pattern); + + if (!patternMatch.Success) + { + throw InvalidPattern(); + } + + var prefix = patternMatch.Groups["Prefix"].Value; + var suffix = patternMatch.Groups["Suffix"].Value; + + if (!configInfo.IsForAllSourceTypes() && + (configInfo.SourceType != typeof(ExpandoObject)) && + configInfo.SourceType.IsEnumerable()) + { + configInfo = configInfo + .Clone() + .ForSourceType(configInfo.SourceType.GetEnumerableElementType()); + } + + if ((configInfo.TargetType != typeof(object)) && + (configInfo.TargetType != typeof(ExpandoObject)) && + configInfo.TargetType.IsEnumerable()) + { + configInfo = configInfo + .Clone() + .ForTargetType(configInfo.TargetType.GetEnumerableElementType()); + } + + return new ElementKeyPartFactory(prefix, suffix, configInfo); + } + + private static MappingConfigurationException InvalidPattern() + { + return new MappingConfigurationException( + "An enumerable element key pattern must contain a single 'i' character " + + "as a placeholder for the enumerable index"); + } + + #endregion + + private string Pattern => _prefixString + "i" + _suffixString; + + public Expression GetElementKeyPartMatcher() + => _keyPartMatcher ?? (_keyPartMatcher = CreateKeyPartRegex().ToConstantExpression()); + + private Regex CreateKeyPartRegex() + { + return new Regex( + _prefixString + "[0-9]+" + _suffixString +#if !NET_STANDARD + , RegexOptions.Compiled +#endif + ); + } + + public Expression GetElementKeyPrefixOrNull() => _prefix; + + public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) + { + var otherFactory = ((ElementKeyPartFactory)otherConfiguredItem); + + if ((_prefixString != otherFactory._prefixString) || (_suffixString != otherFactory._suffixString)) + { + return false; + } + + return base.ConflictsWith(otherConfiguredItem); + } + + public override string GetConflictMessage() + => $"Element keys are already configured {TargetScopeDescription} to be {Pattern}"; + + public IEnumerable GetElementKeyParts(Expression index) + { + if (_prefix != null) + { + yield return _prefix; + } + + yield return ConfigInfo.MapperContext.ValueConverters.GetConversion(index, typeof(string)); + + if (_suffix != null) + { + yield return _suffix; + } + } + + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion + public override string ToString() + { + var sourceType = ConfigInfo.IsForAllSourceTypes() + ? "All sources" + : ConfigInfo.SourceType.GetFriendlyName(); + + var targetTypeName = ConfigInfo.TargetType == typeof(object) + ? "All targets" + : TargetTypeName; + + return $"{sourceType} -> {targetTypeName}: {Pattern}"; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/Dictionaries/JoiningNameFactory.cs b/AgileMapper/Configuration/Dictionaries/JoiningNameFactory.cs new file mode 100644 index 000000000..122002f45 --- /dev/null +++ b/AgileMapper/Configuration/Dictionaries/JoiningNameFactory.cs @@ -0,0 +1,247 @@ +namespace AgileObjects.AgileMapper.Configuration.Dictionaries +{ + using System; + using System.Dynamic; + using System.Linq.Expressions; + using Extensions.Internal; + using Members; + using Members.Dictionaries; + using ReadableExpressions.Extensions; + using static DictionaryContext; + + internal class JoiningNameFactory : DictionaryKeyPartFactoryBase + { + private readonly string _separator; + private readonly bool _isDefault; + private readonly Func _joinedNameFactory; + private Expression _separatorConstant; + + private JoiningNameFactory(string separator, MappingConfigInfo configInfo, bool isDefault) + : base(configInfo) + { + _separator = separator; + _isDefault = isDefault; + + if (IsFlattened) + { + _joinedNameFactory = Flatten; + } + else + { + _joinedNameFactory = HandleLeadingSeparator; + } + } + + #region Factory Methods + + public static JoiningNameFactory UnderscoredForSourceDynamics(MapperContext mapperContext) + { + var sourceExpandoObject = new MappingConfigInfo(mapperContext) + .ForAllRuleSets() + .ForSourceType() + .ForAllTargetTypes() + .Set(DictionaryType.Expando); + + return ForDefault("_", sourceExpandoObject); + } + + public static JoiningNameFactory UnderscoredForTargetDynamics(MapperContext mapperContext) + { + var targetExpandoObject = new MappingConfigInfo(mapperContext) + .ForAllRuleSets() + .ForAllSourceTypes() + .ForTargetType() + .Set(DictionaryType.Expando); + + return ForDefault("_", targetExpandoObject); + } + + public static JoiningNameFactory Dotted(MapperContext mapperContext) + => ForDefault(".", MappingConfigInfo.AllRuleSetsSourceTypesAndTargetTypes(mapperContext)); + + public static JoiningNameFactory Flattened(MappingConfigInfo configInfo) + => For(string.Empty, configInfo); + + public static JoiningNameFactory For(string separator, MappingConfigInfo configInfo) + => new JoiningNameFactory(separator, configInfo, isDefault: false); + + public static JoiningNameFactory ForDefault(string separator, MappingConfigInfo configInfo) + => new JoiningNameFactory(separator, configInfo, isDefault: true); + + #endregion + + public Expression Separator + => _separatorConstant ?? (_separatorConstant = _separator.ToConstantExpression()); + + private string SeparatorDescription + => IsFlattened ? "flattened" : "separated with '" + _separator + "'"; + + private bool IsFlattened => _separator == string.Empty; + + public override bool AppliesTo(IBasicMapperData mapperData) + { + if (!base.AppliesTo(mapperData)) + { + return false; + } + + var applicableDictionarycontext = ConfigInfo.Get(); + + if (applicableDictionarycontext == All) + { + return true; + } + + while (mapperData != null) + { + if (mapperData.TargetMember.IsDictionary) + { + return false; + } + + mapperData = mapperData.Parent; + } + + return true; + } + + public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) + { + var otherFactory = ((JoiningNameFactory)otherConfiguredItem); + + if (IsForAllTargetTypes != otherFactory.IsForAllTargetTypes) + { + return false; + } + + var separatorsAreTheSame = _separator == otherFactory._separator; + + if (_isDefault && !separatorsAreTheSame) + { + return false; + } + + if (ConfigInfo.Get() != otherFactory.ConfigInfo.Get()) + { + return false; + } + + var thisContext = ConfigInfo.Get(); + + if (thisContext == All) + { + if (separatorsAreTheSame) + { + return true; + } + + var otherContext = otherFactory.ConfigInfo.Get(); + + return otherContext == All; + } + + return base.ConflictsWith(otherConfiguredItem); + } + + public override string GetConflictMessage() + => $"Member names are already configured {TargetScopeDescription} to be {SeparatorDescription}"; + + public Expression GetJoiningName(Member member, IMemberMapperData mapperData) + => _joinedNameFactory.Invoke(member, mapperData); + + private Expression HandleLeadingSeparator(Member member, IMemberMapperData mapperData) + { + var memberName = GetJoiningNamePart(member, mapperData); + + if (_separator != ".") + { + memberName = memberName.Replace(".", _separator); + } + + if (memberName.StartsWith(_separator, StringComparison.Ordinal)) + { + if (IsRootMember(member, mapperData)) + { + memberName = memberName.Substring(_separator.Length); + } + } + else if (!IsRootMember(member, mapperData)) + { + memberName = _separator + memberName; + } + + return memberName.ToConstantExpression(); + } + + private static string GetJoiningNamePart(Member member, IMemberMapperData mapperData) + { + var dictionarySettings = mapperData.MapperContext.UserConfigurations.Dictionaries; + var memberName = dictionarySettings.GetMemberKeyOrNull(member, mapperData) ?? member.JoiningName; + + return memberName; + } + + private static bool IsRootMember(Member member, IMemberMapperData mapperData) + { + var memberIndex = Array.IndexOf(mapperData.TargetMember.MemberChain, member, 0); + + if (memberIndex == 1) + { + return true; + } + + if (mapperData.TargetMember is DictionaryTargetMember) + { + return mapperData.TargetMember.MemberChain[memberIndex - 1].IsDictionary; + } + + var rootDictionaryContextIndex = mapperData.TargetMember.MemberChain.Length; + var sourceMember = mapperData.SourceMember; + + while (mapperData.SourceMember.Type == sourceMember.Type) + { + --rootDictionaryContextIndex; + mapperData = mapperData.Parent; + + if (mapperData == null) + { + return false; + } + } + + memberIndex = memberIndex - rootDictionaryContextIndex; + + return memberIndex == 1; + } + + private static Expression Flatten(Member member, IMemberMapperData mapperData) + { + var memberName = GetJoiningNamePart(member, mapperData); + + if (memberName.StartsWith('.')) + { + memberName = memberName.Substring(1); + } + + return memberName.ToConstantExpression(); + } + + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion + public override string ToString() + { + var sourceType = ConfigInfo.IsForAllSourceTypes() + ? "All sources" + : ConfigInfo.SourceType.GetFriendlyName(); + + var targetTypeName = ConfigInfo.TargetType == typeof(object) + ? "All targets" + : TargetTypeName; + + return $"{sourceType} -> {targetTypeName}: {SeparatorDescription}"; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/ElementKeyPartFactory.cs b/AgileMapper/Configuration/ElementKeyPartFactory.cs deleted file mode 100644 index c2bfcfae0..000000000 --- a/AgileMapper/Configuration/ElementKeyPartFactory.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace AgileObjects.AgileMapper.Configuration -{ - using System.Collections.Generic; - using System.Linq.Expressions; - using System.Text.RegularExpressions; - using Extensions; - - internal class ElementKeyPartFactory : UserConfiguredItemBase - { - private readonly Expression _prefix; - private readonly Expression _suffix; - - private ElementKeyPartFactory( - string prefix, - string suffix, - MappingConfigInfo configInfo) - : base(configInfo) - { - if (!string.IsNullOrEmpty(prefix)) - { - _prefix = prefix.ToConstantExpression(); - } - - if (!string.IsNullOrEmpty(suffix)) - { - _suffix = suffix.ToConstantExpression(); - } - } - - #region Factory Methods - - public static ElementKeyPartFactory SquareBracketedIndex(MapperContext mapperContext) - => new ElementKeyPartFactory("[", "]", MappingConfigInfo.AllRuleSetsSourceTypesAndTargetTypes(mapperContext)); - - private static readonly Regex _patternMatcher = new Regex("^(?[^i]*)i{1}(?[^i]*)$" -#if !NET_STANDARD - , RegexOptions.Compiled -#endif - ); - - public static ElementKeyPartFactory For(string pattern, MappingConfigInfo configInfo) - { - if (string.IsNullOrWhiteSpace(pattern)) - { - throw NewInvalidPatternException(); - } - - var patternMatch = _patternMatcher.Match(pattern); - - if (!patternMatch.Success) - { - throw NewInvalidPatternException(); - } - - var prefix = patternMatch.Groups["Prefix"].Value; - var suffix = patternMatch.Groups["Suffix"].Value; - - if (!configInfo.IsForAllSourceTypes && !configInfo.SourceType.IsEnumerable()) - { - configInfo = configInfo - .Clone() - .ForSourceType(typeof(IEnumerable<>).MakeGenericType(configInfo.SourceType)); - } - - if ((configInfo.TargetType != typeof(object)) && !configInfo.TargetType.IsEnumerable()) - { - configInfo = configInfo - .Clone() - .ForTargetType(typeof(IEnumerable<>).MakeGenericType(configInfo.TargetType)); - } - - return new ElementKeyPartFactory(prefix, suffix, configInfo); - } - - private static MappingConfigurationException NewInvalidPatternException() - { - return new MappingConfigurationException( - "An enumerable element key pattern must contain a single 'i' character " + - "as a placeholder for the enumerable index"); - } - - #endregion - - public Expression GetElementKeyPrefixOrNull() => _prefix; - - public IEnumerable GetElementKeyParts(Expression index) - { - if (_prefix != null) - { - yield return _prefix; - } - - yield return ConfigInfo.MapperContext.ValueConverters.GetConversion(index, typeof(string)); - - if (_suffix != null) - { - yield return _suffix; - } - } - } -} \ No newline at end of file diff --git a/AgileMapper/Configuration/EnumComparisonFixer.cs b/AgileMapper/Configuration/EnumComparisonFixer.cs index c905f46e2..988102f22 100644 --- a/AgileMapper/Configuration/EnumComparisonFixer.cs +++ b/AgileMapper/Configuration/EnumComparisonFixer.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class EnumComparisonFixer : ExpressionVisitor diff --git a/AgileMapper/Configuration/EnumMemberPair.cs b/AgileMapper/Configuration/EnumMemberPair.cs index 2b17f0a1d..1e7e58403 100644 --- a/AgileMapper/Configuration/EnumMemberPair.cs +++ b/AgileMapper/Configuration/EnumMemberPair.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using TypeConversion; internal class EnumMemberPair diff --git a/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs b/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs index fc8e7fce5..c2a578a0f 100644 --- a/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs +++ b/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs @@ -36,6 +36,11 @@ public IEnumerator GetEnumerator() return _inlineContextsCache.Values.GetEnumerator(); } + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #endregion diff --git a/AgileMapper/Configuration/Inline/InlineMapperKey.cs b/AgileMapper/Configuration/Inline/InlineMapperKey.cs index 258d43400..0a43b4de6 100644 --- a/AgileMapper/Configuration/Inline/InlineMapperKey.cs +++ b/AgileMapper/Configuration/Inline/InlineMapperKey.cs @@ -4,7 +4,7 @@ namespace AgileObjects.AgileMapper.Configuration.Inline using System.Collections.Generic; using System.Linq.Expressions; using Api.Configuration; - using Extensions; + using Extensions.Internal; using Members; internal class InlineMapperKey : IInlineMapperKey diff --git a/AgileMapper/Configuration/JoiningNameFactory.cs b/AgileMapper/Configuration/JoiningNameFactory.cs deleted file mode 100644 index b3165185a..000000000 --- a/AgileMapper/Configuration/JoiningNameFactory.cs +++ /dev/null @@ -1,148 +0,0 @@ -namespace AgileObjects.AgileMapper.Configuration -{ - using System; - using System.Linq.Expressions; - using Extensions; - using Members; - using ReadableExpressions.Extensions; - - internal class JoiningNameFactory : UserConfiguredItemBase - { - private readonly string _separator; - private readonly Func _joinedNameFactory; - private readonly Type _targetType; - private readonly bool _isDefault; - private readonly bool _isGlobal; - - private JoiningNameFactory( - string separator, - Func joinedNameFactory, - MappingConfigInfo configInfo) - : base(configInfo) - { - _separator = separator; - _joinedNameFactory = joinedNameFactory; - _targetType = configInfo.TargetType; - _isDefault = HasDefault(separator); - _isGlobal = _targetType == typeof(object); - } - - #region Factory Methods - - public static JoiningNameFactory Dotted(MapperContext mapperContext) - => For(".", MappingConfigInfo.AllRuleSetsSourceTypesAndTargetTypes(mapperContext)); - - public static JoiningNameFactory For(string separator, MappingConfigInfo configInfo) - => new JoiningNameFactory(separator, HandleLeadingSeparator, configInfo); - - public static JoiningNameFactory Flattened(MappingConfigInfo configInfo) - => new JoiningNameFactory(string.Empty, Flatten, configInfo); - - #endregion - - public string TargetScopeDescription - => _isGlobal ? "globally" : "for target type " + _targetType.GetFriendlyName(); - - public string SeparatorDescription - => IsFlattened ? "flattened" : "separated with '" + _separator + "'"; - - private bool IsFlattened => !_isDefault && (_separator == string.Empty); - - public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) - { - if (_isDefault) - { - return false; - } - - if (_isGlobal != ((JoiningNameFactory)otherConfiguredItem)._isGlobal) - { - return false; - } - - return base.ConflictsWith(otherConfiguredItem); - } - - public Expression GetJoiningName(Member member, IMemberMapperData mapperData) - => _joinedNameFactory.Invoke(_separator, member, mapperData); - - private static Expression HandleLeadingSeparator(string separator, Member member, IMemberMapperData mapperData) - { - var memberName = GetJoiningNamePart(member, mapperData); - - if (!HasDefault(separator)) - { - memberName = memberName.Replace(".", separator); - } - - if (memberName.StartsWith(separator, StringComparison.Ordinal)) - { - if (IsRootMember(member, mapperData)) - { - memberName = memberName.Substring(separator.Length); - } - } - else if (!IsRootMember(member, mapperData)) - { - memberName = separator + memberName; - } - - return memberName.ToConstantExpression(); - } - - private static string GetJoiningNamePart(Member member, IMemberMapperData mapperData) - { - var dictionarySettings = mapperData.MapperContext.UserConfigurations.Dictionaries; - var memberName = dictionarySettings.GetMemberKeyOrNull(member, mapperData) ?? member.JoiningName; - - return memberName; - } - - private static bool IsRootMember(Member member, IMemberMapperData mapperData) - { - var memberIndex = Array.IndexOf(mapperData.TargetMember.MemberChain, member, 0); - - if (memberIndex == 1) - { - return true; - } - - if (mapperData.TargetMember is DictionaryTargetMember) - { - return mapperData.TargetMember.MemberChain[memberIndex - 1].IsDictionary; - } - - var rootDictionaryContextIndex = mapperData.TargetMember.MemberChain.Length; - var sourceMember = mapperData.SourceMember; - - while (mapperData.SourceMember.Type == sourceMember.Type) - { - --rootDictionaryContextIndex; - mapperData = mapperData.Parent; - - if (mapperData == null) - { - return false; - } - } - - memberIndex = memberIndex - rootDictionaryContextIndex; - - return memberIndex == 1; - } - - private static bool HasDefault(string separator) => separator == "."; - - private static Expression Flatten(string separator, Member member, IMemberMapperData mapperData) - { - var memberName = GetJoiningNamePart(member, mapperData); - - if (memberName.StartsWith('.')) - { - memberName = memberName.Substring(1); - } - - return memberName.ToConstantExpression(); - } - } -} \ No newline at end of file diff --git a/AgileMapper/Configuration/MappingConfigInfo.cs b/AgileMapper/Configuration/MappingConfigInfo.cs index cbaae73f7..7cdaf885f 100644 --- a/AgileMapper/Configuration/MappingConfigInfo.cs +++ b/AgileMapper/Configuration/MappingConfigInfo.cs @@ -1,21 +1,21 @@ namespace AgileObjects.AgileMapper.Configuration { using System; + using System.Collections.Generic; using System.Globalization; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; - using NetStandardPolyfills; using ObjectPopulation; using ReadableExpressions; - internal class MappingConfigInfo + internal class MappingConfigInfo : ITypePair { - private static readonly Type _allSourceTypes = typeof(MappingConfigInfo); private static readonly MappingRuleSet _allRuleSets = new MappingRuleSet("*", true, null, null, null); private ConfiguredLambdaInfo _conditionLambda; private bool _negateCondition; + private Dictionary _data; public MappingConfigInfo(MapperContext mapperContext) { @@ -36,7 +36,7 @@ public static MappingConfigInfo AllRuleSetsSourceTypesAndTargetTypes(MapperConte public Type SourceType { get; private set; } - public MappingConfigInfo ForAllSourceTypes() => ForSourceType(_allSourceTypes); + public MappingConfigInfo ForAllSourceTypes() => ForSourceType(Constants.AllTypes); public MappingConfigInfo ForSourceType() => ForSourceType(typeof(TSource)); @@ -48,13 +48,6 @@ public MappingConfigInfo ForSourceType(Type sourceType) public bool HasSameSourceTypeAs(MappingConfigInfo otherConfigInfo) => otherConfigInfo.SourceType == SourceType; - public bool IsForSourceType(MappingConfigInfo otherConfigInfo) => IsForSourceType(otherConfigInfo.SourceType); - - private bool IsForSourceType(Type sourceType) - => IsForAllSourceTypes || sourceType.IsAssignableTo(SourceType); - - public bool IsForAllSourceTypes => SourceType == _allSourceTypes; - public Type TargetType { get; private set; } public MappingConfigInfo ForAllTargetTypes() => ForTargetType(); @@ -67,34 +60,10 @@ public MappingConfigInfo ForTargetType(Type targetType) return this; } - public bool IsForTargetType(MappingConfigInfo otherConfigInfo) - => otherConfigInfo.TargetType.IsAssignableTo(TargetType); - public bool HasSameTargetTypeAs(MappingConfigInfo otherConfigInfo) => TargetType == otherConfigInfo.TargetType; public bool HasCompatibleTypes(MappingConfigInfo otherConfigInfo) - => HasCompatibleTypes(otherConfigInfo.SourceType, otherConfigInfo.TargetType); - - public bool HasCompatibleTypes(IBasicMapperData mapperData) - => HasCompatibleTypes(mapperData.SourceType, mapperData.TargetType); - - public bool HasCompatibleTypes(Type sourceType, Type targetType) - { - if (IsForSourceType(sourceType)) - { - if (targetType.IsAssignableTo(TargetType)) - { - return true; - } - - if (targetType.IsInterface()) - { - return Array.IndexOf(TargetType.GetAllInterfaces(), targetType) != -1; - } - } - - return false; - } + => ((ITypePair)this).HasCompatibleTypes(otherConfigInfo); public MappingRuleSet RuleSet { get; private set; } @@ -205,6 +174,16 @@ public Expression GetConditionOrNull( #endregion + public T Get() => Data.TryGetValue(typeof(T), out var value) ? (T)value : default(T); + + public MappingConfigInfo Set(T value) + { + Data[typeof(T)] = value; + return this; + } + + private Dictionary Data => (_data ?? (_data = new Dictionary())); + public IBasicMapperData ToMapperData() { var dummyTargetMember = QualifiedMember @@ -219,13 +198,25 @@ public IBasicMapperData ToMapperData() public MappingConfigInfo Clone() { - return new MappingConfigInfo(MapperContext) + var cloned = new MappingConfigInfo(MapperContext) { SourceType = SourceType, TargetType = TargetType, SourceValueType = SourceValueType, RuleSet = RuleSet }; + + if (_data == null) + { + return cloned; + } + + foreach (var itemByType in _data) + { + cloned.Data.Add(itemByType.Key, itemByType.Value); + } + + return cloned; } private class TypeTestFinder : ExpressionVisitor diff --git a/AgileMapper/Configuration/MappingConfigurationException.cs b/AgileMapper/Configuration/MappingConfigurationException.cs index 7552feaf8..b0ec29dbc 100644 --- a/AgileMapper/Configuration/MappingConfigurationException.cs +++ b/AgileMapper/Configuration/MappingConfigurationException.cs @@ -10,6 +10,11 @@ public class MappingConfigurationException : Exception /// /// Initializes a new instance of the MappingConfigurationException class. /// + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion public MappingConfigurationException() { } diff --git a/AgileMapper/Configuration/ParametersSwapper.cs b/AgileMapper/Configuration/ParametersSwapper.cs index ec23ed863..f2104f331 100644 --- a/AgileMapper/Configuration/ParametersSwapper.cs +++ b/AgileMapper/Configuration/ParametersSwapper.cs @@ -4,10 +4,11 @@ namespace AgileObjects.AgileMapper.Configuration using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; using ObjectPopulation; + using static Members.Member; internal class ParametersSwapper { @@ -89,8 +90,8 @@ private static Expression SwapForContextParameter(SwapArgs swapArgs) } var memberContextType = IsCallbackContext(contextTypes) ? contextType : contextType.GetAllInterfaces().First(); - var sourceProperty = memberContextType.GetPublicInstanceProperty("Source"); - var targetProperty = memberContextType.GetPublicInstanceProperty("Target"); + var sourceProperty = memberContextType.GetPublicInstanceProperty(RootSourceMemberName); + var targetProperty = memberContextType.GetPublicInstanceProperty(RootTargetMemberName); var indexProperty = memberContextType.GetPublicInstanceProperty("EnumerableIndex"); var parentProperty = memberContextType.GetPublicInstanceProperty("Parent"); diff --git a/AgileMapper/Configuration/PotentialCloneExtensions.cs b/AgileMapper/Configuration/PotentialCloneExtensions.cs index c5ba95e68..4d92597ef 100644 --- a/AgileMapper/Configuration/PotentialCloneExtensions.cs +++ b/AgileMapper/Configuration/PotentialCloneExtensions.cs @@ -2,7 +2,7 @@ { using System.Collections.Generic; using System.Linq; - using Extensions; + using Extensions.Internal; internal static class PotentialCloneExtensions { diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index 7f4190e70..90e1f87c8 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -5,7 +5,8 @@ using System.Linq; using System.Linq.Expressions; using DataSources; - using Extensions; + using Dictionaries; + using Extensions.Internal; using Members; using ObjectPopulation; diff --git a/AgileMapper/Configuration/UserConfiguredItemBase.cs b/AgileMapper/Configuration/UserConfiguredItemBase.cs index 550fec4ef..b34e77214 100644 --- a/AgileMapper/Configuration/UserConfiguredItemBase.cs +++ b/AgileMapper/Configuration/UserConfiguredItemBase.cs @@ -136,7 +136,7 @@ protected bool MemberPathHasMatchingSourceAndTargetTypes(IBasicMapperData mapper { while (mapperData != null) { - if (ConfigInfo.HasCompatibleTypes(mapperData)) + if (mapperData.HasCompatibleTypes(ConfigInfo)) { return true; } diff --git a/AgileMapper/Constants.cs b/AgileMapper/Constants.cs index d3e4f3b29..aa623bcd6 100644 --- a/AgileMapper/Constants.cs +++ b/AgileMapper/Constants.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal static class Constants @@ -15,6 +15,7 @@ internal static class Constants public static readonly string EnumerableElementName = "[i]"; public static readonly Type[] NoTypeArguments = Enumerable.EmptyArray; + public static readonly Type AllTypes = typeof(Constants); public static readonly Expression EmptyExpression = Expression.Empty(); diff --git a/AgileMapper/DataSources/ConfiguredDataSource.cs b/AgileMapper/DataSources/ConfiguredDataSource.cs index 4ae4fd643..1a625c53f 100644 --- a/AgileMapper/DataSources/ConfiguredDataSource.cs +++ b/AgileMapper/DataSources/ConfiguredDataSource.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.DataSources { using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal class ConfiguredDataSource : DataSourceBase, IConfiguredDataSource diff --git a/AgileMapper/DataSources/ConfiguredDataSourceFactory.cs b/AgileMapper/DataSources/ConfiguredDataSourceFactory.cs index 2c242be47..04052fa40 100644 --- a/AgileMapper/DataSources/ConfiguredDataSourceFactory.cs +++ b/AgileMapper/DataSources/ConfiguredDataSourceFactory.cs @@ -4,9 +4,7 @@ using Configuration; using Members; - internal class ConfiguredDataSourceFactory : - UserConfiguredItemBase, - IPotentialClone + internal class ConfiguredDataSourceFactory : UserConfiguredItemBase, IPotentialClone { private readonly ConfiguredLambdaInfo _dataSourceLambda; diff --git a/AgileMapper/DataSources/ConfiguredDictionaryDataSourceFactory.cs b/AgileMapper/DataSources/ConfiguredDictionaryDataSourceFactory.cs index 1a09dc0d5..043de073c 100644 --- a/AgileMapper/DataSources/ConfiguredDictionaryDataSourceFactory.cs +++ b/AgileMapper/DataSources/ConfiguredDictionaryDataSourceFactory.cs @@ -2,6 +2,7 @@ { using Configuration; using Members; + using Members.Dictionaries; internal class ConfiguredDictionaryDataSourceFactory : ConfiguredDataSourceFactory { diff --git a/AgileMapper/DataSources/DataSourceBase.cs b/AgileMapper/DataSources/DataSourceBase.cs index aebb8a1a9..daa258e25 100644 --- a/AgileMapper/DataSources/DataSourceBase.cs +++ b/AgileMapper/DataSources/DataSourceBase.cs @@ -2,7 +2,7 @@ { using System.Collections.Generic; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using ReadableExpressions.Extensions; diff --git a/AgileMapper/DataSources/DataSourceFinder.cs b/AgileMapper/DataSources/DataSourceFinder.cs index 1c2774a91..69e215f58 100644 --- a/AgileMapper/DataSources/DataSourceFinder.cs +++ b/AgileMapper/DataSources/DataSourceFinder.cs @@ -2,7 +2,7 @@ { using System.Collections.Generic; using System.Linq; - using Extensions; + using Extensions.Internal; using Members; internal class DataSourceFinder @@ -114,8 +114,8 @@ private static IEnumerable GetSourceMemberDataSources( yield break; } - var bestMatchingSourceMember = SourceMemberMatcher.GetMatchFor(mappingData); - var matchingSourceMemberDataSource = GetSourceMemberDataSourceOrNull(bestMatchingSourceMember, mappingData); + var bestMatchingSourceMember = SourceMemberMatcher.GetMatchFor(mappingData, out var contextMappingData); + var matchingSourceMemberDataSource = GetSourceMemberDataSourceOrNull(bestMatchingSourceMember, contextMappingData); if ((matchingSourceMemberDataSource == null) || configuredDataSources.Any(cds => cds.IsSameAs(matchingSourceMemberDataSource))) diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index 65827053d..aede04d73 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.DataSources using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal class DataSourceSet : IEnumerable @@ -90,9 +90,10 @@ public Expression GetPopulationExpression(IMemberMapperData mapperData) private Expression GetFallbackValueOrNull(IMemberMapperData mapperData) { - var fallbackValue = _dataSources.Last().Value; + var finalDataSource = _dataSources.Last(); + var fallbackValue = finalDataSource.Value; - if (_dataSources.HasOne()) + if (finalDataSource.IsConditional || _dataSources.HasOne()) { return fallbackValue; } diff --git a/AgileMapper/DataSources/DictionaryDataSourceFactory.cs b/AgileMapper/DataSources/DictionaryDataSourceFactory.cs index c4acacced..633af3b5a 100644 --- a/AgileMapper/DataSources/DictionaryDataSourceFactory.cs +++ b/AgileMapper/DataSources/DictionaryDataSourceFactory.cs @@ -2,8 +2,9 @@ { using System; using System.Collections.Generic; - using Extensions; + using Extensions.Internal; using Members; + using Members.Dictionaries; internal class DictionaryDataSourceFactory : IMaptimeDataSourceFactory { @@ -81,7 +82,7 @@ private static DictionarySourceMember GetSourceMember(IMemberMapperData mapperDa while (!parentMapperData.IsRoot) { - if (parentMapperData.TargetMember.LeafMember == mapperData.TargetMember.LeafMember) + if (parentMapperData.TargetMember.LeafMember.Equals(mapperData.TargetMember.LeafMember)) { break; } diff --git a/AgileMapper/DataSources/DictionaryEntryDataSource.cs b/AgileMapper/DataSources/DictionaryEntryDataSource.cs index d90d1f83d..fd796ed1a 100644 --- a/AgileMapper/DataSources/DictionaryEntryDataSource.cs +++ b/AgileMapper/DataSources/DictionaryEntryDataSource.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.DataSources { using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; internal class DictionaryEntryDataSource : DataSourceBase { diff --git a/AgileMapper/DataSources/DictionaryEntryVariablePair.cs b/AgileMapper/DataSources/DictionaryEntryVariablePair.cs index 6326d345d..1f405768f 100644 --- a/AgileMapper/DataSources/DictionaryEntryVariablePair.cs +++ b/AgileMapper/DataSources/DictionaryEntryVariablePair.cs @@ -5,8 +5,9 @@ namespace AgileObjects.AgileMapper.DataSources using System.Linq; using System.Linq.Expressions; using System.Reflection; - using Extensions; + using Extensions.Internal; using Members; + using Members.Dictionaries; using NetStandardPolyfills; internal class DictionaryEntryVariablePair @@ -41,14 +42,7 @@ public DictionaryEntryVariablePair(DictionarySourceMember sourceMember, IMemberM } private static string GetTargetMemberName(IBasicMapperData mapperData) - { - if (mapperData.TargetMemberIsEnumerableElement()) - { - return "element"; - } - - return mapperData.TargetMember.Name.ToCamelCase(); - } + => mapperData.TargetMember.Name.ToCamelCase(); public DictionarySourceMember SourceMember { get; } @@ -106,8 +100,19 @@ public Expression GetMatchingKeyAssignment(Expression targetMemberKey) var firstMatchingKeyOrNull = GetKeyMatchingQuery( HasConstantTargetMemberKey ? targetMemberKey : Key, + (keyParameter, targetKey) => + { + var separator = MapperData.Parent.IsRoot + ? null + : MapperData.GetDictionaryKeyPartSeparator(); + + var elementKeyPartMatcher = MapperData.Parent.TargetMemberIsEnumerableElement() + ? MapperData.GetDictionaryElementKeyPartMatcher() + : null; + + return keyParameter.GetMatchesKeyCall(targetKey, separator, elementKeyPartMatcher); + }, Expression.Equal, - (keyParameter, targetKey) => keyParameter.GetCaseInsensitiveEquals(targetKey), _linqFirstOrDefaultMethod); var keyVariableAssignment = GetKeyAssignment(firstMatchingKeyOrNull); @@ -125,8 +130,8 @@ public Expression GetNoKeysWithMatchingStartQuery(Expression targetMemberKey) var noKeysStartWithTarget = GetKeyMatchingQuery( targetMemberKey, - (keyParameter, targetKey) => GetKeyStartsWithCall(keyParameter, targetKey, StringComparison.Ordinal), GetKeyStartsWithIgnoreCaseCall, + (keyParameter, targetKey) => GetKeyStartsWithCall(keyParameter, targetKey, StringComparison.Ordinal), EnumerableExtensions.EnumerableNoneMethod); return noKeysStartWithTarget; @@ -153,15 +158,15 @@ private static Expression GetKeyStartsWithCall( private Expression GetKeyMatchingQuery( Expression targetMemberKey, - Func rootKeyMatcherFactory, - Func nestedKeyMatcherFactory, + Func keyMatcherFactory, + Func elementKeyMatcherFactory, MethodInfo queryMethod) { var keyParameter = Expression.Parameter(typeof(string), "key"); - var keyMatcher = MapperData.IsRoot - ? rootKeyMatcherFactory.Invoke(keyParameter, targetMemberKey) - : nestedKeyMatcherFactory.Invoke(keyParameter, targetMemberKey); + var keyMatcher = MapperData.TargetMemberIsEnumerableElement() + ? elementKeyMatcherFactory.Invoke(keyParameter, targetMemberKey) + : keyMatcherFactory.Invoke(keyParameter, targetMemberKey); var keyMatchesLambda = Expression.Lambda>(keyMatcher, keyParameter); diff --git a/AgileMapper/DataSources/IDataSource.cs b/AgileMapper/DataSources/IDataSource.cs index 436525359..49078e2ae 100644 --- a/AgileMapper/DataSources/IDataSource.cs +++ b/AgileMapper/DataSources/IDataSource.cs @@ -2,7 +2,7 @@ { using System.Collections.Generic; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal interface IDataSource : IConditionallyChainable diff --git a/AgileMapper/DataSources/SourceMemberDataSource.cs b/AgileMapper/DataSources/SourceMemberDataSource.cs index c1bef5433..67edd2784 100644 --- a/AgileMapper/DataSources/SourceMemberDataSource.cs +++ b/AgileMapper/DataSources/SourceMemberDataSource.cs @@ -3,9 +3,8 @@ using System.Linq.Expressions; using System.Collections.Generic; #if NET_STANDARD - using System.Reflection; #endif - using Extensions; + using Extensions.Internal; using Members; using ReadableExpressions.Extensions; diff --git a/AgileMapper/DerivedTypesCache.cs b/AgileMapper/DerivedTypesCache.cs index 14877a1df..1f5ba85e4 100644 --- a/AgileMapper/DerivedTypesCache.cs +++ b/AgileMapper/DerivedTypesCache.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Reflection; using Caching; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class DerivedTypesCache diff --git a/AgileMapper/Extensions/CollectionData.cs b/AgileMapper/Extensions/Internal/CollectionData.cs similarity index 98% rename from AgileMapper/Extensions/CollectionData.cs rename to AgileMapper/Extensions/Internal/CollectionData.cs index 1a8c54f40..6f9a9572c 100644 --- a/AgileMapper/Extensions/CollectionData.cs +++ b/AgileMapper/Extensions/Internal/CollectionData.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System; using System.Collections.Generic; diff --git a/AgileMapper/Extensions/EnumerableExtensions.cs b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs similarity index 99% rename from AgileMapper/Extensions/EnumerableExtensions.cs rename to AgileMapper/Extensions/Internal/EnumerableExtensions.cs index 93fdda177..5923fba15 100644 --- a/AgileMapper/Extensions/EnumerableExtensions.cs +++ b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System; using System.Collections.Generic; diff --git a/AgileMapper/Extensions/ExpressionEquator.cs b/AgileMapper/Extensions/Internal/ExpressionEquator.cs similarity index 99% rename from AgileMapper/Extensions/ExpressionEquator.cs rename to AgileMapper/Extensions/Internal/ExpressionEquator.cs index 871b7afb9..16d9308b9 100644 --- a/AgileMapper/Extensions/ExpressionEquator.cs +++ b/AgileMapper/Extensions/Internal/ExpressionEquator.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System; using System.Collections.Generic; diff --git a/AgileMapper/Extensions/ExpressionExtensions.Replace.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs similarity index 99% rename from AgileMapper/Extensions/ExpressionExtensions.Replace.cs rename to AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs index a741f058b..5d6f5d1cc 100644 --- a/AgileMapper/Extensions/ExpressionExtensions.Replace.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System; using System.Collections.Generic; diff --git a/AgileMapper/Extensions/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs similarity index 87% rename from AgileMapper/Extensions/ExpressionExtensions.cs rename to AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 970fbe900..243d57ed5 100644 --- a/AgileMapper/Extensions/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System; using System.Collections.Generic; @@ -113,20 +113,7 @@ public static Expression GetIsNotDefaultComparison(this Expression expression) var typeDefault = expression.Type.ToDefaultExpression(); - if (!expression.Type.IsValueType() || !expression.Type.IsComplex()) - { - return Expression.NotEqual(expression, typeDefault); - } - - var objectEquals = typeof(object).GetPublicStaticMethod("Equals"); - - var objectEqualsCall = Expression.Call( - null, - objectEquals, - expression.GetConversionTo(typeof(object)), - typeDefault.GetConversionTo(typeof(object))); - - return Expression.IsFalse(objectEqualsCall); + return Expression.NotEqual(expression, typeDefault); } public static Expression GetIndexAccess(this Expression indexedExpression, Expression indexValue) @@ -136,8 +123,11 @@ public static Expression GetIndexAccess(this Expression indexedExpression, Expre return Expression.ArrayIndex(indexedExpression, indexValue); } - var indexer = indexedExpression.Type - .GetPublicInstanceProperties() + var relevantTypes = new[] { indexedExpression.Type } + .Concat(indexedExpression.Type.GetAllInterfaces()); + + var indexer = relevantTypes + .SelectMany(t => t.GetPublicInstanceProperties()) .First(p => p.GetIndexParameters().HasOne() && (p.GetIndexParameters()[0].ParameterType == indexValue.Type)); @@ -212,11 +202,7 @@ private static bool TryGetWrapperMethod( } private static MethodInfo GetNonListToArrayConversionMethod(EnumerableTypeHelper typeHelper) - { - return typeHelper.HasCollectionInterface - ? _collectionToArrayMethod - : _linqToArrayMethod; - } + => typeHelper.HasCollectionInterface ? _collectionToArrayMethod : _linqToArrayMethod; public static Expression WithToReadOnlyCollectionCall(this Expression enumerable, Type elementType) { @@ -224,7 +210,8 @@ public static Expression WithToReadOnlyCollectionCall(this Expression enumerable if (TryGetWrapperMethod(typeHelper, "ToReadOnlyCollection", out var method)) { - return GetToEnumerableCall(enumerable, method, elementType); + + return GetToEnumerableCall(enumerable, method, typeHelper.ElementType); } if (typeHelper.IsList) @@ -238,11 +225,26 @@ public static Expression WithToReadOnlyCollectionCall(this Expression enumerable } var nonListToArrayMethod = GetNonListToArrayConversionMethod(typeHelper); - var toArrayCall = GetToEnumerableCall(enumerable, nonListToArrayMethod, elementType); + var toArrayCall = GetToEnumerableCall(enumerable, nonListToArrayMethod, typeHelper.ElementType); return GetReadOnlyCollectionCreation(typeHelper, toArrayCall); } + public static Expression WithToCollectionCall(this Expression enumerable, Type elementType) + { + var typeHelper = new EnumerableTypeHelper(enumerable.Type, elementType); + + if (typeHelper.HasListInterface) + { + return GetCollectionCreation(typeHelper, enumerable.GetConversionTo(typeHelper.ListInterfaceType)); + } + + var nonListToArrayMethod = GetNonListToArrayConversionMethod(typeHelper); + var toArrayCall = GetToEnumerableCall(enumerable, nonListToArrayMethod, typeHelper.ElementType); + + return GetCollectionCreation(typeHelper, toArrayCall); + } + public static Expression WithToListCall(this Expression enumerable, Type elementType) => GetToEnumerableCall(enumerable, _linqToListMethod, elementType); @@ -297,6 +299,14 @@ private static Expression GetReadOnlyCollectionCreation(EnumerableTypeHelper typ list); } + private static Expression GetCollectionCreation(EnumerableTypeHelper typeHelper, Expression list) + { + // ReSharper disable once AssignNullToNotNullAttribute + return Expression.New( + typeHelper.CollectionType.GetPublicInstanceConstructor(typeHelper.ListInterfaceType), + list); + } + private static Type GetDictionaryType(Type dictionaryType) { return dictionaryType.IsInterface() diff --git a/AgileMapper/Extensions/ExpressionReplacementDictionary.cs b/AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs similarity index 85% rename from AgileMapper/Extensions/ExpressionReplacementDictionary.cs rename to AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs index 1fc2d97b0..4de3cac55 100644 --- a/AgileMapper/Extensions/ExpressionReplacementDictionary.cs +++ b/AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System.Collections.Generic; using System.Linq.Expressions; diff --git a/AgileMapper/Extensions/IConditionallyChainable.cs b/AgileMapper/Extensions/Internal/IConditionallyChainable.cs similarity index 78% rename from AgileMapper/Extensions/IConditionallyChainable.cs rename to AgileMapper/Extensions/Internal/IConditionallyChainable.cs index 5acead483..4560f3325 100644 --- a/AgileMapper/Extensions/IConditionallyChainable.cs +++ b/AgileMapper/Extensions/Internal/IConditionallyChainable.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System.Linq.Expressions; diff --git a/AgileMapper/Extensions/Internal/ObjectExtensions.cs b/AgileMapper/Extensions/Internal/ObjectExtensions.cs new file mode 100644 index 000000000..af8875ce2 --- /dev/null +++ b/AgileMapper/Extensions/Internal/ObjectExtensions.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.Extensions.Internal +{ + using System; + using System.Reflection; + using NetStandardPolyfills; + + /// + /// Provides extensions methods on the Object Type. This class is not intended + /// to be used from your code. + /// + public static class ObjectExtensions + { + internal static readonly MethodInfo GetRuntimeSourceTypeMethod = + typeof(ObjectExtensions).GetPublicStaticMethod("GetRuntimeSourceType"); + + /// + /// Gets the runtime type of the given object. + /// + /// The declared Type of the given object. + /// The source object for which to determine the runtime Type. + /// The runtime type of the given object. + public static Type GetRuntimeSourceType(this TDeclared source) + => source?.GetType() ?? typeof(TDeclared); + + internal static Type GetRuntimeTargetType(this TDeclared item, Type sourceType) + => (item != null) ? item.GetType() : typeof(TDeclared).GetRuntimeTargetType(sourceType); + + internal static Type GetRuntimeTargetType(this Type targetType, Type sourceType) + => sourceType.IsAssignableTo(targetType) ? sourceType : targetType; + } +} diff --git a/AgileMapper/Extensions/ReflectionExtensions.cs b/AgileMapper/Extensions/Internal/ReflectionExtensions.cs similarity index 78% rename from AgileMapper/Extensions/ReflectionExtensions.cs rename to AgileMapper/Extensions/Internal/ReflectionExtensions.cs index 8d24a45eb..723699ea3 100644 --- a/AgileMapper/Extensions/ReflectionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ReflectionExtensions.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System.Reflection; using NetStandardPolyfills; @@ -7,6 +7,12 @@ internal static class ReflectionExtensions { public static readonly bool ReflectionNotPermitted; + // This definitely get executed, but code coverage doesn't pick it up + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion static ReflectionExtensions() { try diff --git a/AgileMapper/Extensions/StringExpressionExtensions.cs b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs similarity index 61% rename from AgileMapper/Extensions/StringExpressionExtensions.cs rename to AgileMapper/Extensions/Internal/StringExpressionExtensions.cs index 6067d569a..a46f088da 100644 --- a/AgileMapper/Extensions/StringExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs @@ -1,9 +1,10 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; + using System.Text.RegularExpressions; using NetStandardPolyfills; internal static class StringExpressionExtensions @@ -85,12 +86,57 @@ private static void OptimiseForStringConcat(IList expressions) expressions.Insert(0, currentNamePart.ToConstantExpression()); } - public static Expression GetLeftCall(this Expression stringAccess, int numberOfCharacters) + public static Expression GetFirstOrDefaultCall(this Expression stringAccess) { return Expression.Call( - typeof(StringExtensions).GetPublicStaticMethod("Left"), + typeof(StringExtensions).GetPublicStaticMethod("FirstOrDefault"), + stringAccess); + } + + public static Expression GetMatchesKeyCall( + this Expression stringAccess, + Expression keyValue, + Expression separator, + Expression elementKeyPartMatcher) + { + if (separator == null) + { + return Expression.Call( + typeof(StringExtensions).GetPublicStaticMethod("MatchesKey", parameterCount: 2), + stringAccess, + keyValue); + } + + if (elementKeyPartMatcher == null) + { + return GetMatchesKeyWithSeparatorCall(stringAccess, keyValue, separator); + } + + var matcherPattern = ((Regex)((ConstantExpression)elementKeyPartMatcher).Value).ToString(); + + if (matcherPattern == "[0-9]+") + { + // No prefix or suffix specified - removing the separator won't + // affect the element key parts: + return GetMatchesKeyWithSeparatorCall(stringAccess, keyValue, separator); + } + + return Expression.Call( + typeof(StringExtensions).GetPublicStaticMethod("MatchesKey", parameterCount: 4), + stringAccess, + keyValue, + separator, + elementKeyPartMatcher); + } + + private static Expression GetMatchesKeyWithSeparatorCall(Expression stringAccess, Expression keyValue, + Expression separator) + { + return Expression.Call( + typeof(StringExtensions).GetPublicStaticMethod("MatchesKey", parameterCount: 3), stringAccess, - numberOfCharacters.ToConstantExpression()); + keyValue, + separator); } } } \ No newline at end of file diff --git a/AgileMapper/Extensions/Internal/StringExtensions.cs b/AgileMapper/Extensions/Internal/StringExtensions.cs new file mode 100644 index 000000000..ea090cd50 --- /dev/null +++ b/AgileMapper/Extensions/Internal/StringExtensions.cs @@ -0,0 +1,127 @@ +namespace AgileObjects.AgileMapper.Extensions.Internal +{ + using System.Text.RegularExpressions; + using static System.StringComparison; + + internal static class StringExtensions + { + public static string ToPascalCase(this string value) + => char.ToUpperInvariant(value[0]) + value.Substring(1); + + public static string ToCamelCase(this string value) + => char.ToLowerInvariant(value[0]) + value.Substring(1); + + public static string FirstOrDefault(this string value) + { + if (string.IsNullOrEmpty(value) || (value.Length <= 1)) + { + return value; + } + + return value[0].ToString(); + } + + public static bool EqualsIgnoreCase(this string value, string otherValue) + => value.Equals(otherValue, OrdinalIgnoreCase); + + public static bool StartsWithIgnoreCase(this string value, string substring) + => value.StartsWith(substring, OrdinalIgnoreCase); + + public static bool MatchesKey( + this string subjectKey, + string queryKey, + string separator, + Regex elementKeyPartMatcher) + { + if (queryKey == null) + { + // This can happen when mapping to types with multiple, nested + // recursive relationships, e.g: + // Dictionary<,> -> Order -> OrderItems -> Order -> OrderItems + // ...it's basically not supported + return false; + } + + if (subjectKey.EqualsIgnoreCase(queryKey)) + { + return true; + } + + var elementKeyParts = elementKeyPartMatcher.Matches(queryKey); + + var searchEndIndex = queryKey.Length; + + for (var i = elementKeyParts.Count; i > 0; --i) + { + var elementKeyPart = elementKeyParts[i - 1]; + var matchStartIndex = elementKeyPart.Index; + var matchEndIndex = matchStartIndex + elementKeyPart.Length; + + ReplaceSeparatorsInSubstring(matchStartIndex, matchEndIndex, ref queryKey, separator, ref searchEndIndex); + } + + ReplaceSeparatorsInSubstring(searchEndIndex, 0, ref queryKey, separator, ref searchEndIndex); + + return subjectKey.EqualsIgnoreCase(queryKey); + } + + private static void ReplaceSeparatorsInSubstring( + int matchStartIndex, + int matchEndIndex, + ref string queryKey, + string separator, + ref int searchEndIndex) + { + var querySubstring = queryKey.Substring(matchEndIndex, searchEndIndex - matchEndIndex); + + if (querySubstring.IndexOf(separator, Ordinal) == -1) + { + searchEndIndex = matchStartIndex; + return; + } + + var flattenedQuerySubstring = querySubstring.Replace(separator, null); + + queryKey = queryKey + .Remove(matchEndIndex, searchEndIndex - matchEndIndex) + .Insert(matchEndIndex, flattenedQuerySubstring); + + searchEndIndex = matchStartIndex; + } + + public static bool MatchesKey(this string subjectKey, string queryKey, string separator) + { + if (queryKey == null) + { + // This can happen when mapping to types with multiple, nested + // recursive relationships, e.g: + // Dictionary<,> -> Order -> OrderItems -> Order -> OrderItems + // ...it's basically not supported + return false; + } + + return subjectKey.EqualsIgnoreCase(queryKey) || + subjectKey.MatchesFlattenedKey(queryKey, separator); + } + + private static bool MatchesFlattenedKey(this string subjectKey, string queryKey, string separator) + { + return (queryKey.IndexOf(separator, Ordinal) != -1) && + subjectKey.EqualsIgnoreCase(queryKey.Replace(separator, null)); + } + + public static bool MatchesKey(this string subjectKey, string queryKey) + { + if (queryKey == null) + { + // This can happen when mapping to types with multiple, nested + // recursive relationships, e.g: + // Dictionary<,> -> Order -> OrderItems -> Order -> OrderItems + // ...it's basically not supported + return false; + } + + return subjectKey.EqualsIgnoreCase(queryKey); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Extensions/TrustTester.cs b/AgileMapper/Extensions/Internal/TrustTester.cs similarity index 73% rename from AgileMapper/Extensions/TrustTester.cs rename to AgileMapper/Extensions/Internal/TrustTester.cs index 40d619b22..382a86388 100644 --- a/AgileMapper/Extensions/TrustTester.cs +++ b/AgileMapper/Extensions/Internal/TrustTester.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { internal static class TrustTester { diff --git a/AgileMapper/Extensions/TypeExtensions.cs b/AgileMapper/Extensions/Internal/TypeExtensions.cs similarity index 89% rename from AgileMapper/Extensions/TypeExtensions.cs rename to AgileMapper/Extensions/Internal/TypeExtensions.cs index 54e62fc60..20f68e9ad 100644 --- a/AgileMapper/Extensions/TypeExtensions.cs +++ b/AgileMapper/Extensions/Internal/TypeExtensions.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { using System; using System.Collections; @@ -185,12 +185,17 @@ public static bool IsSimple(this Type type) { type = type.GetNonNullableType(); - if (type.GetTypeCode() == NetStandardTypeCode.Object) + if (type == typeof(ValueType)) { - return type == typeof(Guid); + return true; } - return true; + if (type.GetTypeCode() != NetStandardTypeCode.Object) + { + return true; + } + + return type == typeof(Guid); } public static bool IsDictionary(this Type type) @@ -208,29 +213,35 @@ public static bool IsDictionary(this Type type, out KeyValuePair key public static KeyValuePair GetDictionaryTypes(this Type type) { - if (!type.IsGenericType()) - { - return default(KeyValuePair); - } + var dictionaryType = GetDictionaryType(type); - var typeDefinition = type.GetGenericTypeDefinition(); + return (dictionaryType != null) + ? GetDictionaryTypesFrom(dictionaryType) + : default(KeyValuePair); + } - if ((typeDefinition == typeof(Dictionary<,>)) || (typeDefinition == typeof(IDictionary<,>))) + public static Type GetDictionaryType(this Type type) + { + if (type.IsGenericType()) { - return GetDictionaryTypesFrom(type); + var typeDefinition = type.GetGenericTypeDefinition(); + + if ((typeDefinition == typeof(Dictionary<,>)) || (typeDefinition == typeof(IDictionary<,>))) + { + return type; + } } var interfaceType = type .GetAllInterfaces() - .FirstOrDefault(t => - t.IsGenericType() && - (t.GetGenericTypeDefinition() == typeof(IDictionary<,>))); + .FirstOrDefault(t => t.IsClosedTypeOf(typeof(IDictionary<,>))); - return (interfaceType != null) - ? GetDictionaryTypesFrom(interfaceType) - : default(KeyValuePair); + return interfaceType; } + public static bool IsClosedTypeOf(this Type type, Type genericTypeDefinition) + => type.IsGenericType() && (type.GetGenericTypeDefinition() == genericTypeDefinition); + private static KeyValuePair GetDictionaryTypesFrom(Type type) { var types = type.GetGenericTypeArguments(); diff --git a/AgileMapper/Extensions/TypeInfo.cs b/AgileMapper/Extensions/Internal/TypeInfo.cs similarity index 78% rename from AgileMapper/Extensions/TypeInfo.cs rename to AgileMapper/Extensions/Internal/TypeInfo.cs index 66655b679..e02a12cd3 100644 --- a/AgileMapper/Extensions/TypeInfo.cs +++ b/AgileMapper/Extensions/Internal/TypeInfo.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.Extensions +namespace AgileObjects.AgileMapper.Extensions.Internal { internal static class TypeInfo { diff --git a/AgileMapper/Extensions/ObjectExtensions.cs b/AgileMapper/Extensions/ObjectExtensions.cs deleted file mode 100644 index 5a4078958..000000000 --- a/AgileMapper/Extensions/ObjectExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace AgileObjects.AgileMapper.Extensions -{ - using System; - using System.Reflection; - using NetStandardPolyfills; - - internal static class ObjectExtensions - { - public static readonly MethodInfo GetRuntimeSourceTypeMethod = - typeof(ObjectExtensions).GetPublicStaticMethod("GetRuntimeSourceType"); - - public static Type GetRuntimeSourceType(this TDeclared item) - { - return (item != null) ? item.GetType() : typeof(TDeclared); - } - - public static Type GetRuntimeTargetType(this TDeclared item, Type sourceType) - => (item != null) ? item.GetType() : typeof(TDeclared).GetRuntimeTargetType(sourceType); - - public static Type GetRuntimeTargetType(this Type targetType, Type sourceType) - => sourceType.IsAssignableTo(targetType) ? sourceType : targetType; - } -} diff --git a/AgileMapper/Extensions/StringExtensions.cs b/AgileMapper/Extensions/StringExtensions.cs deleted file mode 100644 index 3b856c7d0..000000000 --- a/AgileMapper/Extensions/StringExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace AgileObjects.AgileMapper.Extensions -{ - internal static class StringExtensions - { - public static string ToPascalCase(this string value) - => char.ToUpperInvariant(value[0]) + value.Substring(1); - - public static string ToCamelCase(this string value) - => char.ToLowerInvariant(value[0]) + value.Substring(1); - - public static string Left(this string value, int numberOfCharacters) - { - if (string.IsNullOrEmpty(value) || (value.Length <= numberOfCharacters)) - { - return value; - } - - return value.Substring(0, numberOfCharacters); - } - } -} \ No newline at end of file diff --git a/AgileMapper/Flattening/FlattenedObject.cs b/AgileMapper/Flattening/FlattenedObject.cs deleted file mode 100644 index 1592def4d..000000000 --- a/AgileMapper/Flattening/FlattenedObject.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace AgileObjects.AgileMapper.Flattening -{ - using System.Collections.Generic; - using System.Dynamic; - - internal class FlattenedObject : DynamicObject - { - private readonly Dictionary _propertyValuesByName; - - public FlattenedObject(Dictionary propertyValuesByName) - { - _propertyValuesByName = propertyValuesByName; - } - - public override bool TryGetMember(GetMemberBinder binder, out object result) - => _propertyValuesByName.TryGetValue(binder.Name, out result); - } -} diff --git a/AgileMapper/Flattening/ObjectFlattener.cs b/AgileMapper/Flattening/ObjectFlattener.cs deleted file mode 100644 index f38eba71f..000000000 --- a/AgileMapper/Flattening/ObjectFlattener.cs +++ /dev/null @@ -1,159 +0,0 @@ -namespace AgileObjects.AgileMapper.Flattening -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - using System.Reflection; - using Extensions; - using Members; - using NetStandardPolyfills; - - internal class ObjectFlattener - { - #region Cached Items - - private static readonly ParameterExpression _objectFlattenerParameter = Parameters.Create(); - - private static readonly MethodInfo _getPropertiesMethod = typeof(ObjectFlattener) - .GetNonPublicInstanceMethods("GetPropertyValuesByName") - .Last(); - - #endregion - - public FlattenedObject Flatten(TSource source) - => new FlattenedObject(GetPropertyValuesByName(source)); - - private Dictionary GetPropertyValuesByName(TSource source) - { - var propertyValuesByName = - GetPropertyValuesByName(source, parentMemberName: null) - .ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2); - - return propertyValuesByName; - } - - internal IEnumerable> GetPropertyValuesByName( - TSource parentObject, - string parentMemberName) - { - foreach (var sourceMember in GlobalContext.Instance.MemberCache.GetSourceMembers(typeof(TSource))) - { - var name = GetName(parentMemberName, sourceMember); - var value = GetValue(parentObject, sourceMember); - - if (sourceMember.IsSimple || (sourceMember.IsEnumerable && sourceMember.ElementType.IsSimple())) - { - yield return Tuple.Create(name, value); - continue; - } - - if (sourceMember.IsComplex) - { - foreach (var nestedPropertyValueByName in GetComplexTypePropertyValuesByName(value, sourceMember.Type, name)) - { - yield return nestedPropertyValueByName; - } - - continue; - } - - foreach (var nestedPropertyValueByName in GetEnumerablePropertyValuesByName(value, sourceMember.ElementType, name)) - { - yield return nestedPropertyValueByName; - } - } - } - - private static string GetName(string parentMemberName, Member sourceMember) - { - if (parentMemberName == null) - { - return sourceMember.Name; - } - - return parentMemberName + "_" + sourceMember.Name; - } - - private object GetValue(TSource source, Member member) - { - if (source == null) - { - return member.Type.IsValueType() ? Activator.CreateInstance(member.Type) : null; - } - - var cacheKey = typeof(TSource).FullName + $".{member.Name}: GetValue"; - - var valueFunc = GlobalContext.Instance.Cache.GetOrAdd(cacheKey, k => - { - var sourceParameter = Parameters.Create("source"); - var valueAccess = member.GetAccess(sourceParameter); - - if (member.Type.IsValueType()) - { - valueAccess = valueAccess.GetConversionTo(typeof(object)); - } - - var valueLambda = Expression.Lambda>(valueAccess, sourceParameter); - - return valueLambda.Compile(); - }); - - return valueFunc.Invoke(source); - } - - private IEnumerable> GetComplexTypePropertyValuesByName( - object parentComplexType, - Type parentMemberType, - string parentMemberName) - { - var cacheKey = parentMemberType.FullName + ": GetPropertiesCaller"; - - var getPropertiesFunc = GlobalContext.Instance.Cache.GetOrAdd(cacheKey, k => - { - var sourceParameter = Parameters.Create("source"); - var parentMemberNameParameter = Parameters.Create("parentMemberName"); - - var getPropertiesCall = Expression.Call( - _objectFlattenerParameter, - _getPropertiesMethod.MakeGenericMethod(parentMemberType), - sourceParameter.GetConversionTo(parentMemberType), - parentMemberNameParameter); - - var getPropertiesLambda = Expression - .Lambda>>>( - getPropertiesCall, - _objectFlattenerParameter, - sourceParameter, - parentMemberNameParameter); - - return getPropertiesLambda.Compile(); - }); - - return getPropertiesFunc.Invoke(this, parentComplexType, parentMemberName); - } - - private IEnumerable> GetEnumerablePropertyValuesByName( - object parentEnumerable, - Type declaredEnumerableElementType, - string parentMemberName) - { - var items = (IEnumerable)parentEnumerable; - - var i = 0; - - foreach (var item in items) - { - var itemType = item?.GetType() ?? declaredEnumerableElementType; - - foreach (var nestedPropertyValueByName in GetComplexTypePropertyValuesByName(item, itemType, parentMemberName + i)) - { - yield return nestedPropertyValueByName; - } - - ++i; - } - } - } -} \ No newline at end of file diff --git a/AgileMapper/Mapper.cs b/AgileMapper/Mapper.cs index 29c35919d..0fa76f49d 100644 --- a/AgileMapper/Mapper.cs +++ b/AgileMapper/Mapper.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper { using System; + using System.Dynamic; using System.Linq.Expressions; using Api; using Api.Configuration; @@ -204,7 +205,7 @@ TSource IMapper.Clone( return ((IMapper)this).Map(source).ToANew(configurations); } - dynamic IMapper.Flatten(TSource source) => Context.ObjectFlattener.Flatten(source); + dynamic IMapper.Flatten(TSource source) => Map(source).ToANew(); ITargetTypeSelector IMapper.Map(TSource source) => new MappingExecutor(source, Context); diff --git a/AgileMapper/MapperContext.cs b/AgileMapper/MapperContext.cs index 80d60e96b..823ad1cd8 100644 --- a/AgileMapper/MapperContext.cs +++ b/AgileMapper/MapperContext.cs @@ -4,7 +4,6 @@ using Configuration; using Configuration.Inline; using DataSources; - using Flattening; using Members; using Members.Sources; using ObjectPopulation; @@ -15,7 +14,6 @@ internal class MapperContext { internal static readonly MapperContext Default = new MapperContext(); - private ObjectFlattener _objectFlattener; private InlineMapperContextSet _inlineContexts; public MapperContext() @@ -44,8 +42,6 @@ public MapperContext() public ObjectMapperFactory ObjectMapperFactory { get; } - public ObjectFlattener ObjectFlattener => _objectFlattener ?? (_objectFlattener = new ObjectFlattener()); - public InlineMapperContextSet InlineContexts => _inlineContexts ?? (_inlineContexts = new InlineMapperContextSet(this)); public UserConfigurationSet UserConfigurations { get; } diff --git a/AgileMapper/MappingExecutor.cs b/AgileMapper/MappingExecutor.cs index 9e7fddbb9..615ead039 100644 --- a/AgileMapper/MappingExecutor.cs +++ b/AgileMapper/MappingExecutor.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; using Api; using Api.Configuration; - using Extensions; + using Extensions.Internal; using ObjectPopulation; internal class MappingExecutor : ITargetTypeSelector, IMappingContext diff --git a/AgileMapper/MappingRuleSetCollection.cs b/AgileMapper/MappingRuleSetCollection.cs index 47d619d01..9ba548543 100644 --- a/AgileMapper/MappingRuleSetCollection.cs +++ b/AgileMapper/MappingRuleSetCollection.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper { using System.Collections.Generic; - using Extensions; + using Extensions.Internal; using Members.Population; using ObjectPopulation; using ObjectPopulation.Enumerables; diff --git a/AgileMapper/Members/BasicMapperData.cs b/AgileMapper/Members/BasicMapperData.cs index 663060fc0..2f919aee2 100644 --- a/AgileMapper/Members/BasicMapperData.cs +++ b/AgileMapper/Members/BasicMapperData.cs @@ -32,5 +32,8 @@ public BasicMapperData( public Type TargetType { get; } public QualifiedMember TargetMember { get; } + + public virtual bool HasCompatibleTypes(ITypePair typePair) + => typePair.HasCompatibleTypes(this); } } \ No newline at end of file diff --git a/AgileMapper/Members/ChildMemberMapperData.cs b/AgileMapper/Members/ChildMemberMapperData.cs index 348b0b150..e8991d2d7 100644 --- a/AgileMapper/Members/ChildMemberMapperData.cs +++ b/AgileMapper/Members/ChildMemberMapperData.cs @@ -35,5 +35,7 @@ public ChildMemberMapperData(QualifiedMember targetMember, ObjectMapperData pare public Expression TargetInstance => Parent.TargetInstance; public ExpressionInfoFinder ExpressionInfoFinder => Parent.ExpressionInfoFinder; + + public override bool HasCompatibleTypes(ITypePair typePair) => Parent.HasCompatibleTypes(typePair); } } \ No newline at end of file diff --git a/AgileMapper/Members/ConfiguredSourceMember.cs b/AgileMapper/Members/ConfiguredSourceMember.cs index fc37c7c29..80ff6be45 100644 --- a/AgileMapper/Members/ConfiguredSourceMember.cs +++ b/AgileMapper/Members/ConfiguredSourceMember.cs @@ -4,7 +4,7 @@ namespace AgileObjects.AgileMapper.Members using System.Collections.Generic; using System.Linq.Expressions; using Caching; - using Extensions; + using Extensions.Internal; using ReadableExpressions; using ReadableExpressions.Extensions; @@ -57,6 +57,8 @@ private ConfiguredSourceMember( public Type Type { get; } + public string GetFriendlyTypeName() => Type.GetFriendlyName(); + public bool IsEnumerable { get; } public string Name { get; } @@ -82,6 +84,8 @@ public IQualifiedMember RelativeTo(IQualifiedMember otherMember) relativeMemberChain); } + public bool HasCompatibleType(Type type) => false; + public bool CouldMatch(QualifiedMember otherMember) => _matchedTargetMemberJoinedNames.CouldMatch(otherMember.JoinedNames); diff --git a/AgileMapper/Members/DictionaryEntrySourceMember.cs b/AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs similarity index 88% rename from AgileMapper/Members/DictionaryEntrySourceMember.cs rename to AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs index 6d6ea04a6..bb093c5e6 100644 --- a/AgileMapper/Members/DictionaryEntrySourceMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryEntrySourceMember.cs @@ -1,8 +1,8 @@ -namespace AgileObjects.AgileMapper.Members +namespace AgileObjects.AgileMapper.Members.Dictionaries { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using ReadableExpressions.Extensions; internal class DictionaryEntrySourceMember : IQualifiedMember @@ -45,13 +45,15 @@ private DictionaryEntrySourceMember( _pathFactory = pathFactory; _matchedTargetMember = matchedTargetMember; Parent = parent; - _childMembers = childMembers ?? new[] { Member.RootSource("Source", type) }; + _childMembers = childMembers ?? new[] { Member.RootSource(type) }; } public DictionarySourceMember Parent { get; } public Type Type { get; } + public string GetFriendlyTypeName() => Type.GetFriendlyName(); + public Type ElementType => _childMembers.First().ElementType; public bool IsEnumerable { get; } @@ -90,14 +92,23 @@ public IQualifiedMember WithType(Type runtimeType) Array.Copy(_childMembers, 0, childMembers, 0, childMembers.Length - 1); childMembers[childMembers.Length - 1] = _childMembers.Last().WithType(runtimeType); - return new DictionaryEntrySourceMember( + var dictionaryEntry = new DictionaryEntrySourceMember( runtimeType, _pathFactory, _matchedTargetMember, Parent, childMembers); + + if (runtimeType.IsDictionary()) + { + return new DictionarySourceMember(dictionaryEntry, _matchedTargetMember); + } + + return dictionaryEntry; } + public bool HasCompatibleType(Type type) => Parent.HasCompatibleType(type); + public bool CouldMatch(QualifiedMember otherMember) => _matchedTargetMember.CouldMatch(otherMember); public bool Matches(IQualifiedMember otherMember) diff --git a/AgileMapper/Members/DictionaryMemberMapperDataExtensions.cs b/AgileMapper/Members/Dictionaries/DictionaryMemberMapperDataExtensions.cs similarity index 89% rename from AgileMapper/Members/DictionaryMemberMapperDataExtensions.cs rename to AgileMapper/Members/Dictionaries/DictionaryMemberMapperDataExtensions.cs index c0ee05936..d874796f5 100644 --- a/AgileMapper/Members/DictionaryMemberMapperDataExtensions.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryMemberMapperDataExtensions.cs @@ -1,13 +1,31 @@ -namespace AgileObjects.AgileMapper.Members +namespace AgileObjects.AgileMapper.Members.Dictionaries { using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal static class DictionaryMemberMapperDataExtensions { + public static Expression GetDictionaryKeyPartSeparator(this IMemberMapperData mapperData) + { + return mapperData + .MapperContext + .UserConfigurations + .Dictionaries + .GetSeparator(mapperData); + } + + public static Expression GetDictionaryElementKeyPartMatcher(this IMemberMapperData mapperData) + { + return mapperData + .MapperContext + .UserConfigurations + .Dictionaries + .GetElementKeyPartMatcher(mapperData); + } + public static Expression GetTargetMemberDictionaryKey(this IMemberMapperData mapperData) { var configuredKey = mapperData.MapperContext @@ -144,7 +162,7 @@ private static void AddEnumerableMemberNamePart( memberPartExpressions.InsertRange(0, elementKeyParts); } - public static IEnumerable GetTargetMemberDictionaryElementKeyParts( + public static IList GetTargetMemberDictionaryElementKeyParts( this IMemberMapperData mapperData, Expression index) { diff --git a/AgileMapper/Members/DictionarySourceMember.cs b/AgileMapper/Members/Dictionaries/DictionarySourceMember.cs similarity index 80% rename from AgileMapper/Members/DictionarySourceMember.cs rename to AgileMapper/Members/Dictionaries/DictionarySourceMember.cs index 1e1173e7a..d44bfef1b 100644 --- a/AgileMapper/Members/DictionarySourceMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionarySourceMember.cs @@ -1,8 +1,8 @@ -namespace AgileObjects.AgileMapper.Members +namespace AgileObjects.AgileMapper.Members.Dictionaries { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; internal class DictionarySourceMember : IQualifiedMember { @@ -16,7 +16,7 @@ public DictionarySourceMember(IMemberMapperData mapperData) public DictionarySourceMember(IQualifiedMember wrappedSourceMember, QualifiedMember matchedTargetMember) : this( - wrappedSourceMember.Type, + wrappedSourceMember.Type.GetDictionaryType(), wrappedSourceMember, matchedTargetMember, wrappedSourceMember.Matches(matchedTargetMember)) @@ -49,7 +49,9 @@ private DictionarySourceMember( ValueType = valueType; } - EntryMember = new DictionaryEntrySourceMember(ValueType, matchedTargetMember, this); + EntryMember = (wrappedSourceMember as DictionaryEntrySourceMember) ?? + new DictionaryEntrySourceMember(ValueType, matchedTargetMember, this); + HasObjectEntries = ValueType == typeof(object); CouldContainSourceInstance = @@ -58,6 +60,8 @@ private DictionarySourceMember( public Type Type { get; } + public string GetFriendlyTypeName() => _wrappedSourceMember.GetFriendlyTypeName(); + public Type KeyType { get; } public Type ValueType { get; } @@ -78,12 +82,9 @@ private DictionarySourceMember( public IQualifiedMember GetElementMember() { - if (EntryMember.IsEnumerable) - { - return EntryMember.GetElementMember(); - } - - return EntryMember.GetInstanceElementMember(); + return EntryMember.IsEnumerable + ? EntryMember.GetElementMember() + : EntryMember.GetInstanceElementMember(); } public IQualifiedMember Append(Member childMember) => EntryMember.Append(childMember); @@ -99,7 +100,10 @@ public IQualifiedMember RelativeTo(IQualifiedMember otherMember) ValueType); } - public IQualifiedMember WithType(Type runtimeType) => this; + public IQualifiedMember WithType(Type runtimeType) + => (runtimeType != _wrappedSourceMember.Type) ? EntryMember.WithType(runtimeType) : this; + + public bool HasCompatibleType(Type type) => _wrappedSourceMember.HasCompatibleType(type); public bool CouldMatch(QualifiedMember otherMember) => _wrappedSourceMember.CouldMatch(otherMember); diff --git a/AgileMapper/Members/DictionaryTargetMember.cs b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs similarity index 83% rename from AgileMapper/Members/DictionaryTargetMember.cs rename to AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs index 75273f46b..a7bfe6160 100644 --- a/AgileMapper/Members/DictionaryTargetMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs @@ -1,11 +1,14 @@ -namespace AgileObjects.AgileMapper.Members +namespace AgileObjects.AgileMapper.Members.Dictionaries { using System; using System.Collections.Generic; + using System.Dynamic; + using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; using ReadableExpressions.Extensions; + using static System.Linq.Expressions.ExpressionType; internal class DictionaryTargetMember : QualifiedMember { @@ -60,6 +63,26 @@ public override Type GetElementType(Type sourceElementType) public override bool GuardObjectValuePopulations => true; + public override bool HasCompatibleType(Type type) + { + if (type == typeof(ExpandoObject)) + { + return _rootDictionaryMember.Type == typeof(ExpandoObject); + } + + if (base.HasCompatibleType(type)) + { + return true; + } + + if (this != _rootDictionaryMember) + { + return false; + } + + return (_rootDictionaryMember.Type != typeof(ExpandoObject)) && type.IsDictionary(); + } + public DictionaryTargetMember Append(ParameterExpression key) { var memberKey = new DictionaryMemberKey(ValueType, key.Name, this); @@ -134,28 +157,15 @@ public override Expression GetAccess(Expression instance, IMemberMapperData mapp return base.GetAccess(instance, mapperData); } - if (ReturnNullAccess()) + if (ReturnKeyedAccess()) { - return Type.ToDefaultExpression(); + return GetKeyedAccess(mapperData); } - return GetKeyedAccess(mapperData); + return Type.ToDefaultExpression(); } - private bool ReturnNullAccess() - { - if (Type == ValueType) - { - return false; - } - - if (Type.IsSimple()) - { - return false; - } - - return true; - } + private bool ReturnKeyedAccess() => (Type == ValueType) || Type.IsSimple(); private Expression GetKeyedAccess(IMemberMapperData mapperData) { @@ -173,7 +183,7 @@ private Expression GetDictionaryAccess(IMemberMapperData mapperData) { var parentContextAccess = mapperData.GetAppropriateMappingContextAccess(typeof(object), _rootDictionaryMember.Type); - if (parentContextAccess.NodeType != ExpressionType.Parameter) + if (parentContextAccess.NodeType != Parameter) { return MemberMapperDataExtensions.GetTargetAccess(parentContextAccess, _rootDictionaryMember.Type); } @@ -210,7 +220,7 @@ public override BlockExpression GetAccessChecked(IMemberMapperData mapperData) private Expression GetTryGetValueCall(IMemberMapperData mapperData, out ParameterExpression valueVariable) { var dictionaryAccess = GetDictionaryAccess(mapperData); - var tryGetValueMethod = dictionaryAccess.Type.GetPublicInstanceMethod("TryGetValue"); + var tryGetValueMethod = dictionaryAccess.Type.GetDictionaryType().GetPublicInstanceMethod("TryGetValue"); var key = GetKey(mapperData); valueVariable = Expression.Variable(ValueType, "existingValue"); @@ -256,7 +266,7 @@ public override Expression GetPopulation(Expression value, IMemberMapperData map return keyedAssignment; } - private bool ValueIsFlattening(Expression value, out BlockExpression flattening) + private bool ValueIsFlattening(Expression value, out Expression flattening) { if (!(HasObjectEntries || HasSimpleEntries)) { @@ -264,32 +274,53 @@ private bool ValueIsFlattening(Expression value, out BlockExpression flattening) return false; } - ICollection blockParameters; + if (value.NodeType == Try) + { + value = ((TryExpression)value).Body; + } + + if ((value.NodeType != Block)) + { + flattening = null; + return false; + } + + var flatteningBlock = (BlockExpression)value; + var flatteningVariables = flatteningBlock.Variables.ToList(); - if (value.NodeType == ExpressionType.Block) + if (flatteningBlock.Expressions[0].NodeType == Try) { - flattening = (BlockExpression)value; - blockParameters = flattening.Variables; - value = flattening.Expressions[0]; + flatteningBlock = (BlockExpression)((TryExpression)flatteningBlock.Expressions[0]).Body; + flatteningVariables.AddRange(flatteningBlock.Variables); } else { - blockParameters = Enumerable.EmptyArray; + flatteningBlock = (BlockExpression)value; } - if (value.NodeType != ExpressionType.Try) + if (flatteningBlock.Expressions.HasOne()) { flattening = null; return false; } - flattening = (BlockExpression)((TryExpression)value).Body; + flattening = flatteningBlock; + var flatteningExpressions = GetMappingExpressions(flattening); - flattening = blockParameters.Any() - ? Expression.Block(blockParameters, flatteningExpressions) + if (flatteningExpressions.HasOne() && + (flatteningExpressions[0].NodeType == Block)) + { + flatteningBlock = (BlockExpression)flatteningExpressions[0]; + flatteningVariables.AddRange(flatteningBlock.Variables); + flattening = flatteningBlock.Update(flatteningVariables, flatteningBlock.Expressions); + return true; + } + + flattening = flatteningVariables.Any() + ? Expression.Block(flatteningVariables, flatteningExpressions) : flatteningExpressions.HasOne() - ? (BlockExpression)flatteningExpressions[0] + ? flatteningExpressions[0] : Expression.Block(flatteningExpressions); return true; @@ -299,12 +330,11 @@ private static IList GetMappingExpressions(Expression mapping) { var expressions = new List(); - while (mapping.NodeType == ExpressionType.Block) + while (mapping.NodeType == Block) { var mappingBlock = (BlockExpression)mapping; - expressions.AddRange(mappingBlock.Expressions); - expressions.Remove(mappingBlock.Result); + expressions.AddRange(mappingBlock.Expressions.Except(new[] { mappingBlock.Result })); mapping = mappingBlock.Result; } diff --git a/AgileMapper/Members/ExpressionInfoFinder.cs b/AgileMapper/Members/ExpressionInfoFinder.cs index 6820c713a..ebbee8f21 100644 --- a/AgileMapper/Members/ExpressionInfoFinder.cs +++ b/AgileMapper/Members/ExpressionInfoFinder.cs @@ -3,8 +3,9 @@ namespace AgileObjects.AgileMapper.Members using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using ReadableExpressions.Extensions; + using static Member; internal class ExpressionInfoFinder { @@ -131,12 +132,12 @@ private bool IsNotRootObject(MemberExpression memberAccess) return false; } - if (memberAccess.Member.Name == "Source") + if (memberAccess.Member.Name == RootSourceMemberName) { return false; } - return _includeTargetNullChecking || (memberAccess.Member.Name != "Target"); + return _includeTargetNullChecking || (memberAccess.Member.Name != RootTargetMemberName); } private static bool IsNullableHasValueAccess(MemberExpression memberAccess) diff --git a/AgileMapper/Members/IBasicMapperData.cs b/AgileMapper/Members/IBasicMapperData.cs index 92e473d81..c71cd0add 100644 --- a/AgileMapper/Members/IBasicMapperData.cs +++ b/AgileMapper/Members/IBasicMapperData.cs @@ -1,8 +1,6 @@ namespace AgileObjects.AgileMapper.Members { - using System; - - internal interface IBasicMapperData + internal interface IBasicMapperData : ITypePair { MappingRuleSet RuleSet { get; } @@ -10,10 +8,8 @@ internal interface IBasicMapperData IBasicMapperData Parent { get; } - Type SourceType { get; } - - Type TargetType { get; } - QualifiedMember TargetMember { get; } + + bool HasCompatibleTypes(ITypePair typePair); } } \ No newline at end of file diff --git a/AgileMapper/Members/IQualifiedMember.cs b/AgileMapper/Members/IQualifiedMember.cs index 17a866ce2..8eb7290ea 100644 --- a/AgileMapper/Members/IQualifiedMember.cs +++ b/AgileMapper/Members/IQualifiedMember.cs @@ -7,6 +7,8 @@ internal interface IQualifiedMember { Type Type { get; } + string GetFriendlyTypeName(); + bool IsEnumerable { get; } string Name { get; } @@ -21,6 +23,8 @@ internal interface IQualifiedMember IQualifiedMember WithType(Type runtimeType); + bool HasCompatibleType(Type type); + bool CouldMatch(QualifiedMember otherMember); bool Matches(IQualifiedMember otherMember); diff --git a/AgileMapper/Members/ITypePair.cs b/AgileMapper/Members/ITypePair.cs new file mode 100644 index 000000000..726fd1a64 --- /dev/null +++ b/AgileMapper/Members/ITypePair.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.Members +{ + using System; + + internal interface ITypePair + { + Type SourceType { get; } + + Type TargetType { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/MappingTypes.cs b/AgileMapper/Members/MappingTypes.cs index 22d226a50..c2cc770c3 100644 --- a/AgileMapper/Members/MappingTypes.cs +++ b/AgileMapper/Members/MappingTypes.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.Members { using System; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class MappingTypes diff --git a/AgileMapper/Members/Member.cs b/AgileMapper/Members/Member.cs index 21fa9d888..c6d0e4c85 100644 --- a/AgileMapper/Members/Member.cs +++ b/AgileMapper/Members/Member.cs @@ -6,13 +6,17 @@ namespace AgileObjects.AgileMapper.Members #endif using System.Linq.Expressions; using System.Reflection; - using Extensions; + using Dictionaries; + using Extensions.Internal; using NetStandardPolyfills; using ObjectPopulation; using ReadableExpressions.Extensions; internal class Member { + public const string RootSourceMemberName = "Source"; + public const string RootTargetMemberName = "Target"; + private readonly Func _accessFactory; private Member( @@ -70,13 +74,13 @@ private Member( public static Member RootSource() => SourceMemberCache.MemberInstance; - public static Member RootSource(Type type) => RootSource("Source", type); + public static Member RootSource(Type type) => RootSource(RootSourceMemberName, type); public static Member RootSource(string signature, Type type) => Root(signature, type); public static Member RootTarget() => TargetMemberCache.MemberInstance; - public static Member RootTarget(Type type) => Root("Target", type); + public static Member RootTarget(Type type) => Root(RootTargetMemberName, type); private static Member Root(string name, Type type) { diff --git a/AgileMapper/Members/MemberExtensions.cs b/AgileMapper/Members/MemberExtensions.cs index 6b78867d4..4f7376362 100644 --- a/AgileMapper/Members/MemberExtensions.cs +++ b/AgileMapper/Members/MemberExtensions.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; using ReadableExpressions.Extensions; using static System.StringComparison; @@ -25,7 +25,7 @@ public static string GetFriendlyTargetPath(this IQualifiedMember targetMember, I private static string GetMemberPath(IQualifiedMember member, IQualifiedMember rootMember) { - var rootTypeName = rootMember.Type.GetFriendlyName(); + var rootTypeName = rootMember.GetFriendlyTypeName(); var memberPath = member.GetPath(); if (memberPath == rootMember.Name) @@ -89,9 +89,7 @@ public static bool IsUnmappable(this QualifiedMember member, out string reason) return true; } - if (member.IsEnumerable && - member.Type.IsGenericType() && - (member.Type.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>))) + if (member.IsEnumerable && member.Type.IsClosedTypeOf(typeof(ReadOnlyCollection<>))) { reason = "readonly " + member.Type.GetFriendlyName(); return true; @@ -143,7 +141,7 @@ public static bool CouldMatch(this ICollection memberNames, ICollection< return otherMemberNames .Any(otherJoinedName => (otherJoinedName == Constants.RootMemberName) || memberNames - .Any(joinedName => (joinedName == Constants.RootMemberName) || otherJoinedName.StartsWith(joinedName, OrdinalIgnoreCase))); + .Any(joinedName => (joinedName == Constants.RootMemberName) || otherJoinedName.StartsWithIgnoreCase(joinedName))); } public static bool Match(this ICollection memberNames, ICollection otherMemberNames) @@ -158,8 +156,8 @@ public static bool Match(this ICollection memberNames, ICollection otherMemberName.Equals(memberName, OrdinalIgnoreCase)); + ? memberName.EqualsIgnoreCase(otherMemberNames.First()) + : otherMemberNames.Any(otherMemberName => otherMemberName.EqualsIgnoreCase(memberName)); } public static TMember GetElementMember(this TMember enumerableMember) diff --git a/AgileMapper/Members/MemberFinder.cs b/AgileMapper/Members/MemberFinder.cs index a39107f11..59f62f929 100644 --- a/AgileMapper/Members/MemberFinder.cs +++ b/AgileMapper/Members/MemberFinder.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Reflection; using Caching; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class MemberCache @@ -105,7 +105,7 @@ private static IEnumerable GetMethods( private static bool OnlyRelevantCallable(MethodBase method) { return !method.IsSpecialName && - method.Name.StartsWith("Get", StringComparison.OrdinalIgnoreCase) && + method.Name.StartsWithIgnoreCase("Get") && (Array.IndexOf(_methodsToIgnore, method.Name) == -1) && method.GetParameters().None(); } @@ -113,7 +113,7 @@ private static bool OnlyRelevantCallable(MethodBase method) private static bool OnlyCallableSetters(MethodInfo method) { return !method.IsSpecialName && - method.Name.StartsWith("Set", StringComparison.OrdinalIgnoreCase) && + method.Name.StartsWithIgnoreCase("Set") && method.GetParameters().HasOne(); } diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 101570238..4ef79b0fe 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -7,9 +7,11 @@ namespace AgileObjects.AgileMapper.Members using System.Linq.Expressions; using System.Reflection; using DataSources; - using Extensions; + using Dictionaries; + using Extensions.Internal; using NetStandardPolyfills; using ObjectPopulation; + using static Member; internal static class MemberMapperDataExtensions { @@ -80,10 +82,8 @@ public static ExpressionInfoFinder.ExpressionInfo GetExpressionInfoFor( return mapperData.ExpressionInfoFinder.FindIn(value, targetCanBeNull); } - public static bool SourceIsNotFlatObject(this IMemberMapperData mapperData) - { - return !mapperData.SourceType.IsDictionary(); - } + public static bool SourceIsFlatObject(this IMemberMapperData mapperData) + => mapperData.SourceType.IsDictionary(); public static bool SourceMemberIsStringKeyedDictionary( this IMemberMapperData mapperData, @@ -106,16 +106,6 @@ public static DictionarySourceMember GetDictionarySourceMemberOrNull(this IMembe return dictionarySourceMember; } - if (!(mapperData.SourceMember is DictionaryEntrySourceMember dictionaryEntrySourceMember)) - { - return null; - } - - if (dictionaryEntrySourceMember.Type.IsDictionary()) - { - return dictionaryEntrySourceMember.Parent; - } - // We're mapping a dictionary entry by its runtime type: return null; } @@ -474,10 +464,10 @@ private static Expression GetAccess( return accessMethodFactory.Invoke(contextAccess, type); } - var propertyName = new[] { "Source", "Target" }[contextTypesIndex]; + var propertyName = new[] { RootSourceMemberName, RootTargetMemberName }[contextTypesIndex]; - var property = contextAccess.Type.GetPublicInstanceProperty(propertyName) - ?? typeof(IMappingData<,>) + var property = contextAccess.Type.GetPublicInstanceProperty(propertyName) ?? + typeof(IMappingData<,>) .MakeGenericType(contextTypes[0], contextTypes[1]) .GetPublicInstanceProperty(propertyName); diff --git a/AgileMapper/Members/NamingSettings.cs b/AgileMapper/Members/NamingSettings.cs index fdc267932..6c81aff4f 100644 --- a/AgileMapper/Members/NamingSettings.cs +++ b/AgileMapper/Members/NamingSettings.cs @@ -6,7 +6,7 @@ using System.Text.RegularExpressions; using Caching; using Configuration; - using Extensions; + using Extensions.Internal; internal class NamingSettings { diff --git a/AgileMapper/Members/Population/MemberPopulation.cs b/AgileMapper/Members/Population/MemberPopulation.cs index 3ac7537a3..bbfca6153 100644 --- a/AgileMapper/Members/Population/MemberPopulation.cs +++ b/AgileMapper/Members/Population/MemberPopulation.cs @@ -5,7 +5,7 @@ namespace AgileObjects.AgileMapper.Members.Population using System.Linq.Expressions; using Configuration; using DataSources; - using Extensions; + using Extensions.Internal; using ReadableExpressions; internal class MemberPopulation : IMemberPopulation diff --git a/AgileMapper/Members/QualifiedMember.cs b/AgileMapper/Members/QualifiedMember.cs index 8125a4d4f..c3490e570 100644 --- a/AgileMapper/Members/QualifiedMember.cs +++ b/AgileMapper/Members/QualifiedMember.cs @@ -6,7 +6,9 @@ namespace AgileObjects.AgileMapper.Members using System.Linq; using System.Linq.Expressions; using Caching; - using Extensions; + using Dictionaries; + using Extensions.Internal; + using NetStandardPolyfills; using ReadableExpressions.Extensions; internal class QualifiedMember : IQualifiedMember @@ -141,6 +143,8 @@ public static QualifiedMember From(Member[] memberChain, MapperContext mapperCon public Type Type => LeafMember?.Type; + public string GetFriendlyTypeName() => Type.GetFriendlyName(); + public Type ElementType => LeafMember?.ElementType; public virtual Type GetElementType(Type sourceElementType) => ElementType; @@ -206,6 +210,8 @@ public bool IsRecursionRoot() public virtual bool GuardObjectValuePopulations => false; + public virtual bool HasCompatibleType(Type type) => Type.IsAssignableTo(type); + IQualifiedMember IQualifiedMember.GetElementMember() => this.GetElementMember(); IQualifiedMember IQualifiedMember.Append(Member childMember) => Append(childMember); @@ -215,12 +221,7 @@ public QualifiedMember Append(Member childMember) => _childMemberCache.GetOrAdd(childMember, CreateChildMember); protected virtual QualifiedMember CreateChildMember(Member childMember) - { - var qualifiedChildMember = new QualifiedMember(childMember, this, _mapperContext); - qualifiedChildMember = _mapperContext.QualifiedMemberFactory.GetFinalTargetMember(qualifiedChildMember); - - return qualifiedChildMember; - } + => CreateFinalMember(new QualifiedMember(childMember, this, _mapperContext)); public QualifiedMember GetChildMember(string registrationName) => _childMemberCache.Values.First(childMember => childMember.RegistrationName == registrationName); @@ -241,7 +242,17 @@ public IQualifiedMember RelativeTo(IQualifiedMember otherMember) return new QualifiedMember(relativeMemberChain, this); } - IQualifiedMember IQualifiedMember.WithType(Type runtimeType) => WithType(runtimeType); + IQualifiedMember IQualifiedMember.WithType(Type runtimeType) + { + var typedMember = WithType(runtimeType); + + if (runtimeType.IsDictionary()) + { + return new DictionarySourceMember(typedMember, typedMember); + } + + return typedMember; + } public QualifiedMember WithType(Type runtimeType) { @@ -267,9 +278,18 @@ protected virtual QualifiedMember CreateRuntimeTypedMember(Type runtimeType) newMemberChain[MemberChain.Length - 1] = LeafMember.WithType(runtimeType); - return new QualifiedMember(newMemberChain, this); + return CreateFinalMember(new QualifiedMember(newMemberChain, this)); } + private QualifiedMember CreateFinalMember(QualifiedMember member) + { + return member.IsTargetMember + ? _mapperContext.QualifiedMemberFactory.GetFinalTargetMember(member) + : member; + } + + private bool IsTargetMember => MemberChain[0].Name == Member.RootTargetMemberName; + public bool CouldMatch(QualifiedMember otherMember) => JoinedNames.CouldMatch(otherMember.JoinedNames); public virtual bool Matches(IQualifiedMember otherMember) diff --git a/AgileMapper/Members/QualifiedMemberFactory.cs b/AgileMapper/Members/QualifiedMemberFactory.cs index 2a21ab84f..f54aa88c8 100644 --- a/AgileMapper/Members/QualifiedMemberFactory.cs +++ b/AgileMapper/Members/QualifiedMemberFactory.cs @@ -1,7 +1,8 @@ namespace AgileObjects.AgileMapper.Members { using Caching; - using Extensions; + using Dictionaries; + using Extensions.Internal; internal class QualifiedMemberFactory { diff --git a/AgileMapper/Members/SourceMemberMatcher.cs b/AgileMapper/Members/SourceMemberMatcher.cs index e992f8017..8458f2f51 100644 --- a/AgileMapper/Members/SourceMemberMatcher.cs +++ b/AgileMapper/Members/SourceMemberMatcher.cs @@ -6,35 +6,55 @@ internal static class SourceMemberMatcher { - public static IQualifiedMember GetMatchFor(IChildMemberMappingData targetData) + public static IQualifiedMember GetMatchFor( + IChildMemberMappingData targetData, + out IChildMemberMappingData contextMappingData) { var parentSourceMember = targetData.MapperData.SourceMember; if (ExactMatchingSourceMemberExists(parentSourceMember, targetData, out var matchingMember)) { + contextMappingData = targetData; return GetFinalSourceMember(matchingMember, targetData); } matchingMember = EnumerateSourceMembers(parentSourceMember, targetData) .FirstOrDefault(sm => IsMatchingMember(sm, targetData.MapperData)); - if (matchingMember == null) + if (matchingMember != null) { - return null; + contextMappingData = targetData; + return GetFinalSourceMember(matchingMember, targetData); } - return GetFinalSourceMember(matchingMember, targetData); - } + var mappingData = targetData.Parent; - private static IQualifiedMember GetFinalSourceMember( - IQualifiedMember sourceMember, - IChildMemberMappingData targetData) - { - return targetData - .MapperData - .MapperContext - .QualifiedMemberFactory - .GetFinalSourceMember(sourceMember, targetData.MapperData.TargetMember); + while (mappingData.Parent != null) + { + if (mappingData.MapperData.TargetMemberIsEnumerableElement()) + { + contextMappingData = null; + return null; + } + + mappingData = mappingData.Parent; + + var childMapperData = new ChildMemberMapperData(targetData.MapperData.TargetMember, mappingData.MapperData); + contextMappingData = mappingData.GetChildMappingData(childMapperData); + + matchingMember = EnumerateSourceMembers(mappingData.MapperData.SourceMember, contextMappingData) + .FirstOrDefault(sm => IsMatchingMember(sm, targetData.MapperData)); + + if (matchingMember == null) + { + continue; + } + + return GetFinalSourceMember(matchingMember, targetData); + } + + contextMappingData = null; + return null; } private static bool ExactMatchingSourceMemberExists( @@ -70,6 +90,17 @@ private static IEnumerable QuerySourceMembers( .Where(filter); } + private static IQualifiedMember GetFinalSourceMember( + IQualifiedMember sourceMember, + IChildMemberMappingData targetData) + { + return targetData + .MapperData + .MapperContext + .QualifiedMemberFactory + .GetFinalSourceMember(sourceMember, targetData.MapperData.TargetMember); + } + private static IEnumerable EnumerateSourceMembers( IQualifiedMember parentMember, IChildMemberMappingData rootData) diff --git a/AgileMapper/Members/TypePairExtensions.cs b/AgileMapper/Members/TypePairExtensions.cs new file mode 100644 index 000000000..02ebc8954 --- /dev/null +++ b/AgileMapper/Members/TypePairExtensions.cs @@ -0,0 +1,42 @@ +namespace AgileObjects.AgileMapper.Members +{ + using System; + using NetStandardPolyfills; + + internal static class TypePairExtensions + { + public static bool IsForAllSourceTypes(this ITypePair typePair) + => typePair.SourceType == Constants.AllTypes; + + public static bool IsForSourceType(this ITypePair typePair, ITypePair otherTypePair) + => typePair.IsForSourceType(otherTypePair.SourceType); + + private static bool IsForSourceType(this ITypePair typePair, Type sourceType) + => IsForAllSourceTypes(typePair) || sourceType.IsAssignableTo(typePair.SourceType); + + public static bool IsForTargetType(this ITypePair typePair, ITypePair otherTypePair) + => otherTypePair.TargetType.IsAssignableTo(typePair.TargetType); + + public static bool HasCompatibleTypes( + this ITypePair typePair, + ITypePair otherTypePair, + Func sourceTypeMatcher = null, + Func targetTypeMatcher = null) + { + var sourceTypesMatch = + typePair.IsForSourceType(otherTypePair.SourceType) || + (sourceTypeMatcher?.Invoke() == true); + + if (!sourceTypesMatch) + { + return false; + } + + var targetTypesMatch = + targetTypeMatcher?.Invoke() ?? + otherTypePair.TargetType.IsAssignableTo(typePair.TargetType); + + return targetTypesMatch; + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ChildMemberMappingData.cs b/AgileMapper/ObjectPopulation/ChildMemberMappingData.cs index 6a84f8e93..73401394a 100644 --- a/AgileMapper/ObjectPopulation/ChildMemberMappingData.cs +++ b/AgileMapper/ObjectPopulation/ChildMemberMappingData.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System; using System.Linq.Expressions; using Caching; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; @@ -12,17 +12,18 @@ internal class ChildMemberMappingData : IChildMemberMappingDat private readonly ObjectMappingData _parent; private readonly ICache> _runtimeTypeGettersCache; - public ChildMemberMappingData(ObjectMappingData parent) + public ChildMemberMappingData(ObjectMappingData parent, IMemberMapperData mapperData) { _parent = parent; _runtimeTypeGettersCache = parent.MapperContext.Cache.CreateScoped>(); + MapperData = mapperData; } public MappingRuleSet RuleSet => _parent.MappingContext.RuleSet; public IObjectMappingData Parent => _parent; - public IMemberMapperData MapperData { get; internal set; } + public IMemberMapperData MapperData { get; } public Type GetSourceMemberRuntimeType(IQualifiedMember sourceMember) { diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index 37480c979..250c49da1 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -7,7 +7,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using System.Reflection; using Caching; using DataSources; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs index e52a9bb4c..5678191f2 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; using ReadableExpressions; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs index 17d0bf215..918e58c75 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using Members.Population; using NetStandardPolyfills; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs index cf52a608e..3b26db8ee 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs @@ -1,12 +1,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { - using System.Collections.Generic; using System.Linq.Expressions; using DataSources; - using Extensions; + using Extensions.Internal; using Members; - using NetStandardPolyfills; - using ReadableExpressions.Extensions; internal class SourceDictionaryShortCircuitFactory : ISourceShortCircuitFactory { @@ -51,11 +48,6 @@ public Expression GetShortCircuit(IObjectMappingData mappingData) var mapValueCall = GetMapValueCall(dictionaryVariables.Value, mapperData); var fallbackValue = GetFallbackValue(mappingData); - if (mapperData.TargetMember.IsRecursionRoot()) - { - AdjustForStandaloneContext(ref mapValueCall, ref fallbackValue, mapperData); - } - var valueMappingOrFallback = Expression.Condition(foundValueNonNull, mapValueCall, fallbackValue); var returnMapValueResult = Expression.Return(mapperData.ReturnLabelTarget, valueMappingOrFallback); var ifEntryExistsShortCircuit = Expression.IfThen(entryExistsTest, returnMapValueResult); @@ -87,51 +79,15 @@ private static Expression GetEntryExistsTest(DictionaryEntryVariablePair diction } private static MethodCallExpression GetMapValueCall(Expression sourceValue, IMemberMapperData mapperData) - { - if (mapperData.TargetMemberIsEnumerableElement()) - { - return mapperData.Parent.GetMapCall(sourceValue); - } - - return mapperData.Parent.GetMapCall(sourceValue, mapperData.TargetMember, dataSourceIndex: 0); - } + => mapperData.Parent.GetMapCall(sourceValue, mapperData.TargetMember, dataSourceIndex: 0); private static Expression GetFallbackValue(IObjectMappingData mappingData) { - if (mappingData.MapperData.TargetMemberIsEnumerableElement()) - { - return mappingData.MapperData.TargetMember.Type.ToDefaultExpression(); - } - return mappingData.MappingContext .RuleSet .FallbackDataSourceFactory .Create(mappingData.MapperData) .Value; } - - private static void AdjustForStandaloneContext( - ref MethodCallExpression mapValueCall, - ref Expression fallbackValue, - IMemberMapperData mapperData) - { - var parentMappingTypes = mapperData.Parent.MappingDataObject.Type.GetGenericTypeArguments(); - var parentContextAccess = mapperData.GetAppropriateMappingContextAccess(parentMappingTypes); - var typedParentContextAccess = mapperData.GetTypedContextAccess(parentContextAccess, parentMappingTypes); - var parentTargetAccess = mapperData.GetTargetAccess(parentContextAccess, mapperData.TargetType); - - var replacements = new Dictionary(2) - { - [mapValueCall.GetSubject()] = typedParentContextAccess, - [mapValueCall.Arguments[1]] = parentTargetAccess - }; - - mapValueCall = mapValueCall.Replace(replacements); - - if (fallbackValue.NodeType != ExpressionType.Default) - { - fallbackValue = parentTargetAccess; - } - } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs index b99aab546..26f408b0a 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members.Population; internal class StructPopulationExpressionFactory : PopulationExpressionFactoryBase diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs index 7cc585a1f..43b0caffa 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs @@ -4,7 +4,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal static class TargetObjectResolutionFactory diff --git a/AgileMapper/ObjectPopulation/DefaultValueDataSourceFactory.cs b/AgileMapper/ObjectPopulation/DefaultValueDataSourceFactory.cs index 24cab7ba2..6cc1f2bbb 100644 --- a/AgileMapper/ObjectPopulation/DefaultValueDataSourceFactory.cs +++ b/AgileMapper/ObjectPopulation/DefaultValueDataSourceFactory.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using DataSources; - using Extensions; + using Extensions.Internal; using Members; internal class DefaultValueDataSourceFactory : IDataSourceFactory diff --git a/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs b/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs index 8e331cc7f..358390938 100644 --- a/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs +++ b/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs @@ -5,7 +5,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Linq; using System.Linq.Expressions; using Configuration; - using Extensions; + using Extensions.Internal; using Members; internal static class DerivedComplexTypeMappingsFactory diff --git a/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs b/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs index b94dee13b..d30227485 100644 --- a/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs +++ b/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal static class DerivedMappingFactory diff --git a/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs b/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs index 892f1020e..29fde6b8a 100644 --- a/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs +++ b/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal class DerivedSourceTypeCheck diff --git a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs index 8cd084403..b157b15b9 100644 --- a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs @@ -8,8 +8,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using ComplexTypes; using DataSources; using Enumerables.Dictionaries; - using Extensions; + using Extensions.Internal; using Members; + using Members.Dictionaries; using NetStandardPolyfills; using ReadableExpressions; @@ -28,7 +29,7 @@ private DictionaryMappingExpressionFactory() private static IEnumerable GetAllTargetMembers(ObjectMapperData mapperData) { - var targetMembersFromSource = EnumerateTargetMembers(mapperData).ToArray(); + var targetMembersFromSource = EnumerateAllTargetMembers(mapperData).ToArray(); var configuredDataSourceFactories = mapperData.MapperContext .UserConfigurations @@ -49,29 +50,146 @@ private static IEnumerable GetAllTargetMembers(ObjectMapperData return allTargetMembers; } - private static IEnumerable EnumerateTargetMembers(ObjectMapperData mapperData) + private static IEnumerable EnumerateAllTargetMembers(ObjectMapperData mapperData) { - var targetDictionaryMember = (DictionaryTargetMember)mapperData.TargetMember; var sourceMembers = GlobalContext.Instance.MemberCache.GetSourceMembers(mapperData.SourceType); + var targetDictionaryMember = (DictionaryTargetMember)mapperData.TargetMember; + + var targetMembers = EnumerateTargetMembers( + sourceMembers, + targetDictionaryMember, + mapperData, + m => m.Name); + + foreach (var targetMember in targetMembers) + { + yield return targetMember; + } + + if (mapperData.IsRoot) + { + yield break; + } + + foreach (var targetMember in GetParentContextFlattenedTargetMembers(mapperData, targetDictionaryMember)) + { + yield return targetMember; + } + } + + private static IEnumerable GetParentContextFlattenedTargetMembers( + ObjectMapperData mapperData, + DictionaryTargetMember targetDictionaryMember) + { + while (mapperData.Parent != null) + { + mapperData = mapperData.Parent; + + var sourceMembers = GlobalContext.Instance + .MemberCache + .GetSourceMembers(mapperData.SourceType) + .SelectMany(sm => MatchingFlattenedMembers(sm, targetDictionaryMember)) + .ToArray(); + + var targetMembers = EnumerateTargetMembers( + sourceMembers, + targetDictionaryMember, + mapperData, + m => m.Name.StartsWithIgnoreCase(targetDictionaryMember.Name) + ? m.Name.Substring(targetDictionaryMember.Name.Length) + : m.Name); + + foreach (var targetMember in targetMembers) + { + yield return targetMember; + } + } + } + + private static IEnumerable MatchingFlattenedMembers(Member sourceMember, IQualifiedMember targetDictionaryMember) + { + if (sourceMember.Name.EqualsIgnoreCase(targetDictionaryMember.Name)) + { + return Enumerable.Empty; + } + + if (sourceMember.Name.StartsWithIgnoreCase(targetDictionaryMember.Name)) + { + // e.g. ValueLine1 -> Value + return new[] { sourceMember }; + } + + if (!targetDictionaryMember.Name.StartsWithIgnoreCase(sourceMember.Name)) + { + return Enumerable.Empty; + } + + // e.g. Val => Value + return GetNestedFlattenedMembers(sourceMember, sourceMember.Name, targetDictionaryMember.Name); + } + private static IEnumerable GetNestedFlattenedMembers( + Member parentMember, + string sourceMemberNameMatchSoFar, + string targetMemberName) + { + return GlobalContext.Instance + .MemberCache + .GetSourceMembers(parentMember.Type) + .SelectMany(sm => + { + var flattenedSourceMemberName = sourceMemberNameMatchSoFar + sm.Name; + + if (!targetMemberName.StartsWithIgnoreCase(flattenedSourceMemberName)) + { + return Enumerable.Empty; + } + + if (targetMemberName.EqualsIgnoreCase(flattenedSourceMemberName)) + { + return GlobalContext.Instance + .MemberCache + .GetSourceMembers(sm.Type); + } + + return GetNestedFlattenedMembers( + sm, + flattenedSourceMemberName, + targetMemberName); + }) + .ToArray(); + } + + private static IEnumerable EnumerateTargetMembers( + IEnumerable sourceMembers, + DictionaryTargetMember targetDictionaryMember, + ObjectMapperData mapperData, + Func targetMemberNameFactory) + { foreach (var sourceMember in sourceMembers) { - var entryTargetMember = targetDictionaryMember.Append(sourceMember.DeclaringType, sourceMember.Name); + var targetEntryMemberName = targetMemberNameFactory.Invoke(sourceMember); + var targetEntryMember = targetDictionaryMember.Append(sourceMember.DeclaringType, targetEntryMemberName); - var entryMapperData = new ChildMemberMapperData(entryTargetMember, mapperData); + if (targetDictionaryMember.HasObjectEntries) + { + targetEntryMember = (DictionaryTargetMember)targetEntryMember.WithType(sourceMember.Type); + } + + var entryMapperData = new ChildMemberMapperData(targetEntryMember, mapperData); var configuredKey = GetCustomKeyOrNull(entryMapperData); if (configuredKey != null) { - entryTargetMember.SetCustomKey(configuredKey); + targetEntryMember.SetCustomKey(configuredKey); } if (!sourceMember.IsSimple) { - entryTargetMember = entryTargetMember.WithTypeOf(sourceMember); + targetEntryMember = targetEntryMember.WithTypeOf(sourceMember); } - yield return entryTargetMember; + yield return targetEntryMember; } } @@ -284,33 +402,14 @@ private static Expression GetMappedDictionaryAssignment( var comparerProperty = mapperData.SourceObject.Type.GetPublicInstanceProperty("Comparer"); - Expression dictionaryConstruction; + var comparer = Expression.Property(mapperData.SourceObject, comparerProperty); - if (comparerProperty == null) - { - if (mapperData.TargetType.IsInterface()) - { - dictionaryConstruction = Expression.New(GetConcreteDictionaryType(mapperData.TargetType)); - } - else - { - dictionaryConstruction = mapperData - .MapperContext - .ConstructionFactory - .GetNewObjectCreation(mappingData); - } - } - else - { - var comparer = Expression.Property(mapperData.SourceObject, comparerProperty); - - var constructor = FindDictionaryConstructor( - mapperData.TargetType, - comparer.Type, - numberOfParameters: 1); + var constructor = FindDictionaryConstructor( + mapperData.TargetType, + comparer.Type, + numberOfParameters: 1); - dictionaryConstruction = Expression.New(constructor, comparer); - } + var dictionaryConstruction = Expression.New(constructor, comparer); return GetDictionaryAssignment(dictionaryConstruction, mappingData); } @@ -349,6 +448,11 @@ private static Expression GetDictionaryAssignment(Expression value, IObjectMappi mappingData, mapperData.HasMapperFuncs); + if (valueResolution == mapperData.TargetInstance) + { + return null; + } + return mapperData.TargetInstance.AssignTo(valueResolution); } diff --git a/AgileMapper/ObjectPopulation/Enumerables/DefaultSourceEnumerableAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/DefaultSourceEnumerableAdapter.cs index 3cfd7192a..5060a5087 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/DefaultSourceEnumerableAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/DefaultSourceEnumerableAdapter.cs @@ -17,13 +17,15 @@ public Expression GetSourceCountAccess() return Expression.ArrayLength(SourceValue); } - var countPropertyInfo = + var countPropertyInfo = SourceValue.Type.GetPublicInstanceProperty("Count") ?? SourceTypeHelper.CollectionInterfaceType.GetPublicInstanceProperty("Count"); return Expression.Property(SourceValue, countPropertyInfo); } + public Expression GetMappingShortCircuitOrNull() => null; + public IPopulationLoopData GetPopulationLoopData() { if (SourceTypeHelper.HasListInterface) diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index 96661cb50..3c60413f1 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -1,11 +1,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { + using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using DataSources; - using Extensions; + using Extensions.Internal; using Members; + using Members.Dictionaries; using Members.Population; internal class DictionaryPopulationBuilder @@ -114,14 +116,51 @@ private Expression AssignDictionaryEntry( } var derivedSourceTypes = mappingData.MapperData.GetDerivedSourceTypes(); + var hasDerivedSourceTypes = derivedSourceTypes.Any(); - if (derivedSourceTypes.None()) + List typedVariables; + List mappingExpressions; + + if (hasDerivedSourceTypes) { - return GetPopulation(loopData, dictionaryEntryMember, mappingData); + typedVariables = new List(derivedSourceTypes.Count); + mappingExpressions = new List(typedVariables.Count * 2 + 2); + + AddDerivedSourceTypePopulations( + loopData, + dictionaryEntryMember, + mappingData, + derivedSourceTypes, + typedVariables, + mappingExpressions); + } + else + { + typedVariables = null; + mappingExpressions = new List(2); } - var typedVariables = new List(derivedSourceTypes.Count); - var mappingExpressions = new List(typedVariables.Count * 2 + 1); + InsertSourceElementNullCheck(loopData, mappingExpressions); + + mappingExpressions.Add(GetPopulation(loopData, dictionaryEntryMember, mappingData)); + + var mappingBlock = hasDerivedSourceTypes + ? Expression.Block(typedVariables, mappingExpressions) + : Expression.Block(mappingExpressions); + + return mappingBlock; + } + + private void AddDerivedSourceTypePopulations( + IPopulationLoopData loopData, + QualifiedMember dictionaryEntryMember, + IObjectMappingData mappingData, + IEnumerable derivedSourceTypes, + ICollection typedVariables, + ICollection mappingExpressions) + { + var sourceElement = loopData.GetSourceElementValue(); + var mapNextElement = Expression.Continue(loopData.ContinueLoopTarget); var orderedDerivedSourceTypes = derivedSourceTypes .OrderBy(t => t, TypeComparer.MostToLeastDerived); @@ -129,7 +168,6 @@ private Expression AssignDictionaryEntry( foreach (var derivedSourceType in orderedDerivedSourceTypes) { var derivedSourceCheck = new DerivedSourceTypeCheck(derivedSourceType); - var sourceElement = loopData.GetSourceElementValue(); var typedVariableAssignment = derivedSourceCheck.GetTypedVariableAssignment(sourceElement); typedVariables.Add(derivedSourceCheck.TypedVariable); @@ -137,18 +175,30 @@ private Expression AssignDictionaryEntry( var derivedTypeMapping = GetDerivedTypeMapping(derivedSourceCheck, mappingData); var derivedTypePopulation = GetPopulation(derivedTypeMapping, dictionaryEntryMember, mappingData); - var mapNextElement = Expression.Continue(loopData.ContinueLoopTarget); - var derivedMappingBlock = Expression.Block(derivedTypePopulation, mapNextElement); + var incrementCounter = _wrappedBuilder.GetCounterIncrement(); + var derivedMappingBlock = Expression.Block(derivedTypePopulation, incrementCounter, mapNextElement); var ifDerivedTypeReturn = Expression.IfThen(derivedSourceCheck.TypeCheck, derivedMappingBlock); mappingExpressions.Add(ifDerivedTypeReturn); } + } - mappingExpressions.Add(GetPopulation(loopData, dictionaryEntryMember, mappingData)); + private static void InsertSourceElementNullCheck(IPopulationLoopData loopData, IList mappingExpressions) + { + var sourceElement = loopData.GetSourceElementValue(); + + if (sourceElement.Type.CannotBeNull()) + { + return; + } - var mappingBlock = Expression.Block(typedVariables, mappingExpressions); + loopData.NeedsContinueTarget = true; - return mappingBlock; + var sourceElementIsNull = sourceElement.GetIsDefaultComparison(); + var continueLoop = Expression.Continue(loopData.ContinueLoopTarget); + var ifNullContinue = Expression.IfThen(sourceElementIsNull, continueLoop); + + mappingExpressions.Insert(0, ifNullContinue); } private Expression GetPopulation( diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryToDictionaryPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryToDictionaryPopulationLoopData.cs index 3e0a71c4c..e33314747 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryToDictionaryPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryToDictionaryPopulationLoopData.cs @@ -2,7 +2,7 @@ { using System.Collections.Generic; using System.Linq.Expressions; - using Members; + using Members.Dictionaries; internal class DictionaryToDictionaryPopulationLoopData : EnumerableSourcePopulationLoopData { diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceElementsDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs similarity index 94% rename from AgileMapper/ObjectPopulation/Enumerables/SourceElementsDictionaryAdapter.cs rename to AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs index f9766a31a..54cc79416 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceElementsDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs @@ -1,11 +1,11 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using DataSources; - using Extensions; - using Members; + using Extensions.Internal; + using Members.Dictionaries; using NetStandardPolyfills; internal class SourceElementsDictionaryAdapter : SourceEnumerableAdapterBase, ISourceEnumerableAdapter @@ -84,6 +84,8 @@ public Expression GetSourceCountAccess() public override bool UseReadOnlyTargetWrapper => base.UseReadOnlyTargetWrapper && Builder.Context.ElementTypesAreSimple; + public Expression GetMappingShortCircuitOrNull() => null; + public IPopulationLoopData GetPopulationLoopData() { if (Builder.ElementTypesAreSimple) diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceElementsDictionaryPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs similarity index 95% rename from AgileMapper/ObjectPopulation/Enumerables/SourceElementsDictionaryPopulationLoopData.cs rename to AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs index 5d20f769b..45ad8a862 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceElementsDictionaryPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryPopulationLoopData.cs @@ -1,11 +1,10 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { using System; using System.Collections.Generic; using System.Linq.Expressions; using DataSources; - using Extensions; - using Members; + using Extensions.Internal; using NetStandardPolyfills; internal class SourceElementsDictionaryPopulationLoopData : IPopulationLoopData @@ -18,13 +17,6 @@ internal class SourceElementsDictionaryPopulationLoopData : IPopulationLoopData private readonly Expression _sourceElement; private ParameterExpression _elementKeyExists; - public SourceElementsDictionaryPopulationLoopData( - DictionarySourceMember sourceMember, - EnumerablePopulationBuilder builder) - : this(new DictionaryEntryVariablePair(sourceMember, builder.MapperData), builder) - { - } - public SourceElementsDictionaryPopulationLoopData( DictionaryEntryVariablePair dictionaryVariables, EnumerablePopulationBuilder builder) diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs similarity index 92% rename from AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs rename to AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs index 5a3fb03d3..1f757e853 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs @@ -1,9 +1,9 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { using System.Linq.Expressions; using DataSources; - using Extensions; - using Members; + using Extensions.Internal; + using Members.Dictionaries; internal class SourceInstanceDictionaryAdapter : SourceEnumerableAdapterBase, ISourceEnumerableAdapter { @@ -55,6 +55,8 @@ private Expression GetNullValueEntryShortCircuit(Expression shortCircuitReturn) public Expression GetSourceCountAccess() => _defaultAdapter.GetSourceCountAccess(); + public Expression GetMappingShortCircuitOrNull() => null; + public IPopulationLoopData GetPopulationLoopData() => _defaultAdapter.GetPopulationLoopData(); } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs similarity index 63% rename from AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs rename to AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs index 4814f3e50..88e425207 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs @@ -1,10 +1,10 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { using System.Collections; using System.Linq; using System.Linq.Expressions; - using Extensions; - using Members; + using Extensions.Internal; + using Members.Dictionaries; using NetStandardPolyfills; internal class SourceObjectDictionaryAdapter : SourceEnumerableAdapterBase, ISourceEnumerableAdapter @@ -35,27 +35,17 @@ public override Expression GetSourceValues() var enumerableIsNull = untypedEnumerableVariable.GetIsDefaultComparison(); var ifNotEnumerableReturnEmpty = Expression.IfThen(enumerableIsNull, returnEmpty); - var typedEnumerableAssignment = - GetTypedEnumerableAssignment(untypedEnumerableVariable, out var typedEnumerableVariable); - - var enumerableIsTyped = typedEnumerableVariable.GetIsNotDefaultComparison(); - var returnTypedEnumerable = Expression.Return(returnLabel, typedEnumerableVariable); - var ifTypedEnumerableReturn = Expression.IfThen(enumerableIsTyped, returnTypedEnumerable); - var returnProjectionResult = GetEntryValueProjection(untypedEnumerableVariable, returnLabel); var sourceValueBlock = Expression.Block( new[] { _instanceDictionaryAdapter.DictionaryVariables.Key, - untypedEnumerableVariable, - typedEnumerableVariable + untypedEnumerableVariable }, ifKeyNotFoundShortCircuit, enumerableAssignment, ifNotEnumerableReturnEmpty, - typedEnumerableAssignment, - ifTypedEnumerableReturn, returnProjectionResult); return sourceValueBlock; @@ -71,30 +61,26 @@ private Expression GetUntypedEnumerableAssignment(out ParameterExpression enumer return enumerableAssignment; } - private Expression GetTypedEnumerableAssignment( - Expression untypedEnumerableVariable, - out ParameterExpression typedEnumerableVariable) - { - var targetEnumerableType = _emptyTarget.Type; - var enumerableAsTyped = Expression.TypeAs(untypedEnumerableVariable, targetEnumerableType); - var typedEnumerableVariableName = targetEnumerableType.GetVariableNameInCamelCase(); - typedEnumerableVariable = Expression.Variable(targetEnumerableType, typedEnumerableVariableName); - var typedEnumerableAssignment = typedEnumerableVariable.AssignTo(enumerableAsTyped); - - return typedEnumerableAssignment; - } - private Expression GetEntryValueProjection( Expression untypedEnumerableVariable, LabelTarget returnLabel) { - var linqCastMethod = typeof(Enumerable).GetPublicStaticMethod("Cast"); - var typedCastMethod = linqCastMethod.MakeGenericMethod(typeof(object)); - var linqCastCall = Expression.Call(null, typedCastMethod, untypedEnumerableVariable); - - var sourceItemsProjection = Builder.Context.ElementTypesAreSimple - ? Builder.GetSourceItemsProjection(linqCastCall, GetSourceElementConversion) - : Builder.GetSourceItemsProjection(linqCastCall, GetSourceElementMapping); + Expression sourceItemsProjection; + + if (Builder.Context.ElementTypesAreSimple) + { + var linqCastMethod = typeof(Enumerable).GetPublicStaticMethod("Cast"); + var typedCastMethod = linqCastMethod.MakeGenericMethod(typeof(object)); + var linqCastCall = Expression.Call(null, typedCastMethod, untypedEnumerableVariable); + sourceItemsProjection = Builder.GetSourceItemsProjection(linqCastCall, GetSourceElementConversion); + } + else + { + sourceItemsProjection = Builder.MapperData.Parent.GetMapCall( + untypedEnumerableVariable, + Builder.MapperData.TargetMember, + dataSourceIndex: 0); + } var returnProjectionResult = Expression.Label(returnLabel, sourceItemsProjection); @@ -104,14 +90,30 @@ private Expression GetEntryValueProjection( private Expression GetSourceElementConversion(Expression sourceParameter) => Builder.GetSimpleElementConversion(sourceParameter); - private Expression GetSourceElementMapping(Expression sourceParameter, Expression counter) - => Builder.MapperData.GetMapCall(sourceParameter); - public Expression GetSourceCountAccess() => _instanceDictionaryAdapter.GetSourceCountAccess(); public override bool UseReadOnlyTargetWrapper => base.UseReadOnlyTargetWrapper && Builder.Context.ElementTypesAreSimple; + public Expression GetMappingShortCircuitOrNull() + { + if (Builder.ElementTypesAreSimple) + { + return null; + } + + var sourceEnumerableFoundTest = SourceObjectDictionaryPopulationLoopData + .GetSourceEnumerableFoundTest(_emptyTarget, Builder); + + var projectionAsTargetType = Expression.TypeAs(Builder.SourceValue, Builder.MapperData.TargetType); + var convertedProjection = TargetTypeHelper.GetEnumerableConversion(Builder.SourceValue); + var projectionResult = Expression.Coalesce(projectionAsTargetType, convertedProjection); + var returnConvertedProjection = Expression.Return(Builder.MapperData.ReturnLabelTarget, projectionResult); + var ifProjectedReturn = Expression.IfThen(sourceEnumerableFoundTest, returnConvertedProjection); + + return ifProjectedReturn; + } + public IPopulationLoopData GetPopulationLoopData() { return new SourceObjectDictionaryPopulationLoopData( diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs similarity index 80% rename from AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryPopulationLoopData.cs rename to AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs index 7354e97b4..9cde53b83 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs @@ -1,8 +1,8 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { using System.Linq.Expressions; using DataSources; - using Extensions; + using Extensions.Internal; internal class SourceObjectDictionaryPopulationLoopData : IPopulationLoopData { @@ -22,7 +22,7 @@ public SourceObjectDictionaryPopulationLoopData( _enumerableLoopData = new EnumerableSourcePopulationLoopData(builder); _elementsDictionaryLoopData = new SourceElementsDictionaryPopulationLoopData(dictionaryVariables, builder); - _sourceEnumerableFound = Expression.Variable(typeof(bool), "sourceEnumerableFound"); + _sourceEnumerableFound = Parameters.Create("sourceEnumerableFound"); ContinueLoopTarget = Expression.Label(typeof(void), "Continue"); LoopExitCheck = GetCompositeLoopExitCheck(); @@ -50,15 +50,15 @@ private Expression GetCompositeLoopExitCheck() public Expression GetElementMapping(IObjectMappingData enumerableMappingData) { - var convertedEnumeratorValue = _enumerableLoopData.GetElementMapping(enumerableMappingData); - var convertedElementValue = _elementsDictionaryLoopData.GetElementMapping(enumerableMappingData); - - return Expression.Condition(_sourceEnumerableFound, convertedEnumeratorValue, convertedElementValue); + return Expression.Condition( + _sourceEnumerableFound, + _enumerableLoopData.SourceElement, + _elementsDictionaryLoopData.GetElementMapping(enumerableMappingData)); } public Expression Adapt(LoopExpression loop) { - var sourceEnumerableFoundTest = Expression.NotEqual(_builder.SourceValue, _emptyTarget); + var sourceEnumerableFoundTest = GetSourceEnumerableFoundTest(_emptyTarget, _builder); var assignSourceEnumerableFound = (Expression)_sourceEnumerableFound.AssignTo(sourceEnumerableFoundTest); var adaptedLoop = _elementsDictionaryLoopData.Adapt(loop); @@ -73,6 +73,13 @@ public Expression Adapt(LoopExpression loop) new[] { assignSourceEnumerableFound }.Append(enumerableLoopBlock.Expressions)); } + public static BinaryExpression GetSourceEnumerableFoundTest( + Expression emptyTarget, + EnumerablePopulationBuilder builder) + { + return Expression.NotEqual(builder.SourceValue, emptyTarget); + } + private Expression GetEnumeratorIfNecessary(Expression getEnumeratorCall) { return Expression.Condition( diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index e9150ec14..423f3791a 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -6,7 +6,7 @@ using System.Linq.Expressions; using System.Reflection; using Caching; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; @@ -20,12 +20,6 @@ internal class EnumerablePopulationBuilder (m.GetParameters().Length == 2) && (m.GetParameters()[1].ParameterType.GetGenericTypeArguments().Length == 2)); - private static readonly MethodInfo _selectWithIndexMethod = typeof(Enumerable) - .GetPublicStaticMethods("Select") - .Last(m => - (m.GetParameters().Length == 2) && - (m.GetParameters()[1].ParameterType.GetGenericTypeArguments().Length == 3)); - private static readonly MethodInfo _forEachMethod = typeof(EnumerableExtensions) .GetPublicStaticMethods("ForEach") .First(); @@ -84,6 +78,8 @@ public static implicit operator BlockExpression(EnumerablePopulationBuilder buil #endregion + public Expression GetCounterIncrement() => Expression.PreIncrementAssign(Counter); + public ParameterExpression Counter => _counterVariable ?? (_counterVariable = GetCounterVariable()); private ParameterExpression GetCounterVariable() @@ -235,6 +231,10 @@ public void AssignSourceVariableFromSourceObject() } AssignSourceVariableFrom(SourceValue); + + var shortCircuit = _sourceAdapter.GetMappingShortCircuitOrNull(); + + _populationExpressions.AddUnlessNullOrEmpty(shortCircuit); } public void AssignSourceVariableFrom(Func sourceItemsSelection) @@ -541,18 +541,6 @@ public Expression GetSourceItemsProjection( _sourceElementParameter); } - public Expression GetSourceItemsProjection( - Expression sourceEnumerableValue, - Func projectionFuncFactory) - { - return GetSourceItemsProjection( - sourceEnumerableValue, - _selectWithIndexMethod, - projectionFuncFactory, - _sourceElementParameter, - Counter); - } - private Expression GetSourceItemsProjection( Expression sourceEnumerableValue, MethodInfo linqSelectOverload, diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs index 6d979e852..50c98adf2 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationContext.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs index 3ca5e3f33..138f719f6 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs @@ -5,7 +5,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class EnumerableSourcePopulationLoopData : IPopulationLoopData diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index d10b51a8f..ceac09f66 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -4,7 +4,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class EnumerableTypeHelper @@ -48,8 +48,7 @@ public bool IsDeclaredReadOnly private bool IsReadOnlyCollectionInterface() { #if NET_STANDARD - return EnumerableType.IsGenericType() && - (EnumerableType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>)); + return EnumerableType.IsClosedTypeOf(typeof(IReadOnlyCollection<>)); #else return EnumerableType.IsInterface() && (EnumerableType.Name == "IReadOnlyCollection`1") && @@ -109,6 +108,11 @@ public Expression GetEnumerableConversion(Expression instance) return instance.WithToReadOnlyCollectionCall(ElementType); } + if (IsCollection) + { + return instance.WithToCollectionCall(ElementType); + } + return instance.WithToListCall(ElementType); } } diff --git a/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs index 53e5a67e8..c3849ab69 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs @@ -1,8 +1,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables { - using System; using System.Linq.Expressions; - using Extensions; internal interface IPopulationLoopData { @@ -18,38 +16,4 @@ internal interface IPopulationLoopData Expression Adapt(LoopExpression loop); } - - internal static class PopulationLoopDataExtensions - { - public static Expression BuildPopulationLoop( - this TLoopData loopData, - EnumerablePopulationBuilder builder, - IObjectMappingData mappingData, - Func elementPopulationFactory) - where TLoopData : IPopulationLoopData - { - // TODO: Not all enumerable mappings require the Counter - var breakLoop = Expression.Break(Expression.Label(typeof(void), "Break")); - - var elementPopulation = elementPopulationFactory.Invoke(loopData, mappingData); - - var loopBody = Expression.Block( - Expression.IfThen(loopData.LoopExitCheck, breakLoop), - elementPopulation, - Expression.PreIncrementAssign(builder.Counter)); - - var populationLoop = loopData.NeedsContinueTarget - ? Expression.Loop(loopBody, breakLoop.Target, loopData.ContinueLoopTarget) - : Expression.Loop(loopBody, breakLoop.Target); - - var adaptedLoop = loopData.Adapt(populationLoop); - - var population = Expression.Block( - new[] { builder.Counter }, - builder.Counter.AssignTo(0.ToConstantExpression()), - adaptedLoop); - - return population; - } - } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/ISourceEnumerableAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/ISourceEnumerableAdapter.cs index 23347ce7d..3c0ac8277 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/ISourceEnumerableAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/ISourceEnumerableAdapter.cs @@ -10,6 +10,8 @@ internal interface ISourceEnumerableAdapter bool UseReadOnlyTargetWrapper { get; } + Expression GetMappingShortCircuitOrNull(); + IPopulationLoopData GetPopulationLoopData(); } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/IndexedSourcePopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/IndexedSourcePopulationLoopData.cs index 607a9e096..c3bfb842f 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/IndexedSourcePopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/IndexedSourcePopulationLoopData.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables { using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; internal class IndexedSourcePopulationLoopData : IPopulationLoopData { diff --git a/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs b/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs new file mode 100644 index 000000000..091fba010 --- /dev/null +++ b/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs @@ -0,0 +1,40 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +{ + using System; + using System.Linq.Expressions; + using Extensions.Internal; + + internal static class PopulationLoopDataExtensions + { + public static Expression BuildPopulationLoop( + this TLoopData loopData, + EnumerablePopulationBuilder builder, + IObjectMappingData mappingData, + Func elementPopulationFactory) + where TLoopData : IPopulationLoopData + { + // TODO: Not all enumerable mappings require the Counter + var breakLoop = Expression.Break(Expression.Label(typeof(void), "Break")); + + var elementPopulation = elementPopulationFactory.Invoke(loopData, mappingData); + + var loopBody = Expression.Block( + Expression.IfThen(loopData.LoopExitCheck, breakLoop), + elementPopulation, + builder.GetCounterIncrement()); + + var populationLoop = loopData.NeedsContinueTarget + ? Expression.Loop(loopBody, breakLoop.Target, loopData.ContinueLoopTarget) + : Expression.Loop(loopBody, breakLoop.Target); + + var adaptedLoop = loopData.Adapt(populationLoop); + + var population = Expression.Block( + new[] { builder.Counter }, + builder.Counter.AssignTo(0.ToConstantExpression()), + adaptedLoop); + + return population; + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs b/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs index c189317af..22a2a72eb 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs @@ -4,7 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; - using Extensions; + using Extensions.Internal; /// /// Wraps a readonly collection to enable efficient creation of a new array. This object @@ -92,14 +92,20 @@ public ReadOnlyCollectionWrapper(IList existingItems, int numberOfNewItems) /// /// The zero-based index of the element to get or set. /// The element at the specified index. - #region ExcludeFromCodeCoverage -#if DEBUG - [ExcludeFromCodeCoverage] -#endif - #endregion public T this[int index] { + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion get => _items[index]; + + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion set => _items[index] = value; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterFactory.cs b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterFactory.cs index 41079a74a..558adf1f5 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterFactory.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterFactory.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables { + using Dictionaries; using Members; internal static class SourceEnumerableAdapterFactory diff --git a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs index b3a675ec7..f8dd5a345 100644 --- a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs +++ b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs @@ -2,8 +2,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Linq.Expressions; using DataSources; - using Extensions; + using Extensions.Internal; using Members; + using Members.Dictionaries; internal class ExistingOrDefaultValueDataSourceFactory : IDataSourceFactory { diff --git a/AgileMapper/ObjectPopulation/MapperDataContext.cs b/AgileMapper/ObjectPopulation/MapperDataContext.cs index 146101e04..899b6780d 100644 --- a/AgileMapper/ObjectPopulation/MapperDataContext.cs +++ b/AgileMapper/ObjectPopulation/MapperDataContext.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Linq; - using Extensions; + using Extensions.Internal; using Members; internal class MapperDataContext diff --git a/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs b/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs index bc879e596..018a13d1d 100644 --- a/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs @@ -25,7 +25,7 @@ public virtual bool AppliesTo(CallbackPosition callbackPosition, IBasicMapperDat => (CallbackPosition == callbackPosition) && base.AppliesTo(mapperData); protected override bool MemberPathMatches(IBasicMapperData mapperData) - => ConfigInfo.HasCompatibleTypes(mapperData); + => mapperData.HasCompatibleTypes(ConfigInfo); public Expression Create(IMemberMapperData mapperData) { diff --git a/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs b/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs index 51ab63986..f14ebd795 100644 --- a/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal static class MappingDataCreationFactory diff --git a/AgileMapper/ObjectPopulation/MappingDataFactory.cs b/AgileMapper/ObjectPopulation/MappingDataFactory.cs index ecd7840d7..b75d845e8 100644 --- a/AgileMapper/ObjectPopulation/MappingDataFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingDataFactory.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Reflection; using Enumerables; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 42ab0db78..b30a1ce36 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -4,13 +4,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; #if NET_STANDARD - using System.Reflection; #endif using Members; using NetStandardPolyfills; using static CallbackPosition; + using static System.Linq.Expressions.ExpressionType; internal abstract class MappingExpressionFactoryBase { @@ -71,7 +71,7 @@ private bool MappingAlwaysBranchesToDerivedType(IObjectMappingData mappingData, { derivedTypeMappings = GetDerivedTypeMappings(mappingData); - if (derivedTypeMappings.NodeType != ExpressionType.Goto) + if (derivedTypeMappings.NodeType != Goto) { return false; } @@ -102,21 +102,21 @@ private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperDa protected abstract IEnumerable GetObjectPopulation(IObjectMappingData mappingData); - private static bool NothingIsBeingMapped(IList mappingExpressions, ObjectMapperData mapperData) + private static bool NothingIsBeingMapped(IList mappingExpressions, IMemberMapperData mapperData) { if (mappingExpressions.None()) { return true; } - if (mappingExpressions[0].NodeType != ExpressionType.Assign) + if (mappingExpressions[0].NodeType != Assign) { return false; } var assignedValue = ((BinaryExpression)mappingExpressions[0]).Right; - if (assignedValue.NodeType == ExpressionType.Default) + if (assignedValue.NodeType == Default) { return true; } @@ -133,9 +133,14 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping AdjustForSingleExpressionBlockIfApplicable(ref mappingExpressions); - if (mappingExpressions[0].NodeType != ExpressionType.Block) + if (mappingExpressions.HasOne() && (mappingExpressions[0].NodeType == Constant)) { - if (mappingExpressions[0].NodeType == ExpressionType.MemberAccess) + goto CreateFullMappingBlock; + } + + if (mappingExpressions[0].NodeType != Block) + { + if (mappingExpressions[0].NodeType == MemberAccess) { return GetReturnExpression(mappingExpressions[0], mappingExtras); } @@ -145,12 +150,12 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping goto CreateFullMappingBlock; } - var localVariableAssignment = mappingExpressions.First(exp => exp.NodeType == ExpressionType.Assign); + var firstAssignment = (BinaryExpression)mappingExpressions.First(exp => exp.NodeType == Assign); - if (mappingExpressions.Last() == localVariableAssignment) + if ((firstAssignment.Left.NodeType == Parameter) && + (mappingExpressions.Last() == firstAssignment)) { - var assignedValue = ((BinaryExpression)localVariableAssignment).Right; - returnExpression = GetReturnExpression(assignedValue, mappingExtras); + returnExpression = GetReturnExpression(firstAssignment.Right, mappingExtras); if (mappingExpressions.HasOne()) { @@ -178,7 +183,7 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping private static void AdjustForSingleExpressionBlockIfApplicable(ref IList mappingExpressions) { - if (!mappingExpressions.HasOne() || (mappingExpressions[0].NodeType != ExpressionType.Block)) + if (!mappingExpressions.HasOne() || (mappingExpressions[0].NodeType != Block)) { return; } diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index de4839b32..9353e5726 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal static class MappingFactory @@ -54,7 +54,12 @@ public static Expression GetChildMapping( if (childMapperData.TargetMemberEverRecurses()) { - childMapperData.CacheMappedObjects = childMapperData.SourceIsNotFlatObject(); + if (childMapperData.SourceIsFlatObject()) + { + return Constants.EmptyExpression; + } + + childMapperData.CacheMappedObjects = true; var mapRecursionCall = GetMapRecursionCallFor( childMappingData, diff --git a/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs b/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs index 51e2fe979..2e5a37eec 100644 --- a/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs @@ -3,7 +3,7 @@ using System; using System.Linq.Expressions; using Configuration; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index 63a8f09b9..e31261834 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -7,10 +7,11 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Reflection; using DataSources; using Enumerables; - using Extensions; + using Extensions.Internal; using Members; using Members.Sources; using NetStandardPolyfills; + using static Members.Member; internal class ObjectMapperData : BasicMapperData, IMemberMapperData { @@ -50,9 +51,9 @@ private ObjectMapperData( SourceMember = sourceMember; var mappingDataType = typeof(IMappingData<,>).MakeGenericType(SourceType, TargetType); - SourceObject = GetMappingDataProperty(mappingDataType, "Source"); - TargetObject = Expression.Property(MappingDataObject, "Target"); - CreatedObject = Expression.Property(MappingDataObject, "CreatedObject"); + SourceObject = GetMappingDataProperty(mappingDataType, RootSourceMemberName); + TargetObject = GetMappingDataProperty(RootTargetMemberName); + CreatedObject = GetMappingDataProperty("CreatedObject"); var isPartOfDerivedTypeMapping = declaredTypeMapperData != null; @@ -64,7 +65,7 @@ private ObjectMapperData( else { EnumerableIndex = GetMappingDataProperty(mappingDataType, "EnumerableIndex"); - ParentObject = Expression.Property(MappingDataObject, "Parent"); + ParentObject = GetMappingDataProperty("Parent"); } ExpressionInfoFinder = new ExpressionInfoFinder(MappingDataObject); @@ -145,12 +146,12 @@ private Expression GetMappingDataProperty(Type mappingDataType, string propertyN { var property = mappingDataType.GetPublicInstanceProperty(propertyName); - // ReSharper disable once AssignNullToNotNullAttribute - var propertyAccess = Expression.Property(MappingDataObject, property); - - return propertyAccess; + return Expression.Property(MappingDataObject, property); } + private Expression GetMappingDataProperty(string propertyName) + => Expression.Property(MappingDataObject, propertyName); + private static MethodInfo GetMapMethod(Type mappingDataType, int numberOfArguments) { return mappingDataType @@ -282,7 +283,7 @@ private static bool TypeHasACompatibleChildMember( #region Factory Method public static ObjectMapperData For(IObjectMappingData mappingData) - { + { var membersSource = mappingData.MapperKey.GetMembersSource(mappingData.Parent); var sourceMember = membersSource.GetSourceMember().WithType(typeof(TSource)); var targetMember = membersSource.GetTargetMember().WithType(typeof(TTarget)); @@ -327,6 +328,14 @@ public static ObjectMapperData For(IObjectMappingData mappingD public MapperDataContext Context { get; } + public override bool HasCompatibleTypes(ITypePair typePair) + { + return typePair.HasCompatibleTypes( + this, + () => SourceMember.HasCompatibleType(typePair.SourceType), + () => TargetMember.HasCompatibleType(typePair.TargetType)); + } + public IQualifiedMember GetSourceMemberFor(string targetMemberRegistrationName, int dataSourceIndex) => _dataSourcesByTargetMemberName[targetMemberRegistrationName][dataSourceIndex].SourceMember; diff --git a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs index 42e9e6413..6fe927f28 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs @@ -6,7 +6,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Caching; using ComplexTypes; using Enumerables; - using Extensions; + using Extensions.Internal; using Validation; internal class ObjectMapperFactory diff --git a/AgileMapper/ObjectPopulation/ObjectMappingData.cs b/AgileMapper/ObjectPopulation/ObjectMappingData.cs index 85eb6afe4..97c52c8a6 100644 --- a/AgileMapper/ObjectPopulation/ObjectMappingData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMappingData.cs @@ -98,19 +98,8 @@ public ObjectMapperData MapperData private Dictionary> MappedObjectsBySource => _mappedObjectsBySource ?? (_mappedObjectsBySource = new Dictionary>(13)); - private ChildMemberMappingData _childMappingData; - IChildMemberMappingData IObjectMappingData.GetChildMappingData(IMemberMapperData childMapperData) - { - if (_childMappingData == null) - { - _childMappingData = new ChildMemberMappingData(this); - } - - _childMappingData.MapperData = childMapperData; - - return _childMappingData; - } + => new ChildMemberMappingData(this, childMapperData); #endregion diff --git a/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs b/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs index b272db0bd..ebae431b7 100644 --- a/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs @@ -1,10 +1,11 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System; + using System.Dynamic; using System.Linq; using System.Linq.Expressions; using Enumerables; - using Extensions; + using Extensions.Internal; using Members; using Members.Sources; using NetStandardPolyfills; @@ -32,7 +33,27 @@ public static IObjectMappingData ForRoot( TTarget target, IMappingContext mappingContext) { - var mapperKey = new RootObjectMapperKey(MappingTypes.For(source, target), mappingContext); + RootObjectMapperKey mapperKey; + + if ((target == null) && (typeof(TTarget) == typeof(object))) + { + // This is a 'create new' mapping where the target type has come + // through as 'object'. This happens when you use .ToANew(), + // and I can't see how to differentiate that from .ToANew(). + // Given that the former is more likely and that people asking for + // .ToANew() are doing something weird, default the target + // type to ExpandoObject: + mapperKey = new RootObjectMapperKey(MappingTypes.For(source, default(ExpandoObject)), mappingContext); + + return Create( + source, + default(ExpandoObject), + null, + mapperKey, + mappingContext); + } + + mapperKey = new RootObjectMapperKey(MappingTypes.For(source, target), mappingContext); return Create( source, diff --git a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs index 250e7f6a0..15c9d5e44 100644 --- a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Collections.Generic; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal class SimpleTypeMappingExpressionFactory : MappingExpressionFactoryBase diff --git a/AgileMapper/ObjectPopulation/SourceAccessFinder.cs b/AgileMapper/ObjectPopulation/SourceAccessFinder.cs index b3ebc6056..2a7454bcf 100644 --- a/AgileMapper/ObjectPopulation/SourceAccessFinder.cs +++ b/AgileMapper/ObjectPopulation/SourceAccessFinder.cs @@ -25,7 +25,7 @@ public static bool MultipleAccessesExist(IMemberMapperData mapperData, Expressio protected override Expression VisitMember(MemberExpression memberAccess) { if ((memberAccess.Expression == _mappingDataObject) && - (memberAccess.Member.Name == "Source")) + (memberAccess.Member.Name == Member.RootSourceMemberName)) { ++_numberOfAccesses; } diff --git a/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs b/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs index a268f0821..f6aa4588f 100644 --- a/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs +++ b/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; internal abstract class SourceMemberTypeDependentKeyBase diff --git a/AgileMapper/Parameters.cs b/AgileMapper/Parameters.cs index 7ec2a598f..2f2f4856b 100644 --- a/AgileMapper/Parameters.cs +++ b/AgileMapper/Parameters.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using Members; using ObjectPopulation; diff --git a/AgileMapper/TypeConversion/ConverterSet.cs b/AgileMapper/TypeConversion/ConverterSet.cs index 5861d433f..bc27636dd 100644 --- a/AgileMapper/TypeConversion/ConverterSet.cs +++ b/AgileMapper/TypeConversion/ConverterSet.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Linq.Expressions; using Configuration; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; using ReadableExpressions.Extensions; @@ -82,7 +82,7 @@ public Expression GetConversion(Expression sourceValue, Type targetType) if (sourceValue.Type.IsAssignableTo(targetType)) { - return targetType.IsNullableType() || (sourceValue.Type.IsSimple() && (targetType == typeof(object))) + return ConvertSourceValueToTargetType(sourceValue, targetType) ? sourceValue.GetConversionTo(targetType) : sourceValue; } @@ -97,6 +97,21 @@ public Expression GetConversion(Expression sourceValue, Type targetType) return conversion; } + private static bool ConvertSourceValueToTargetType(Expression sourceValue, Type targetType) + { + if (targetType.IsNullableType()) + { + return true; + } + + if (!sourceValue.Type.IsValueType() && !sourceValue.Type.IsSimple()) + { + return false; + } + + return (targetType == typeof(object)) || (targetType == typeof(ValueType)); + } + public void CloneTo(ConverterSet converterSet) { if (_converters.Count == converterSet._converters.Count) diff --git a/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs b/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs index 73f9097a7..f5c9ce18e 100644 --- a/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs +++ b/AgileMapper/TypeConversion/FallbackNonSimpleTypeValueConverter.cs @@ -2,7 +2,7 @@ namespace AgileObjects.AgileMapper.TypeConversion { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; internal class FallbackNonSimpleTypeValueConverter : ValueConverterBase { diff --git a/AgileMapper/TypeConversion/NumericConversions.cs b/AgileMapper/TypeConversion/NumericConversions.cs index b25c59e08..3024674ca 100644 --- a/AgileMapper/TypeConversion/NumericConversions.cs +++ b/AgileMapper/TypeConversion/NumericConversions.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; internal static class NumericConversions { diff --git a/AgileMapper/TypeConversion/NumericValueIsInRangeComparison.cs b/AgileMapper/TypeConversion/NumericValueIsInRangeComparison.cs index 9f23ac437..f69a52b70 100644 --- a/AgileMapper/TypeConversion/NumericValueIsInRangeComparison.cs +++ b/AgileMapper/TypeConversion/NumericValueIsInRangeComparison.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; internal static class NumericValueIsInRangeComparison { diff --git a/AgileMapper/TypeConversion/ToBoolConverter.cs b/AgileMapper/TypeConversion/ToBoolConverter.cs index 41bf75fd9..7abee406b 100644 --- a/AgileMapper/TypeConversion/ToBoolConverter.cs +++ b/AgileMapper/TypeConversion/ToBoolConverter.cs @@ -5,7 +5,7 @@ namespace AgileObjects.AgileMapper.TypeConversion using System.Globalization; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using ReadableExpressions.Extensions; internal class ToBoolConverter : ValueConverterBase diff --git a/AgileMapper/TypeConversion/ToCharacterConverter.cs b/AgileMapper/TypeConversion/ToCharacterConverter.cs index 0670679dc..cb96d3ebc 100644 --- a/AgileMapper/TypeConversion/ToCharacterConverter.cs +++ b/AgileMapper/TypeConversion/ToCharacterConverter.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class ToCharacterConverter : ValueConverterBase @@ -76,7 +76,7 @@ private Expression GetFromNumericConversion(Expression sourceValue, Type targetT if (!isWholeNumberNumeric) { - stringValue = stringValue.GetLeftCall(numberOfCharacters: 1); + stringValue = stringValue.GetFirstOrDefaultCall(); } var convertedStringValue = GetFromStringConversion(stringValue, targetType); diff --git a/AgileMapper/TypeConversion/ToEnumConverter.cs b/AgileMapper/TypeConversion/ToEnumConverter.cs index cf8776a32..2976fc397 100644 --- a/AgileMapper/TypeConversion/ToEnumConverter.cs +++ b/AgileMapper/TypeConversion/ToEnumConverter.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class ToEnumConverter : ValueConverterBase diff --git a/AgileMapper/TypeConversion/ToFormattedStringConverter.cs b/AgileMapper/TypeConversion/ToFormattedStringConverter.cs index cccec138b..8de7c361c 100644 --- a/AgileMapper/TypeConversion/ToFormattedStringConverter.cs +++ b/AgileMapper/TypeConversion/ToFormattedStringConverter.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; using System.Reflection; using Configuration; - using Extensions; + using Extensions.Internal; using ReadableExpressions.Extensions; internal class ToFormattedStringConverter : ValueConverterBase diff --git a/AgileMapper/TypeConversion/ToNumericConverter.cs b/AgileMapper/TypeConversion/ToNumericConverter.cs index c104dc426..536fa9873 100644 --- a/AgileMapper/TypeConversion/ToNumericConverter.cs +++ b/AgileMapper/TypeConversion/ToNumericConverter.cs @@ -2,7 +2,7 @@ { using System; using System.Linq; - using Extensions; + using Extensions.Internal; internal class ToNumericConverter : ToNumericConverterBase { diff --git a/AgileMapper/TypeConversion/ToNumericConverterBase.cs b/AgileMapper/TypeConversion/ToNumericConverterBase.cs index c38fb3161..b48eaab11 100644 --- a/AgileMapper/TypeConversion/ToNumericConverterBase.cs +++ b/AgileMapper/TypeConversion/ToNumericConverterBase.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using System.Linq.Expressions; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal abstract class ToNumericConverterBase : TryParseConverterBase diff --git a/AgileMapper/TypeConversion/ToStringConverter.cs b/AgileMapper/TypeConversion/ToStringConverter.cs index e76f1ebd7..70e8f7990 100644 --- a/AgileMapper/TypeConversion/ToStringConverter.cs +++ b/AgileMapper/TypeConversion/ToStringConverter.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal class ToStringConverter : ValueConverterBase diff --git a/AgileMapper/TypeConversion/TryParseConverterBase.cs b/AgileMapper/TypeConversion/TryParseConverterBase.cs index 99438da38..a72fb2a20 100644 --- a/AgileMapper/TypeConversion/TryParseConverterBase.cs +++ b/AgileMapper/TypeConversion/TryParseConverterBase.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.TypeConversion using System; using System.Linq.Expressions; using System.Reflection; - using Extensions; + using Extensions.Internal; using NetStandardPolyfills; internal abstract class TryParseConverterBase : ValueConverterBase diff --git a/AgileMapper/Validation/EnumMappingMismatchFinder.cs b/AgileMapper/Validation/EnumMappingMismatchFinder.cs index bc74264ea..7f766349b 100644 --- a/AgileMapper/Validation/EnumMappingMismatchFinder.cs +++ b/AgileMapper/Validation/EnumMappingMismatchFinder.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Linq.Expressions; using DataSources; - using Extensions; + using Extensions.Internal; using Members; using NetStandardPolyfills; using ObjectPopulation; diff --git a/AgileMapper/Validation/EnumMappingMismatchSet.cs b/AgileMapper/Validation/EnumMappingMismatchSet.cs index 04c3d2d59..94662e57d 100644 --- a/AgileMapper/Validation/EnumMappingMismatchSet.cs +++ b/AgileMapper/Validation/EnumMappingMismatchSet.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Linq.Expressions; using DataSources; - using Extensions; + using Extensions.Internal; using Members; using ReadableExpressions; diff --git a/AgileMapper/Validation/MappingValidationException.cs b/AgileMapper/Validation/MappingValidationException.cs index 6561c59bb..ec04030b1 100644 --- a/AgileMapper/Validation/MappingValidationException.cs +++ b/AgileMapper/Validation/MappingValidationException.cs @@ -12,6 +12,11 @@ public class MappingValidationException : Exception /// /// Initializes a new instance of the MappingValidationException class. /// + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion public MappingValidationException() : this("Mapping validation failed.") { @@ -33,6 +38,11 @@ public MappingValidationException(string message) /// The SerializationInfo containing serialization information. /// The StreamingContext in which the deserialization is being performed. // ReSharper disable UnusedParameter.Local + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion protected MappingValidationException(SerializationInfo info, StreamingContext context) { } diff --git a/AgileMapper/Validation/MappingValidator.cs b/AgileMapper/Validation/MappingValidator.cs index fe25a93f5..0000fd595 100644 --- a/AgileMapper/Validation/MappingValidator.cs +++ b/AgileMapper/Validation/MappingValidator.cs @@ -5,7 +5,7 @@ using System.Text; using Configuration; using DataSources; - using Extensions; + using Extensions.Internal; using Members; using ObjectPopulation;