diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index a553d6056..d3aaa8cff 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -90,6 +90,18 @@ + + + + + + + + + + + + @@ -125,10 +137,13 @@ + + + @@ -214,6 +229,7 @@ AgileMapper.UnitTests.MoreTestClasses + diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs index dac3a5dcc..415cb38ce 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs @@ -15,9 +15,7 @@ public void ShouldExecuteAGlobalPreMappingCallback() { var mappedNames = new List(); - mapper - .Before - .MappingBegins + mapper.Before.MappingBegins .Call((s, t) => mappedNames.AddRange(new[] { ((Person)s).Name, ((PersonViewModel)t).Name })); var source = new Person { Name = "Bernie" }; @@ -36,9 +34,7 @@ public void ShouldExecuteAGlobalPostMappingCallbackConditionally() { var mappedNames = new List(); - mapper - .After - .MappingEnds + mapper.After.MappingEnds .If((s, t) => t.GetType() != typeof(Address)) .Call(ctx => mappedNames.AddRange(new[] { ((PersonViewModel)ctx.Source).Name, ((Person)ctx.Target).Name })); diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreationCallbacks.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreationCallbacks.cs index 0f6f6fae8..d92993430 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreationCallbacks.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreationCallbacks.cs @@ -18,59 +18,62 @@ public void ShouldCallAGlobalObjectCreatedCallback() { using (var mapper = Mapper.CreateNew()) { - var createdInstance = default(PublicProperty); + var createdInstance = default(object); mapper.After .CreatingInstances - .Call(ctx => createdInstance = (PublicProperty)ctx.CreatedObject); + .Call(ctx => createdInstance = ctx.CreatedObject); var source = new PublicField(); var result = mapper.Map(source).ToANew>(); createdInstance.ShouldNotBeNull(); - createdInstance.ShouldBe(result); + createdInstance.ShouldBeOfType>(); + createdInstance.ShouldBeSameAs(result); } } [Fact] public void ShouldWrapAnObjectCreatedCallbackException() { - Should.Throw(() => + var createdEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { mapper.After .CreatingInstances - .Call(ctx => { throw new InvalidOperationException(); }); + .Call(ctx => throw new InvalidOperationException()); mapper.Map(new PublicProperty()).ToANew>(); } }); + + createdEx.ShouldNotBeNull(); + createdEx.Message.ShouldContain("mapping PublicProperty -> PublicField"); } [Fact] public void ShouldWrapANestedObjectCreatingCallbackException() { - var exception = Should.Throw(() => + var createdEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { - mapper - .After + mapper.After .CreatingInstancesOf
() - .Call(ctx => { throw new InvalidOperationException("OH NO"); }); + .Call(ctx => throw new InvalidOperationException("OH NO")); mapper.Map(new PersonViewModel { AddressLine1 = "My House" }).ToANew(); } }); - exception.InnerException.ShouldNotBeNull(); - exception.InnerException.ShouldBeOfType(); + createdEx.InnerException.ShouldNotBeNull(); + createdEx.InnerException.ShouldBeOfType(); // ReSharper disable once PossibleNullReferenceException - exception.InnerException.InnerException.ShouldNotBeNull(); - exception.InnerException.InnerException.ShouldBeOfType(); + createdEx.InnerException.InnerException.ShouldNotBeNull(); + createdEx.InnerException.InnerException.ShouldBeOfType(); // ReSharper disable once PossibleNullReferenceException - exception.InnerException.InnerException.Message.ShouldBe("OH NO"); + createdEx.InnerException.InnerException.Message.ShouldBe("OH NO"); } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs index a2f2fe34e..39e3a9ec1 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs @@ -149,7 +149,7 @@ public void ShouldErrorIfReadOnlySimpleTypeMemberSpecified() } }); - configurationEx.Message.ShouldContain("not writeable"); + configurationEx.Message.ShouldContain("not mappable"); } [Fact] diff --git a/AgileMapper.UnitTests/Members/WhenFindingTargetMembers.cs b/AgileMapper.UnitTests/Members/WhenFindingTargetMembers.cs index 1a5a09466..79d7c0336 100644 --- a/AgileMapper.UnitTests/Members/WhenFindingTargetMembers.cs +++ b/AgileMapper.UnitTests/Members/WhenFindingTargetMembers.cs @@ -70,41 +70,49 @@ public void ShouldFindAPublicReadOnlyComplexTypeProperty() } [Fact] - public void ShouldIgnoreANonPublicField() + public void ShouldFindAPublicReadOnlyArrayField() { var member = MemberFinder - .GetTargetMembers(typeof(InternalField>)) + .GetTargetMembers(typeof(PublicReadOnlyField)) .FirstOrDefault(m => m.Name == "Value"); - member.ShouldBeNull(); + member.ShouldNotBeNull(); + member.Type.ShouldBe(typeof(byte[])); + member.ElementType.ShouldBe(typeof(byte)); + member.IsWriteable.ShouldBeFalse(); } [Fact] - public void ShouldIgnoreAPublicReadOnlyArrayField() + public void ShouldFindAPublicReadOnlySimpleTypeProperty() { var member = MemberFinder - .GetTargetMembers(typeof(PublicReadOnlyField)) + .GetTargetMembers(typeof(PublicReadOnlyProperty)) .FirstOrDefault(m => m.Name == "Value"); - member.ShouldBeNull(); + member.ShouldNotBeNull(); + member.Type.ShouldBe(typeof(long)); + member.IsWriteable.ShouldBeFalse(); } [Fact] - public void ShouldIgnoreAPublicReadOnlySimpleTypeProperty() + public void ShouldFindAReadOnlyArrayProperty() { var member = MemberFinder - .GetTargetMembers(typeof(PublicReadOnlyProperty)) - .FirstOrDefault(m => m.Name == "Value"); + .GetTargetMembers(typeof(PublicReadOnlyProperty)) + .FirstOrDefault(m => m.Name.StartsWith("Value")); - member.ShouldBeNull(); + member.ShouldNotBeNull(); + member.Type.ShouldBe(typeof(long[])); + member.ElementType.ShouldBe(typeof(long)); + member.IsWriteable.ShouldBeFalse(); } [Fact] - public void ShouldIgnoreAReadOnlyArrayProperty() + public void ShouldIgnoreANonPublicField() { var member = MemberFinder - .GetTargetMembers(typeof(PublicReadOnlyProperty)) - .FirstOrDefault(m => m.Name.StartsWith("Value")); + .GetTargetMembers(typeof(InternalField>)) + .FirstOrDefault(m => m.Name == "Value"); member.ShouldBeNull(); } diff --git a/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs b/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs index 034bdba09..0b25c2a51 100644 --- a/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs +++ b/AgileMapper.UnitTests/Reflection/WhenAccessingTypeInformation.cs @@ -41,11 +41,23 @@ public void ShouldNotEvaluateAStringAsEnumerable() #region IsComplex [Fact] - public void ShouldEvaluateAComplexTypeAsComplex() + public void ShouldEvaluateAClassAsComplex() { typeof(Person).IsComplex().ShouldBeTrue(); } + [Fact] + public void ShouldEvaluateAStructAsComplex() + { + typeof(PublicCtorStruct<>).IsComplex().ShouldBeTrue(); + } + + [Fact] + public void ShouldEvaluateAnInterfaceAsComplex() + { + typeof(IPublicInterface<>).IsComplex().ShouldBeTrue(); + } + [Fact] public void ShouldNotEvaluateAnArrayAsComplex() { diff --git a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructCreationCallbacks.cs b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructCreationCallbacks.cs new file mode 100644 index 000000000..5e627387a --- /dev/null +++ b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructCreationCallbacks.cs @@ -0,0 +1,112 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs.Configuration +{ + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenConfiguringStructCreationCallbacks + { + [Fact] + public void ShouldCallAGlobalObjectCreatedCallbackWithAStruct() + { + using (var mapper = Mapper.CreateNew()) + { + var createdInstance = default(object); + + mapper.After + .CreatingInstances + .Call(ctx => createdInstance = ctx.CreatedObject); + + var source = new PublicField { Value = 123456 }; + var result = mapper.Map(source).ToANew>(); + + createdInstance.ShouldNotBeNull(); + createdInstance.ShouldBeOfType>(); + result.Value.ShouldBe(123456); + } + } + + [Fact] + public void ShouldCallAnObjectCreatedCallbackForASpecifiedStructType() + { + using (var mapper = Mapper.CreateNew()) + { + var createdStruct = default(PublicPropertyStruct); + + mapper.After + .CreatingInstancesOf>() + .Call((s, t, p) => createdStruct = p); + + var source = new { Value = "12345" }; + var nonMatchingResult = mapper.Map(source).ToANew>(); + + createdStruct.ShouldBeDefault(); + nonMatchingResult.Value.ShouldBe(12345); + + var matchingResult = mapper.Map(source).ToANew>(); + + createdStruct.ShouldNotBeNull(); + createdStruct.ShouldBe(matchingResult); + } + } + + [Fact] + public void ShouldCallAnObjectCreatedCallbackForSpecifiedSourceStructType() + { + using (var mapper = Mapper.CreateNew()) + { + var creationCount = 0; + + mapper.WhenMapping + .From>() + .To() + .Map(ctx => ctx.Source.Value) + .To(c => c.Name) + .And + .After + .CreatingTargetInstances + .Call(ctx => ++creationCount); + + var nonMatchingSource = new { Name = "Goldblum" }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew(); + + creationCount.ShouldBe(0); + nonMatchingResult.Name.ShouldBe("Goldblum"); + + var matchingSource = new PublicPropertyStruct { Value = "Fishy" }; + var matchingResult = mapper.Map(matchingSource).ToANew(); + + creationCount.ShouldBe(1); + matchingResult.Name.ShouldBe("Fishy"); + } + } + + [Fact] + public void ShouldCallAnObjectCreatedCallbackForSpecifiedSourceAndTargetStructTypes() + { + using (var mapper = Mapper.CreateNew()) + { + var createdStruct = default(PublicCtorStruct); + + mapper.WhenMapping + .From>() + .To>() + .After + .CreatingTargetInstances + .Call(ctx => createdStruct = ctx.CreatedObject); + + var nonMatchingSource = new { Value = "8765" }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + + createdStruct.ShouldBeDefault(); + nonMatchingResult.Value.ShouldBe(8765); + + var matchingSource = new PublicPropertyStruct { Value = 5678 }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + + createdStruct.ShouldNotBeNull(); + createdStruct.ShouldBe(matchingResult); + } + } + } +} diff --git a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs new file mode 100644 index 000000000..a55acb152 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructDataSources.cs @@ -0,0 +1,114 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs.Configuration +{ + using System; + using AgileMapper.Extensions; + using TestClasses; + using Xunit; + + public class WhenConfiguringStructDataSources + { + [Fact] + public void ShouldApplyAConfiguredConstantByConstructorParameterType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map("Not a Guid") + .ToCtor(); + + var source = new PublicProperty { Value = Guid.NewGuid() }; + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldBe("Not a Guid"); + } + } + + [Fact] + public void ShouldApplyAConfiguredExpressionByConstructorParameterType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(ctx => ctx.Source.Value.ToString().Length) + .ToCtor(); + + var guid = Guid.NewGuid(); + var source = new PublicField { Value = guid }; + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(guid.ToString().Length); + } + } + + [Fact] + public void ShouldApplyAConfiguredExpressionByConstructorParameterName() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map((s, t, i) => s.Value * i) + .ToCtor("value"); + + var source = new[] + { + new PublicPropertyStruct { Value = 11 }, + new PublicPropertyStruct { Value = 22 }, + new PublicPropertyStruct { Value = 33 } + }; + var result = mapper.Map(source).ToANew[]>(); + + result.Length.ShouldBe(3); + + result.First().Value.ShouldBe(11 * 0); + result.Second().Value.ShouldBe(22 * 1); + result.Third().Value.ShouldBe(33 * 2); + } + } + + [Fact] + public void ShouldApplyAConfiguredConstant() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map("-- CONFIGURED --") + .To(pps => pps.Value); + + var source = new PublicPropertyStruct { Value = "Mapped!" }; + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldBe("-- CONFIGURED --"); + } + } + + [Fact] + public void ShouldApplyAConfiguredMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To>() + .Map(ctx => ctx.Source.Id) + .To(pfs => pfs.Value1) + .And + .Map(ctx => ctx.Source.Name) + .To(pfs => pfs.Value2); + + var source = new MysteryCustomer { Id = Guid.NewGuid(), Name = "Gyles" }; + var result = mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(source.Id); + result.Value2.ShouldBe("Gyles"); + } + } + } +} diff --git a/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs new file mode 100644 index 000000000..1f7133be2 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/Configuration/WhenConfiguringStructMappingCallbacks.cs @@ -0,0 +1,95 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs.Configuration +{ + using System; + using System.Collections.Generic; + using AgileMapper.Configuration; + using AgileMapper.Extensions; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenConfiguringStructMappingCallbacks + { + [Fact] + public void ShouldExecuteAGlobalPreMappingCallback() + { + using (var mapper = Mapper.CreateNew()) + { + var mappedTypes = new List(); + + mapper.Before.MappingBegins + .Call((s, t) => mappedTypes.Add(t.GetType())); + + mapper.Map(new { Value = "Bernie" }).OnTo(new PublicTwoFieldsStruct()); + mapper.Map(new PublicPropertyStruct()).Over(new PublicPropertyStruct()); + + mappedTypes.ShouldNotBeEmpty(); + mappedTypes.ShouldBe(typeof(PublicTwoFieldsStruct), typeof(PublicPropertyStruct)); + } + } + + [Fact] + public void ShouldExecuteAGlobalPostMappingCallback() + { + using (var mapper = Mapper.CreateNew()) + { + var mappedTargets = new List(); + + mapper.After.MappingEnds + .Call((s, t) => mappedTargets.Add(t)); + + var source = new { Value = "Doobey Do" }; + + mapper.Map(source).Over(new PublicPropertyStruct()); + mapper.Map(source).Over(new PublicPropertyStruct()); + + mappedTargets.ShouldNotBeEmpty(); + mappedTargets.Count.ShouldBe(2); + + mappedTargets.First().ShouldBeOfType>(); + ((PublicPropertyStruct)mappedTargets.First()).Value.ShouldBe("Doobey Do"); + + mappedTargets.Second().ShouldBeOfType>(); + ((PublicPropertyStruct)mappedTargets.Second()).Value.ShouldBeDefault(); + } + } + + [Fact] + public void ShouldErrorIfPreMemberMappingCallbackIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ToANew>() + .Before + .Mapping(pps => pps.Value) + .Call((s, pps) => Console.WriteLine("Pre:" + pps.Value)); + } + }); + + configEx.InnerException.ShouldNotBeNull(); + configEx.InnerException.ShouldBeOfType(); + } + + [Fact] + public void ShouldErrorIfPostMemberMappingCallbackIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ToANew>() + .After + .Mapping(pps => pps.Value) + .Call((s, pps, i) => Console.WriteLine("Post: " + pps.Value)); + } + }); + + configEx.InnerException.ShouldNotBeNull(); + configEx.InnerException.ShouldBeOfType(); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Structs/Dictionaries/WhenMappingFromDictionariesToStructs.cs b/AgileMapper.UnitTests/Structs/Dictionaries/WhenMappingFromDictionariesToStructs.cs new file mode 100644 index 000000000..b686637f3 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/Dictionaries/WhenMappingFromDictionariesToStructs.cs @@ -0,0 +1,82 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs.Dictionaries +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingFromDictionariesToStructs + { + [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 ShouldPopulateAStringMemberFromAnUntypedEntry() + { + var source = new Dictionary { ["value"] = "Yes!" }; + var target = new PublicPropertyStruct { Value = "No!" }; + var result = Mapper.Map(source).Over(target); + + result.Value.ShouldBe("Yes!"); + } + + [Fact] + public void ShouldPopulateANestedBoolMemberFromUntypedDottedEntries() + { + var source = new Dictionary { ["value.value"] = "True" }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.Value.ShouldBeTrue(); + } + + [Fact] + public void ShouldPopulateARootReadOnlyCollectionFromTypedDottedEntries() + { + var source = new Dictionary + { + ["[0].Value1"] = "1", + ["[0].Value2"] = "2", + ["[1].Value1"] = "3", + ["[1].Value2"] = "4", + ["[2].Value1"] = "5", + ["[2].Value2"] = "6" + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Count.ShouldBe(3); + + result.First().Value1.ShouldBe(1); + result.First().Value2.ShouldBe(2); + + result.Second().Value1.ShouldBe(3); + result.Second().Value2.ShouldBe(4); + + result.Third().Value1.ShouldBe(5); + result.Third().Value2.ShouldBe(6); + } + + [Fact] + public void ShouldPopulateANestedCollectionFromTypedDottedEntries() + { + var source = new Dictionary + { + ["Value[0].Value"] = "NO WAY", + ["Value[1].Value"] = "YES WAY" + }; + var result = Mapper.Map(source).ToANew>>>(); + + result.Value.Count.ShouldBe(2); + result.Value.First().Value.ShouldBe("NO WAY"); + result.Value.Second().Value.ShouldBe("YES WAY"); + } + } +} diff --git a/AgileMapper.UnitTests/Structs/WhenMappingOnToStructMembers.cs b/AgileMapper.UnitTests/Structs/WhenMappingOnToStructMembers.cs new file mode 100644 index 000000000..bf8e8e929 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingOnToStructMembers.cs @@ -0,0 +1,74 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingOnToStructMembers + { + [Fact] + public void ShouldMapAMemberProperty() + { + var source = new PublicPropertyStruct> + { + Value = new PublicTwoFieldsStruct + { + Value1 = "Over here!", + Value2 = "Yes, here!" + } + }; + + var target = new PublicPropertyStruct> + { + Value = new PublicTwoFieldsStruct + { + Value1 = "Over there!" + } + }; + + var result = Mapper.Map(source).OnTo(target); + + result.Value.Value1.ShouldNotBe("Over here!"); + result.Value.Value1.ShouldBe("Over there!"); + result.Value.Value2.ShouldBe("Yes, here!"); + } + + [Fact] + public void ShouldMapFromAConfiguredSourceMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .OnTo>() + .Map(ctx => ctx.Source.Address.Line1) + .To(pps => pps.Value); + + var source = new MysteryCustomer { Name = "Andy", Address = new Address { Line1 = "Line 1!" } }; + var target = new PublicPropertyStruct(); + var result = mapper.Map(source).OnTo(target); + + result.Value.ShouldBe("Line 1!"); + } + } + + [Fact] + public void ShouldHandleANullConfiguredSourceMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .OnTo>() + .Map(ctx => ctx.Source.Address.Line1) + .To(pps => pps.Value); + + var source = new MysteryCustomer { Name = "Andy" }; + var target = new PublicPropertyStruct(); + var result = mapper.Map(source).OnTo(target); + + result.Value.ShouldBeNull(); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Structs/WhenMappingOnToStructs.cs b/AgileMapper.UnitTests/Structs/WhenMappingOnToStructs.cs new file mode 100644 index 000000000..7cd15546f --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingOnToStructs.cs @@ -0,0 +1,52 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingOnToStructs + { + [Fact] + public void ShouldMapFromAnAnonymousType() + { + var source = new { Value1 = "One", Value2 = 10.00m }; + var target = new PublicTwoFieldsStruct { Value1 = "Zero" }; + var result = Mapper.Map(source).OnTo(target); + + result.ShouldNotBe(target); + result.Value1.ShouldBe("Zero"); + result.Value2.ShouldBe("10.00"); + } + + [Fact] + public void ShouldPreserveAnExistingSimpleTypePropertyValue() + { + var source = new PublicPropertyStruct { Value = 928 }; + var target = new PublicPropertyStruct { Value = 527 }; + + var result = Mapper.Map(source).OnTo(target); + + result.Value.ShouldBe(527); + } + + [Fact] + public void ShouldOverwriteADefaultSimpleTypePropertyValue() + { + var source = new PublicGetMethod(6372.00m); + var target = new PublicPropertyStruct { Value = null }; + + var result = Mapper.Map(source).OnTo(target); + + result.Value.ShouldBe(6372.00m); + } + + [Fact] + public void ShouldHandleANullSourceObject() + { + var target = new PublicPropertyStruct { Value = 123 }; + var result = Mapper.Map(default(PublicField)).OnTo(target); + + result.ShouldBe(target); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Structs/WhenMappingOverStructMembers.cs b/AgileMapper.UnitTests/Structs/WhenMappingOverStructMembers.cs new file mode 100644 index 000000000..7951c54b9 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingOverStructMembers.cs @@ -0,0 +1,60 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingOverStructMembers + { + [Fact] + public void ShouldOverwriteAMemberToDefault() + { + var source = new PublicProperty> { Value = null }; + var target = new PublicProperty> + { + Value = new PublicPropertyStruct { Value = "Gone" } + }; + + var result = Mapper.Map(source).Over(target); + + result.ShouldBeSameAs(target); + result.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldHandleNoMatchingSourceMember() + { + var source = new { Value1 = "You" }; + var target = new PublicTwoFieldsStruct { Value1 = "kjd", Value2 = 527 }; + var result = Mapper.Map(source).Over(target); + + result.Value1.ShouldBe("You"); + result.Value2.ShouldBe(527); + } + + [Fact] + public void ShouldApplyAConfiguredConstant() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .Over>() + .Map("123") + .To(pps => pps.Value); + + var source = new PublicField> + { + Value = new PublicField { Value = "456" } + }; + var target = new PublicField> + { + Value = new PublicPropertyStruct { Value = 789 } + }; + var result = mapper.Map(source).Over(target); + + result.Value.Value.ShouldBe(123); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Structs/WhenMappingOverStructs.cs b/AgileMapper.UnitTests/Structs/WhenMappingOverStructs.cs new file mode 100644 index 000000000..e7fe8ff6e --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingOverStructs.cs @@ -0,0 +1,73 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using System; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingOverStructs + { + [Fact] + public void ShouldMapFromAnAnonymousType() + { + var source = new { Value1 = Guid.NewGuid(), Value2 = "Mr Pants" }; + var target = new PublicTwoFieldsStruct() + { + Value1 = Guid.NewGuid(), + Value2 = "Mrs Trousers" + }; + var result = Mapper.Map(source).Over(target); + + result.ShouldNotBeSameAs(target); + result.Value1.ShouldBe(source.Value1); + result.Value2.ShouldBe(source.Value2); + } + + [Fact] + public void ShouldSetAnExistingSimpleTypePropertyValueToDefault() + { + var source = new PublicTwoFields(); + var target = new PublicTwoFieldsStruct { Value1 = 537.0, Value2 = 6382 }; + + var result = Mapper.Map(source).Over(target); + + target.Value1.ShouldBe(537.0m); + target.Value2.ShouldBe(6382); + + result.Value1.ShouldBeNull(); + result.Value2.ShouldBeDefault(); + } + + [Fact] + public void ShouldHandleANullSourceObject() + { + var target = new PublicPropertyStruct(); + var result = Mapper.Map(default(PublicField)).Over(target); + + result.ShouldBe(target); + } + + [Fact] + public void ShouldApplyAConfiguredExpression() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .Over>() + .Map(ctx => ctx.Source.Value) + .To(pps => pps.Value1) + .And + .Map((pf, ptfs, i) => pf.Value + ptfs.Value2) + .To(pps => pps.Value2); + + var source = new PublicField { Value = 63872 }; + var target = new PublicTwoFieldsStruct { Value1 = 1, Value2 = 2 }; + var result = mapper.Map(source).Over(target); + + result.Value1.ShouldBe(63872); + result.Value2.ShouldBe(63874); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/Structs/WhenMappingToNewStructMembers.cs b/AgileMapper.UnitTests/Structs/WhenMappingToNewStructMembers.cs new file mode 100644 index 000000000..b94ba04cd --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingToNewStructMembers.cs @@ -0,0 +1,98 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingToNewStructMembers + { + [Fact] + public void ShouldMapToANestedConstructorlessStruct() + { + var source = new { Value = new { Value1 = "Hello", Value2 = "Goodbye" } }; + + var result = Mapper + .Map(source) + .ToANew>>(); + + result.Value.Value1.ShouldBe("Hello"); + result.Value.Value2.ShouldBe("Goodbye"); + } + + [Fact] + public void ShouldMapToANestedStructConstructor() + { + var source = new { Value = new { Value = "800" } }; + var result = Mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldNotBeNull(); + result.Value.Value.ShouldBe(800); + } + + [Fact] + public void ShouldHandleNoMatchingSourceForNestedCtorParameter() + { + var source = new { Value = new { Hello = "123" } }; + var result = Mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBeDefault(); + result.Value.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldHandleRuntimeTypedNestedMemberMatches() + { + var runtimeTypedSource = new + { + Val = (object)new { Ue1 = "Ue1!" }, + Valu = (object)new { E2 = new PublicField { Value = "123" } } + }; + + var runtimeTypedResult = Mapper.Map(runtimeTypedSource).ToANew>>(); + + runtimeTypedResult.Value1.ShouldBe("Ue1!"); + runtimeTypedResult.Value2.ShouldNotBeDefault(); + runtimeTypedResult.Value2.Value.ShouldBe(123); + + var halfRuntimeTypedSource = new { Val = (object)new { Ue1 = "Ue1!!" }, Value2 = (object)123 }; + + var halfRuntimeTypedResult = Mapper.Map(halfRuntimeTypedSource).ToANew>>(); + + halfRuntimeTypedResult.Value1.ShouldBe("Ue1!!"); + halfRuntimeTypedResult.Value2.ShouldBeDefault(); + + var nonRuntimeTypedSource = new { Value1 = (object)123, Value2 = (object)456 }; + + var nonRuntimeTypedResult = Mapper.Map(nonRuntimeTypedSource).ToANew>>(); + + nonRuntimeTypedResult.Value1.ShouldBe("123"); + nonRuntimeTypedResult.Value2.ShouldBeDefault(); + + var unconstructableSource = new { Val = (object)123, Value2 = (object)456 }; + + var unconstructableResult = Mapper.Map(unconstructableSource).ToANew>>(); + + unconstructableResult.ShouldBeNull(); + } + + [Fact] + public void ShouldIgnoreAReadOnlyStructMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.CreateAReadOnlyFieldUsing(new PublicPropertyStruct()); + + var source = new PublicField> + { + Value = new PublicField { Value = "6482" } + + }; + var result = mapper.Map(source).ToANew>>(); + + result.Value.Value.ShouldBeDefault(); + } + } + } +} diff --git a/AgileMapper.UnitTests/Structs/WhenMappingToNewStructs.cs b/AgileMapper.UnitTests/Structs/WhenMappingToNewStructs.cs new file mode 100644 index 000000000..f81519312 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingToNewStructs.cs @@ -0,0 +1,36 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using TestClasses; + using Xunit; + + public class WhenMappingToNewStructs + { + [Fact] + public void ShouldHandleANullSourceObject() + { + var result = Mapper.Map(default(PublicField)).ToANew>(); + + result.ShouldBeDefault(); + result.Value.ShouldBeDefault(); + } + + [Fact] + public void ShouldMapFromAnAnonymousTypeToAStruct() + { + var source = new { Value = "Hello struct!" }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe("Hello struct!"); + } + + [Fact] + public void ShouldConvertFieldValues() + { + var source = new PublicTwoFieldsStruct { Value1 = 123, Value2 = 456 }; + var result = Mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(123L); + result.Value2.ShouldBe("456"); + } + } +} diff --git a/AgileMapper.UnitTests/Structs/WhenMappingToStructEnumerables.cs b/AgileMapper.UnitTests/Structs/WhenMappingToStructEnumerables.cs new file mode 100644 index 000000000..d33a060e7 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingToStructEnumerables.cs @@ -0,0 +1,101 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using TestClasses; + using Xunit; + using static System.Decimal; + + public class WhenMappingToStructEnumerables + { + [Fact] + public void ShouldCreateAStructList() + { + var source = new List> + { + new PublicPropertyStruct { Value = "123.5" }, + new PublicPropertyStruct { Value = "456.7" } + }; + + var result = Mapper.Map(source).ToANew>>(); + + result.Count.ShouldBe(2); + result.First().Value.ShouldBe(123.5); + result.Second().Value.ShouldBe(456.7); + } + + [Fact] + public void ShouldHandleANullRuntimeTypedComplexTypeElement() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew>() + .Map((p, pps) => p.ProductId) + .To(pps => pps.Value); + + var source = new List + { + null, + new MegaProduct { ProductId = "Boomstick" } + }; + + var result = mapper.Map(source).ToANew>>(); + + result.Count.ShouldBe(2); + result.First().ShouldBeDefault(); + result.Second().Value.ShouldBe("Boomstick"); + } + } + + [Fact] + public void ShouldMergeARootComplexTypeArray() + { + var source = new[] + { + new PublicCtorStruct("MASSIVE") + }; + + var target = new[] + { + new PublicPropertyStruct { Value = "tiny" } + }; + + var result = Mapper.Map(source).OnTo(target); + + result.Length.ShouldBe(2); + result.First().Value.ShouldBe("tiny"); + result.Second().Value.ShouldBe("MASSIVE"); + } + + [Fact] + public void ShouldOverwriteANestedReadOnlyCollection() + { + var source = new PublicProperty>> + { + Value = new[] + { + new PublicPropertyStruct { Value = MinValue }, + new PublicPropertyStruct { Value = MaxValue } + } + }; + + var target = new PublicField>> + { + Value = new ReadOnlyCollection>(new[] + { + new PublicPropertyStruct { Value = MinusOne }, + new PublicPropertyStruct { Value = One } + }) + }; + + var result = Mapper.Map(source).Over(target); + + result.Value.Count.ShouldBe(2); + result.Value.First().Value.ShouldBe(MinValue); + result.Value.Second().Value.ShouldBe(MaxValue); + } + } +} diff --git a/AgileMapper.UnitTests/Structs/WhenMappingToUnmappableStructMembers.cs b/AgileMapper.UnitTests/Structs/WhenMappingToUnmappableStructMembers.cs new file mode 100644 index 000000000..335a36b64 --- /dev/null +++ b/AgileMapper.UnitTests/Structs/WhenMappingToUnmappableStructMembers.cs @@ -0,0 +1,41 @@ +namespace AgileObjects.AgileMapper.UnitTests.Structs +{ + using System; + using System.Collections.Generic; + using Shouldly; + using TestClasses; + using Xunit; + + public class WhenMappingToUnmappableStructMembers + { + [Fact] + public void ShouldIgnoreAMemberComplexType() + { + var source = new PublicTwoFields + { + Value1 = Guid.NewGuid(), + Value2 = new Address { Line1 = "One", Line2 = "Two" } + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(source.Value1.ToString()); + result.Value2.ShouldBeNull(); + } + + [Fact] + public void ShouldIgnoreAMemberArray() + { + var guid = Guid.NewGuid(); + + var source = new PublicTwoFields, string> + { + Value1 = new[] { 1, 2, 3 }, + Value2 = guid.ToString() + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value1.ShouldBeNull(); + result.Value2.ShouldBe(guid); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/TestClasses/PublicCtorStruct.cs b/AgileMapper.UnitTests/TestClasses/PublicCtorStruct.cs new file mode 100644 index 000000000..6d3b2c437 --- /dev/null +++ b/AgileMapper.UnitTests/TestClasses/PublicCtorStruct.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.UnitTests.TestClasses +{ + internal struct PublicCtorStruct + { + public PublicCtorStruct(T value) + { + Value = value; + } + + public T Value + { + get; + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/TestClasses/PublicPropertyStruct.cs b/AgileMapper.UnitTests/TestClasses/PublicPropertyStruct.cs new file mode 100644 index 000000000..a173d5431 --- /dev/null +++ b/AgileMapper.UnitTests/TestClasses/PublicPropertyStruct.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.UnitTests.TestClasses +{ + internal struct PublicPropertyStruct + { + public T Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/TestClasses/PublicTwoFieldsStruct.cs b/AgileMapper.UnitTests/TestClasses/PublicTwoFieldsStruct.cs new file mode 100644 index 000000000..bb29306c6 --- /dev/null +++ b/AgileMapper.UnitTests/TestClasses/PublicTwoFieldsStruct.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.TestClasses +{ + internal struct PublicTwoFieldsStruct + { + public T1 Value1; + + public T2 Value2; + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/WhenMappingOnToComplexTypeMembers.cs b/AgileMapper.UnitTests/WhenMappingOnToComplexTypeMembers.cs index 6a238360f..21c932c0f 100644 --- a/AgileMapper.UnitTests/WhenMappingOnToComplexTypeMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingOnToComplexTypeMembers.cs @@ -28,9 +28,9 @@ public void ShouldMapAMemberProperty() var result = Mapper.Map(source).OnTo(target); - result.Address.Line1.ShouldNotBeNull(source.Address.Line1); - result.Address.Line1.ShouldBe(target.Address.Line1); - result.Address.Line2.ShouldBe(source.Address.Line2); + result.Address.Line1.ShouldNotBe("Over here!"); + result.Address.Line1.ShouldBe("Over there!"); + result.Address.Line2.ShouldBe("Yes, here!"); } [Fact] diff --git a/AgileMapper.UnitTests/WhenMappingOverComplexTypeMembers.cs b/AgileMapper.UnitTests/WhenMappingOverComplexTypeMembers.cs index 33ad26c37..1cd0e4004 100644 --- a/AgileMapper.UnitTests/WhenMappingOverComplexTypeMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingOverComplexTypeMembers.cs @@ -1,6 +1,5 @@ namespace AgileObjects.AgileMapper.UnitTests { - using System; using Shouldly; using TestClasses; using Xunit; @@ -82,44 +81,6 @@ public void ShouldNotOverwriteAMemberWithNoMatchingSource() result.Address.ShouldBeSameAs(originalAddress); } - [Fact] - public void ShouldApplyAConfiguredConstant() - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .From() - .Over() - .Map("Big Timmy") - .To(x => x.Name); - - var source = new Person { Name = "Alice" }; - var target = new Person { Name = "Frank" }; - var result = mapper.Map(source).Over(target); - - result.Name.ShouldBe("Big Timmy"); - } - } - - [Fact] - public void ShouldApplyAConfiguredExpression() - { - using (var mapper = Mapper.CreateNew()) - { - mapper.WhenMapping - .From() - .Over() - .Map(ctx => ctx.Source.Id) - .To(x => x.Name); - - var source = new Customer { Id = Guid.NewGuid() }; - var target = new Person(); - var result = mapper.Map(source).Over(target); - - result.Name.ShouldBe(source.Id.ToString()); - } - } - [Fact] public void ShouldHandleANullConfiguredSourceMember() { diff --git a/AgileMapper.UnitTests/WhenMappingOverComplexTypes.cs b/AgileMapper.UnitTests/WhenMappingOverComplexTypes.cs index 9f10421d6..96d23b027 100644 --- a/AgileMapper.UnitTests/WhenMappingOverComplexTypes.cs +++ b/AgileMapper.UnitTests/WhenMappingOverComplexTypes.cs @@ -51,6 +51,44 @@ public void ShouldNullAnExistingSimpleTypePropertyValue() target.Value.ShouldBeNull(); } + [Fact] + public void ShouldApplyAConfiguredConstant() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .Over() + .Map("Big Timmy") + .To(x => x.Name); + + var source = new Person { Name = "Alice" }; + var target = new Person { Name = "Frank" }; + var result = mapper.Map(source).Over(target); + + result.Name.ShouldBe("Big Timmy"); + } + } + + [Fact] + public void ShouldApplyAConfiguredExpression() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .Over() + .Map(ctx => ctx.Source.Id) + .To(x => x.Name); + + var source = new Customer { Id = Guid.NewGuid() }; + var target = new Person(); + var result = mapper.Map(source).Over(target); + + result.Name.ShouldBe(source.Id.ToString()); + } + } + [Fact] public void ShouldHandleANullSourceObject() { diff --git a/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs b/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs index 0c3caf193..5def6f6c5 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs @@ -167,6 +167,7 @@ public void ShouldUseBestMatchingSourceMemberWhenCloning() result.Currency.ShouldBeNull(); result.CurrencyId.ShouldBe(1); } + [Fact] public void ShouldUseBestMatchingSourceMemberWhenNotCloning() { @@ -285,13 +286,16 @@ public void ShouldHandleANullUnconstructableNestedMember() private class Country { + // ReSharper disable once UnusedAutoPropertyAccessor.Local public Currency Currency { get; set; } public int CurrencyId { get; set; } } + // ReSharper disable once ClassNeverInstantiated.Local private class Currency { + // ReSharper disable once UnusedAutoPropertyAccessor.Local public int Id { get; set; } } diff --git a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs index d7c559680..156e1130f 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs @@ -270,6 +270,16 @@ public void ShouldHandleANullSourceMemberInAConfiguredEnumerableSource() } } + [Fact] + public void ShouldMapFromAnEnumerableToAWriteOnlyTarget() + { + var source = new PublicField> { Value = new[] { 1, 2, 3 } }; + var result = Mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldBe("1", "2", "3"); + } + [Fact] public void ShouldHandleANullSourceMemberForAWriteOnlyTarget() { diff --git a/AgileMapper.UnitTests/WhenViewingMappingPlans.cs b/AgileMapper.UnitTests/WhenViewingMappingPlans.cs index 7e79cdbd6..08b82bb6c 100644 --- a/AgileMapper.UnitTests/WhenViewingMappingPlans.cs +++ b/AgileMapper.UnitTests/WhenViewingMappingPlans.cs @@ -70,7 +70,7 @@ public void ShouldIncludeAConfiguredExpression() .GetPlanFor() .Over(); - plan.ShouldContain("personViewModel.Name = sourcePerson.Title + \" \" + sourcePerson.Name"); + plan.ShouldContain("pToPvmData.Target.Name = sourcePerson.Title + \" \" + sourcePerson.Name"); } } diff --git a/AgileMapper/Api/Configuration/CallbackSpecifier.cs b/AgileMapper/Api/Configuration/CallbackSpecifier.cs index 8cf21da38..0db0e866a 100644 --- a/AgileMapper/Api/Configuration/CallbackSpecifier.cs +++ b/AgileMapper/Api/Configuration/CallbackSpecifier.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; using AgileMapper.Configuration; + using AgileObjects.NetStandardPolyfills; using Members; using ObjectPopulation; @@ -59,6 +60,8 @@ public MappingConfigContinuation Call(Action CreateCallbackFactory(TAction callback) { + ThrowIfStructMemberCallback(); + var callbackLambda = ConfiguredLambdaInfo.ForAction(callback, typeof(TSource), typeof(TTarget)); var creationCallbackFactory = new MappingCallbackFactory( @@ -71,5 +74,18 @@ private MappingConfigContinuation CreateCallbackFactory(ConfigInfo); } + + private void ThrowIfStructMemberCallback() + { + if ((_targetMember == QualifiedMember.All) || typeof(TTarget).IsClass()) + { + return; + } + + throw new MappingConfigurationException( + "Cannot configure struct member population callbacks", + new NotSupportedException( + "Structs are populated with Member Initialisations, so cannot have member population callbacks")); + } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index fa8ea526c..68812db88 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -20,17 +20,19 @@ public class CustomDataSourceTargetMemberSpecifier { private readonly MappingConfigInfo _configInfo; - private readonly ConfiguredLambdaInfo _customValueLambda; + private readonly LambdaExpression _customValueLambda; + private readonly ConfiguredLambdaInfo _customValueLambdaInfo; internal CustomDataSourceTargetMemberSpecifier(MappingConfigInfo configInfo, LambdaExpression customValueLambda) - : this(configInfo, ConfiguredLambdaInfo.For(customValueLambda)) + : this(configInfo, default(ConfiguredLambdaInfo)) { + _customValueLambda = customValueLambda; } internal CustomDataSourceTargetMemberSpecifier(MappingConfigInfo configInfo, ConfiguredLambdaInfo customValueLambda) { _configInfo = configInfo; - _customValueLambda = customValueLambda; + _customValueLambdaInfo = customValueLambda; } /// @@ -44,7 +46,7 @@ internal CustomDataSourceTargetMemberSpecifier(MappingConfigInfo configInfo, Con /// public MappingConfigContinuation To( Expression> targetMember) - => RegisterDataSource(() => CreateFromLambda(targetMember)); + => RegisterDataSource(() => CreateFromLambda(targetMember)); /// /// Apply the configuration to the given . @@ -57,16 +59,18 @@ public MappingConfigContinuation To( /// public MappingConfigContinuation To( Expression>> targetSetMethod) - => RegisterDataSource(() => CreateFromLambda(targetSetMethod)); + => RegisterDataSource(() => CreateFromLambda(targetSetMethod)); - private ConfiguredDataSourceFactory CreateFromLambda(LambdaExpression targetMemberLambda) + private ConfiguredDataSourceFactory CreateFromLambda(LambdaExpression targetMemberLambda) { + var valueLambda = GetValueLambda(); + if (IsDictionaryEntry(targetMemberLambda, out var dictionaryEntryMember)) { - return new ConfiguredDictionaryDataSourceFactory(_configInfo, _customValueLambda, dictionaryEntryMember); + return new ConfiguredDictionaryDataSourceFactory(_configInfo, valueLambda, dictionaryEntryMember); } - return new ConfiguredDataSourceFactory(_configInfo, _customValueLambda, targetMemberLambda); + return new ConfiguredDataSourceFactory(_configInfo, valueLambda, targetMemberLambda); } private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out DictionaryTargetMember entryMember) @@ -106,6 +110,34 @@ private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out Dictiona return true; } + private ConfiguredLambdaInfo GetValueLambda() + { + if (_customValueLambdaInfo != null) + { + return _customValueLambdaInfo; + } + + if ((_customValueLambda.Body.NodeType != ExpressionType.Constant) || + (typeof(TTargetValue) == typeof(object)) || + typeof(TTargetValue).IsAssignableFrom(_customValueLambda.ReturnType)) + { + return ConfiguredLambdaInfo.For(_customValueLambda); + } + + var convertedConstantValue = _configInfo + .MapperContext + .ValueConverters + .GetConversion(_customValueLambda.Body, typeof(TTargetValue)); + + var valueLambda = Expression.Lambda>(convertedConstantValue); + var valueFunc = valueLambda.Compile(); + var value = valueFunc.Invoke().ToConstantExpression(typeof(TTargetValue)); + var constantValueLambda = Expression.Lambda>(value); + var valueLambdaInfo = ConfiguredLambdaInfo.For(constantValueLambda); + + return valueLambdaInfo; + } + /// /// Apply the configuration to the constructor parameter with the type specified by the type argument. /// @@ -129,10 +161,10 @@ public MappingConfigContinuation ToCtor(string parameterName) => RegisterDataSource(() => CreateForCtorParam(parameterName)); private ConfiguredDataSourceFactory CreateForCtorParam() - => CreateForCtorParam(GetUniqueConstructorParameterOrThrow()); + => CreateForCtorParam(GetUniqueConstructorParameterOrThrow()); private ConfiguredDataSourceFactory CreateForCtorParam(string name) - => CreateForCtorParam(GetUniqueConstructorParameterOrThrow(name)); + => CreateForCtorParam(GetUniqueConstructorParameterOrThrow(name)); private static ParameterInfo GetUniqueConstructorParameterOrThrow(string name = null) { @@ -192,7 +224,7 @@ private static Exception AmbiguousParameterException(string parameterMatchInfo) typeof(TTarget).GetFriendlyName())); } - private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) + private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) { var memberChain = new[] { @@ -200,9 +232,10 @@ private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) Member.ConstructorParameter(parameter) }; + var valueLambda = GetValueLambda(); var constructorParameter = QualifiedMember.From(memberChain, _configInfo.MapperContext); - return new ConfiguredDataSourceFactory(_configInfo, _customValueLambda, constructorParameter); + return new ConfiguredDataSourceFactory(_configInfo, valueLambda, constructorParameter); } private MappingConfigContinuation RegisterDataSource( @@ -210,12 +243,12 @@ private MappingConfigContinuation RegisterDataSource(); - _configInfo.ForTargetType(); + var configInfo = _configInfo.ForTargetType(); var configuredDataSourceFactory = factoryFactory.Invoke(); - _configInfo.MapperContext.UserConfigurations.Add(configuredDataSourceFactory); + configInfo.MapperContext.UserConfigurations.Add(configuredDataSourceFactory); - return new MappingConfigContinuation(_configInfo); + return new MappingConfigContinuation(configInfo); } private struct AnyParameterType { } diff --git a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs index 5eebb8123..7052d83ab 100644 --- a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs @@ -383,7 +383,7 @@ public TargetTypeSpecifier From() /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator To() where TTarget : class + public IFullMappingConfigurator To() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForAllRuleSets()).To(); /// @@ -392,7 +392,7 @@ public IFullMappingConfigurator To() where TTarget : c /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator ToANew() where TTarget : class + public IFullMappingConfigurator ToANew() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Constants.CreateNew)).ToANew(); /// @@ -401,7 +401,7 @@ public IFullMappingConfigurator ToANew() where TTarget /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator OnTo() where TTarget : class + public IFullMappingConfigurator OnTo() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Constants.Merge)).OnTo(); /// @@ -410,7 +410,7 @@ public IFullMappingConfigurator OnTo() where TTarget : /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator Over() where TTarget : class + public IFullMappingConfigurator Over() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Constants.Overwrite)).Over(); private TargetTypeSpecifier GetAllSourcesTargetTypeSpecifier( diff --git a/AgileMapper/Api/Configuration/PostEventConfigStartingPoint.cs b/AgileMapper/Api/Configuration/PostEventConfigStartingPoint.cs index 70824655f..9493de61d 100644 --- a/AgileMapper/Api/Configuration/PostEventConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/PostEventConfigStartingPoint.cs @@ -37,7 +37,6 @@ public IConditionalPostInstanceCreationCallbackSpecifier /// The type of object the creation of which the callback execution should follow. /// public IConditionalPostInstanceCreationCallbackSpecifier CreatingInstancesOf() - where TObject : class => new InstanceCreationCallbackSpecifier(CallbackPosition.After, _mapperContext); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs b/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs index e9b52d2e1..70340ec11 100644 --- a/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs +++ b/AgileMapper/Api/Configuration/TargetTypeSpecifier.cs @@ -23,7 +23,7 @@ internal TargetTypeSpecifier(MappingConfigInfo configInfo) /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator To() where TTarget : class + public IFullMappingConfigurator To() => new MappingConfigurator(_configInfo.ForAllRuleSets()); /// @@ -32,7 +32,7 @@ public IFullMappingConfigurator To() where TTarget : /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator ToANew() where TTarget : class + public IFullMappingConfigurator ToANew() => UsingRuleSet(Constants.CreateNew); /// @@ -41,7 +41,7 @@ public IFullMappingConfigurator ToANew() where TTarge /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator OnTo() where TTarget : class + public IFullMappingConfigurator OnTo() => UsingRuleSet(Constants.Merge); /// @@ -50,10 +50,10 @@ public IFullMappingConfigurator OnTo() where TTarget /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator Over() where TTarget : class + public IFullMappingConfigurator Over() => UsingRuleSet(Constants.Overwrite); - private MappingConfigurator UsingRuleSet(string name) where TTarget : class + private MappingConfigurator UsingRuleSet(string name) => new MappingConfigurator(_configInfo.ForRuleSet(name)); /// diff --git a/AgileMapper/Api/ITargetTypeSelector.cs b/AgileMapper/Api/ITargetTypeSelector.cs index fd221bf46..86e6b426c 100644 --- a/AgileMapper/Api/ITargetTypeSelector.cs +++ b/AgileMapper/Api/ITargetTypeSelector.cs @@ -10,7 +10,7 @@ public interface ITargetTypeSelector /// /// The type of object to create from the specified source object. /// The result of the new object mapping. - TResult ToANew() where TResult : class; + TResult ToANew(); /// /// Perform an OnTo (merge) mapping. @@ -18,7 +18,7 @@ public interface ITargetTypeSelector /// The type of object on which to perform the mapping. /// The object on which to perform the mapping. /// The mapped object. - TTarget OnTo(TTarget existing) where TTarget : class; + TTarget OnTo(TTarget existing); /// /// Perform an Over (overwrite) mapping. @@ -26,6 +26,6 @@ public interface ITargetTypeSelector /// The type of object on which to perform the mapping. /// The object on which to perform the mapping. /// The mapped object. - TTarget Over(TTarget existing) where TTarget : class; + TTarget Over(TTarget existing); } } diff --git a/AgileMapper/Configuration/ConfiguredLambdaInfo.cs b/AgileMapper/Configuration/ConfiguredLambdaInfo.cs index 069367caa..082b36dda 100644 --- a/AgileMapper/Configuration/ConfiguredLambdaInfo.cs +++ b/AgileMapper/Configuration/ConfiguredLambdaInfo.cs @@ -133,7 +133,7 @@ public Expression GetBody( { return position.IsPriorToObjectCreation(targetMember) ? _parametersSwapper.Swap(_lambda, mapperData, ParametersSwapper.UseTargetMember) - : _parametersSwapper.Swap(_lambda, mapperData, ParametersSwapper.UseInstanceVariable); + : _parametersSwapper.Swap(_lambda, mapperData, ParametersSwapper.UseTargetInstance); } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/ParametersSwapper.cs b/AgileMapper/Configuration/ParametersSwapper.cs index 948099a2a..c8b3e7832 100644 --- a/AgileMapper/Configuration/ParametersSwapper.cs +++ b/AgileMapper/Configuration/ParametersSwapper.cs @@ -151,7 +151,7 @@ private static Expression ReplaceParameters( params Func[] parameterFactories) { var contextInfo = GetAppropriateMappingContext( - new[] { swapArgs.Lambda.Parameters[0].Type, swapArgs.Lambda.Parameters[1].Type }, + swapArgs.Lambda.Parameters.Select(p => p.Type).ToArray(), swapArgs); return swapArgs.Lambda.ReplaceParametersWith(parameterFactories.Select(f => f.Invoke(contextInfo)).ToArray()); @@ -197,20 +197,30 @@ public bool AppliesTo(Type[] contextTypes, Type[] funcArguments) public static Expression UseTargetMember(IMemberMapperData mapperData, Expression contextAccess, Type targetType) => mapperData.GetTargetAccess(contextAccess, targetType); - public static Expression UseInstanceVariable(IMemberMapperData mapperData, Expression contextAccess, Type targetType) + public static Expression UseTargetInstance(IMemberMapperData mapperData, Expression contextAccess, Type targetType) { if (!contextAccess.Type.IsGenericType()) { return UseTargetMember(mapperData, contextAccess, targetType); } - var instanceVariableAccess = mapperData + var targetInstanceAccess = mapperData .GetAppropriateMappingContext(contextAccess.Type.GetGenericArguments()) - .InstanceVariable; + .TargetInstance; - return targetType.IsAssignableFrom(instanceVariableAccess.Type) - ? instanceVariableAccess - : instanceVariableAccess.GetConversionTo(targetType); + return ConvertTargetType(targetType, targetInstanceAccess) + ? targetInstanceAccess.GetConversionTo(targetType) + : targetInstanceAccess; + } + + private static bool ConvertTargetType(Type targetType, Expression targetInstanceAccess) + { + if (targetType.IsAssignableFrom(targetInstanceAccess.Type)) + { + return targetInstanceAccess.Type.IsValueType(); + } + + return true; } public Expression Swap( @@ -234,15 +244,40 @@ public MappingContextInfo(SwapArgs swapArgs, Type[] contextTypes) public MappingContextInfo(SwapArgs swapArgs, Expression contextAccess, Type[] contextTypes) { + var contextSourceType = contextTypes[0]; + var contextTargetType = contextTypes[1]; + var sourceAccess = swapArgs.MapperData.GetSourceAccess(contextAccess, contextSourceType); + var targetAccess = swapArgs.TargetValueFactory.Invoke(swapArgs.MapperData, contextAccess, contextTargetType); + ContextTypes = contextTypes; - CreatedObject = swapArgs.MapperData.CreatedObject; - SourceAccess = swapArgs.MapperData.GetSourceAccess(contextAccess, contextTypes[0]); - TargetAccess = swapArgs.TargetValueFactory.Invoke(swapArgs.MapperData, contextAccess, contextTypes[1]); + CreatedObject = GetCreatedObject(swapArgs, contextTypes); + SourceAccess = GetValueAccess(sourceAccess, contextSourceType); + TargetAccess = GetValueAccess(targetAccess, contextTargetType); Index = swapArgs.MapperData.EnumerableIndex; Parent = swapArgs.MapperData.ParentObject; MappingDataAccess = swapArgs.MapperData.GetTypedContextAccess(contextAccess, contextTypes); } + private static Expression GetCreatedObject(SwapArgs swapArgs, ICollection contextTypes) + { + var neededCreatedObjectType = contextTypes.Last(); + var createdObject = swapArgs.MapperData.CreatedObject; + + if ((contextTypes.Count == 3) && (neededCreatedObjectType == typeof(int?))) + { + return createdObject; + } + + return GetValueAccess(createdObject, neededCreatedObjectType); + } + + private static Expression GetValueAccess(Expression valueAccess, Type neededAccessType) + { + return (neededAccessType != valueAccess.Type) && valueAccess.Type.IsValueType() + ? valueAccess.GetConversionTo(neededAccessType) + : valueAccess; + } + public Type[] ContextTypes { get; } public Expression CreatedObject { get; } diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index 7218295b2..c2004bf40 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -172,7 +172,9 @@ public Expression GetCreationCallbackOrNull(CallbackPosition position, IMemberMa return null; } - return _creationCallbackFactories.FirstOrDefault(f => f.AppliesTo(position, mapperData))?.Create(mapperData); + return _creationCallbackFactories + .FirstOrDefault(f => f.AppliesTo(position, mapperData))? + .Create(mapperData); } #endregion diff --git a/AgileMapper/Configuration/UserConfiguredItemBase.cs b/AgileMapper/Configuration/UserConfiguredItemBase.cs index 13a5ff177..97ece58e3 100644 --- a/AgileMapper/Configuration/UserConfiguredItemBase.cs +++ b/AgileMapper/Configuration/UserConfiguredItemBase.cs @@ -26,13 +26,19 @@ private static QualifiedMember GetTargetMemberOrThrow(LambdaExpression lambda) { var targetMember = lambda.Body.ToTargetMember(MapperContext.Default); - if (targetMember != null) + if (targetMember == null) { - return targetMember; + throw new MappingConfigurationException( + $"Target member {lambda.Body.ToReadableString()} is not writeable"); } - throw new MappingConfigurationException( - $"Target member {lambda.Body.ToReadableString()} is not writeable."); + if (targetMember.IsUnmappable(out var reason)) + { + throw new MappingConfigurationException( + $"Target member {lambda.Body.ToReadableString()} is not mappable ({reason})"); + } + + return targetMember; } protected UserConfiguredItemBase(MappingConfigInfo configInfo, QualifiedMember targetMember) diff --git a/AgileMapper/DataSources/DataSourceFinder.cs b/AgileMapper/DataSources/DataSourceFinder.cs index f18d7693e..45f84be4e 100644 --- a/AgileMapper/DataSources/DataSourceFinder.cs +++ b/AgileMapper/DataSources/DataSourceFinder.cs @@ -136,6 +136,11 @@ private static IEnumerable GetSourceMemberDataSources( yield return matchingSourceMemberDataSource; + if (mappingData.MapperData.TargetMember.IsReadOnly) + { + yield break; + } + if (matchingSourceMemberDataSource.IsConditional) { yield return GetFallbackDataSourceFor(mappingData); diff --git a/AgileMapper/Extensions/EnumerableExtensions.cs b/AgileMapper/Extensions/EnumerableExtensions.cs index b5ff61df8..10c95d82f 100644 --- a/AgileMapper/Extensions/EnumerableExtensions.cs +++ b/AgileMapper/Extensions/EnumerableExtensions.cs @@ -163,6 +163,7 @@ public static T[] ToArray(this ICollection items) return array; } + [DebuggerStepThrough] public static IEnumerable WhereNotNull(this IEnumerable items) => items.Where(item => item != null); public static T[] Prepend(this IList items, T initialItem) diff --git a/AgileMapper/Extensions/ExpressionExtensions.Replace.cs b/AgileMapper/Extensions/ExpressionExtensions.Replace.cs index d1b98ad90..a741f058b 100644 --- a/AgileMapper/Extensions/ExpressionExtensions.Replace.cs +++ b/AgileMapper/Extensions/ExpressionExtensions.Replace.cs @@ -186,10 +186,10 @@ private Expression ReplaceIn(LambdaExpression lambda) private Expression ReplaceIn(MemberExpression memberAccess) => ReplaceIn(memberAccess, ma => ma.Update(Replace(ma.Expression))); private Expression ReplaceIn(MemberInitExpression memberInit) - => ReplaceIn(memberInit, mi => mi.Update(ReplaceIn(mi.NewExpression), mi.Bindings.Select(ReplaceIn))); + => ReplaceIn(memberInit, mi => mi.Update(ReplaceInNew(mi.NewExpression), mi.Bindings.Select(ReplaceIn))); private Expression ReplaceIn(ListInitExpression listInit) - => ReplaceIn(listInit, li => li.Update(ReplaceIn(li.NewExpression), ReplaceIn(li.Initializers))); + => ReplaceIn(listInit, li => li.Update(ReplaceInNew(li.NewExpression), ReplaceIn(li.Initializers))); private Expression ReplaceIn(LoopExpression loop) => ReplaceIn(loop, l => l.Update(l.BreakLabel, l.ContinueLabel, Replace(l.Body))); @@ -216,7 +216,14 @@ private MemberBinding ReplaceIn(MemberBinding binding) private IEnumerable ReplaceIn(IEnumerable initializers) => initializers.Select(init => init.Update(init.Arguments.Select(Replace))); - private NewExpression ReplaceIn(NewExpression newing) => (NewExpression)ReplaceIn(newing, nw => nw.Update(nw.Arguments.Select(Replace))); + private Expression ReplaceIn(NewExpression newing) => ReplaceIn(newing, ReplaceInNew); + + private NewExpression ReplaceInNew(NewExpression newing) + { + return newing.Arguments.None() + ? newing + : newing.Update(newing.Arguments.Select(Replace)); + } private Expression ReplaceIn(NewArrayExpression newArray) => ReplaceIn(newArray, na => na.Update(na.Expressions.Select(Replace))); diff --git a/AgileMapper/Extensions/ExpressionExtensions.cs b/AgileMapper/Extensions/ExpressionExtensions.cs index 6635add98..26feae390 100644 --- a/AgileMapper/Extensions/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/ExpressionExtensions.cs @@ -112,7 +112,22 @@ public static Expression GetIsNotDefaultComparison(this Expression expression) return Expression.Property(expression, "HasValue"); } - return Expression.NotEqual(expression, ToDefaultExpression(expression.Type)); + 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); } public static Expression GetIndexAccess(this Expression indexedExpression, Expression indexValue) @@ -232,13 +247,8 @@ private static Expression GetToEnumerableCall(Expression enumerable, MethodInfo return Expression.Call(typedToEnumerableMethod, enumerable); } - public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type elementType = null) + public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type elementType) { - if (elementType == null) - { - elementType = enumerableType.GetEnumerableElementType(); - } - if (enumerableType.IsArray) { return GetEmptyArray(elementType); diff --git a/AgileMapper/Extensions/TypeExtensions.cs b/AgileMapper/Extensions/TypeExtensions.cs index 8ca08c7bb..26bf0cb11 100644 --- a/AgileMapper/Extensions/TypeExtensions.cs +++ b/AgileMapper/Extensions/TypeExtensions.cs @@ -183,7 +183,14 @@ public static bool IsComplex(this Type type) public static bool IsSimple(this Type type) { - return type.IsValueType() || (type == typeof(string)); + type = type.GetNonNullableType(); + + if (type.GetTypeCode() == NetStandardTypeCode.Object) + { + return type == typeof(Guid); + } + + return true; } public static bool IsDictionary(this Type type) diff --git a/AgileMapper/MappingExecutor.cs b/AgileMapper/MappingExecutor.cs index 1076773a4..460d81d6b 100644 --- a/AgileMapper/MappingExecutor.cs +++ b/AgileMapper/MappingExecutor.cs @@ -27,13 +27,13 @@ public MappingExecutor(MappingRuleSet ruleSet, MapperContext mapperContext) public MappingRuleSet RuleSet { get; private set; } - public TResult ToANew() where TResult : class + public TResult ToANew() => PerformMapping(MapperContext.RuleSets.CreateNew, default(TResult)); - public TTarget OnTo(TTarget existing) where TTarget : class + public TTarget OnTo(TTarget existing) => PerformMapping(MapperContext.RuleSets.Merge, existing); - public TTarget Over(TTarget existing) where TTarget : class + public TTarget Over(TTarget existing) => PerformMapping(MapperContext.RuleSets.Overwrite, existing); private TTarget PerformMapping(MappingRuleSet ruleSet, TTarget target) diff --git a/AgileMapper/Members/ChildMemberMapperData.cs b/AgileMapper/Members/ChildMemberMapperData.cs index d94292413..348b0b150 100644 --- a/AgileMapper/Members/ChildMemberMapperData.cs +++ b/AgileMapper/Members/ChildMemberMapperData.cs @@ -32,7 +32,7 @@ public ChildMemberMapperData(QualifiedMember targetMember, ObjectMapperData pare public Expression EnumerableIndex => Parent.EnumerableIndex; - public ParameterExpression InstanceVariable => Parent.InstanceVariable; + public Expression TargetInstance => Parent.TargetInstance; public ExpressionInfoFinder ExpressionInfoFinder => Parent.ExpressionInfoFinder; } diff --git a/AgileMapper/Members/DictionaryEntrySourceMember.cs b/AgileMapper/Members/DictionaryEntrySourceMember.cs index 49924a62f..4ddb7c1e8 100644 --- a/AgileMapper/Members/DictionaryEntrySourceMember.cs +++ b/AgileMapper/Members/DictionaryEntrySourceMember.cs @@ -55,6 +55,8 @@ private DictionaryEntrySourceMember( public Type Type { get; } + public Type ElementType => _childMembers.First().ElementType; + public bool IsEnumerable { get; } public string Name => _matchedTargetMember.Name; diff --git a/AgileMapper/Members/DictionaryTargetMember.cs b/AgileMapper/Members/DictionaryTargetMember.cs index 73d04c9f0..2a0225cc0 100644 --- a/AgileMapper/Members/DictionaryTargetMember.cs +++ b/AgileMapper/Members/DictionaryTargetMember.cs @@ -192,7 +192,7 @@ private Expression GetDictionaryAccess(IMemberMapperData mapperData) dictionaryMapperData = dictionaryMapperData.Parent; } - return dictionaryMapperData.InstanceVariable; + return dictionaryMapperData.TargetInstance; } public override bool CheckExistingElementValue => !HasObjectEntries && !HasSimpleEntries; @@ -252,7 +252,7 @@ public override Expression GetPopulation(Expression value, IMemberMapperData map return flattening; } - var keyedAccess = GetAccess(mapperData.InstanceVariable, mapperData); + var keyedAccess = this.GetAccess(mapperData); var convertedValue = HasComplexEntries ? GetCheckedValue((BlockExpression)value, keyedAccess, mapperData) @@ -320,6 +320,11 @@ private static IList GetMappingExpressions(Expression mapping) private Expression GetCheckedValue(BlockExpression value, Expression keyedAccess, IMemberMapperData mapperData) { + if (mapperData.SourceMember.IsEnumerable) + { + return value; + } + var checkedAccess = GetAccessChecked(mapperData); var existingValue = checkedAccess.Variables.First(); var replacements = new ExpressionReplacementDictionary(1) { [keyedAccess] = existingValue }; diff --git a/AgileMapper/Members/ExpressionInfoFinder.cs b/AgileMapper/Members/ExpressionInfoFinder.cs index b439fc025..0dba1e1cd 100644 --- a/AgileMapper/Members/ExpressionInfoFinder.cs +++ b/AgileMapper/Members/ExpressionInfoFinder.cs @@ -25,8 +25,6 @@ public ExpressionInfo FindIn(Expression expression, bool targetCanBeNull) private class ExpressionInfoFinderInstance : ExpressionVisitor { - private static readonly Expression[] _noMemberAccesses = Enumerable.EmptyArray; - private readonly Expression _mappingDataObject; private readonly ICollection _stringMemberAccessSubjects; private readonly ICollection _allInvocations; @@ -53,7 +51,7 @@ public ExpressionInfo FindIn(Expression expression) Visit(expression); var nestedAccesses = _nestedAccessesByPath.None() - ? _noMemberAccesses + ? Enumerable.EmptyArray : _nestedAccessesByPath.Values.Reverse().ToArray(); var multiInvocations = _multiInvocations diff --git a/AgileMapper/Members/IMappingData.cs b/AgileMapper/Members/IMappingData.cs index c0982d28b..d599e1520 100644 --- a/AgileMapper/Members/IMappingData.cs +++ b/AgileMapper/Members/IMappingData.cs @@ -17,7 +17,7 @@ public interface IMappingData /// /// The type to which to cast the source object. /// The source object for the mapping context. - TSource GetSource() where TSource : class; + TSource GetSource(); /// /// Gets the target object for the mapping context described by the @@ -25,7 +25,7 @@ public interface IMappingData /// /// The type to which to cast the target object. /// The target object for the mapping context. - TTarget GetTarget() where TTarget : class; + TTarget GetTarget(); /// /// Gets the index of the current enumerable being mapped in the mapping context described by the diff --git a/AgileMapper/Members/IMemberMapperData.cs b/AgileMapper/Members/IMemberMapperData.cs index 2af0f5af8..e2c61acde 100644 --- a/AgileMapper/Members/IMemberMapperData.cs +++ b/AgileMapper/Members/IMemberMapperData.cs @@ -25,7 +25,7 @@ internal interface IMemberMapperData : IBasicMapperData Expression EnumerableIndex { get; } - ParameterExpression InstanceVariable { get; } + Expression TargetInstance { get; } ExpressionInfoFinder ExpressionInfoFinder { get; } } diff --git a/AgileMapper/Members/MappingInstanceData.cs b/AgileMapper/Members/MappingInstanceData.cs index bc5c5f7ec..4d06379ee 100644 --- a/AgileMapper/Members/MappingInstanceData.cs +++ b/AgileMapper/Members/MappingInstanceData.cs @@ -1,5 +1,7 @@ namespace AgileObjects.AgileMapper.Members { + using System.Reflection; + internal class MappingInstanceData : IMappingData, IMappingData { private readonly IMappingData _parent; @@ -31,9 +33,25 @@ protected MappingInstanceData( public int? EnumerableIndex { get; } - T IMappingData.GetSource() => Source as T; + T IMappingData.GetSource() + { + if (typeof(TSource).IsAssignableFrom(typeof(T))) + { + return (T)((object)Source); + } + + return default(T); + } - T IMappingData.GetTarget() => Target as T; + T IMappingData.GetTarget() + { + if (typeof(TTarget).IsAssignableFrom(typeof(T))) + { + return (T)((object)Target); + } + + return default(T); + } public int? GetEnumerableIndex() => EnumerableIndex ?? _parent?.GetEnumerableIndex(); diff --git a/AgileMapper/Members/MemberExtensions.cs b/AgileMapper/Members/MemberExtensions.cs index c36f342e1..a85a27a62 100644 --- a/AgileMapper/Members/MemberExtensions.cs +++ b/AgileMapper/Members/MemberExtensions.cs @@ -2,11 +2,13 @@ { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Extensions; + using NetStandardPolyfills; using ReadableExpressions.Extensions; internal static class MemberExtensions @@ -48,6 +50,69 @@ private static string GetMemberPath(IQualifiedMember member, IQualifiedMember ro return path; } + public static bool IsUnmappable(this QualifiedMember member, out string reason) + { + if (IsStructNonSimpleMember(member)) + { + reason = member.Type.GetFriendlyName() + " member on a struct"; + return true; + } + + if (member.LeafMember.MemberType == MemberType.SetMethod) + { + reason = null; + return false; + } + + if (!member.IsReadable) + { + reason = "write-only member"; + return true; + } + + if (!member.IsReadOnly) + { + reason = null; + return false; + } + + if (member.Type.IsArray) + { + reason = "readonly array"; + return true; + } + + if (member.IsSimple || member.Type.IsValueType()) + { + reason = "readonly " + ((member.IsComplex) ? "struct" : member.Type.GetFriendlyName()); + return true; + } + + if (member.IsEnumerable && + member.Type.IsGenericType() && + (member.Type.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>))) + { + reason = "readonly ReadOnlyCollection"; + return true; + } + + reason = null; + return false; + } + + private static bool IsStructNonSimpleMember(QualifiedMember member) + { + if (member.IsSimple || member.Type.IsValueType()) + { + return false; + } + + return member.MemberChain[member.MemberChain.Length - 2].Type.IsValueType(); + } + + public static Expression GetAccess(this QualifiedMember member, IMemberMapperData mapperData) + => member.GetAccess(mapperData.TargetInstance, mapperData); + public static Expression GetQualifiedAccess(this IEnumerable memberChain, IMemberMapperData mapperData) { // Skip(1) because the 0th member is the mapperData.SourceObject: diff --git a/AgileMapper/Members/MemberFinder.cs b/AgileMapper/Members/MemberFinder.cs index fe42c55b0..5716d383f 100644 --- a/AgileMapper/Members/MemberFinder.cs +++ b/AgileMapper/Members/MemberFinder.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Generic; - using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Caching; @@ -51,8 +50,8 @@ public IList GetTargetMembers(Type targetType) { return _membersCache.GetOrAdd(TypeKey.ForTargetMembers(targetType), key => { - var fields = GetFields(key.Type, OnlyTargets); - var properties = GetProperties(key.Type, OnlyTargets); + var fields = GetFields(key.Type, All); + var properties = GetProperties(key.Type, All); var methods = GetMethods(key.Type, OnlyCallableSetters, Member.SetMethod); var constructorParameterNames = key.Type @@ -82,9 +81,6 @@ private static IEnumerable GetFields(Type targetType, Func true; - private static bool OnlyTargets(FieldInfo field) - => !field.IsInitOnly || IsUseableReadOnlyTarget(field.FieldType); - #endregion #region Properties @@ -97,36 +93,9 @@ private static IEnumerable GetProperties(Type targetType, Func property.IsReadable(); - - private static bool OnlyTargets(PropertyInfo property) - { - if (!property.IsReadable()) - { - // TODO: Test coverage: set-only properties - // Ignore set-only properties: - return false; - } - - return property.IsWriteable() || IsUseableReadOnlyTarget(property.PropertyType); - } + private static bool All(PropertyInfo property) => true; - private static bool IsUseableReadOnlyTarget(Type memberType) - { - // Include readonly object type properties (except arrays): - if (memberType.IsArray || memberType.IsSimple()) - { - return false; - } - - if (memberType.IsGenericType() && - (memberType.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>))) - { - return false; - } - - return true; - } + private static bool OnlyGettable(PropertyInfo property) => property.IsReadable(); #endregion diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 009472de8..5c7ef42b7 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -25,8 +25,11 @@ public static IMemberMapperData GetRootMapperData(this IMemberMapperData mapperD public static bool TargetCouldBePopulated(this IMemberMapperData mapperData) => !TargetIsDefinitelyUnpopulated(mapperData); - public static bool TargetIsDefinitelyPopulated(this IMemberMapperData mapperData) - => mapperData.IsRoot && mapperData.RuleSet.RootHasPopulatedTarget; + public static bool TargetIsDefinitelyPopulated(this IBasicMapperData mapperData) + { + return mapperData.RuleSet.RootHasPopulatedTarget && + (mapperData.IsRoot || mapperData.TargetMemberIsUserStruct()); + } public static bool TargetIsDefinitelyUnpopulated(this IMemberMapperData mapperData) => mapperData.Context.IsForNewElement || (mapperData.IsRoot && !mapperData.RuleSet.RootHasPopulatedTarget); @@ -48,11 +51,11 @@ public static Expression GetTargetMemberAccess(this IMemberMapperData mapperData return mapperData.TargetObject; } - var subjectMapperData = mapperData.TargetMember.LeafMember.DeclaringType == mapperData.InstanceVariable.Type + var subjectMapperData = mapperData.TargetMember.LeafMember.DeclaringType == mapperData.TargetInstance.Type ? mapperData : mapperData.Parent; - return mapperData.TargetMember.GetAccess(subjectMapperData.InstanceVariable, mapperData); + return mapperData.TargetMember.GetAccess(subjectMapperData.TargetInstance, mapperData); } public static ExpressionInfoFinder.ExpressionInfo GetExpressionInfoFor( @@ -102,6 +105,14 @@ public static DictionarySourceMember GetDictionarySourceMemberOrNull(this IMembe public static bool TargetMemberIsEnumerableElement(this IBasicMapperData mapperData) => mapperData.TargetMember.LeafMember.IsEnumerableElement(); + [DebuggerStepThrough] + public static bool TargetMemberHasInitAccessibleValue(this IMemberMapperData mapperData) + => mapperData.TargetMember.IsReadable && !mapperData.Context.IsPartOfUserStructMapping; + + [DebuggerStepThrough] + public static bool TargetMemberIsUserStruct(this IBasicMapperData mapperData) + => mapperData.TargetMember.IsComplex && mapperData.TargetMember.Type.IsValueType(); + public static bool TargetMemberEverRecurses(this IMemberMapperData mapperData) { if (mapperData.TargetMember.IsRecursion) @@ -179,7 +190,7 @@ public static Expression GetFallbackCollectionValue(this IMemberMapperData mappe Expression emptyEnumerable; - if (targetMember.IsReadable) + if (mapperData.TargetMemberHasInitAccessibleValue()) { var existingValue = mapperData.GetTargetMemberAccess(); @@ -206,6 +217,18 @@ public static Expression GetFallbackCollectionValue(this IMemberMapperData mappe public static Expression GetValueConversion(this IMemberMapperData mapperData, Expression value, Type targetType) => mapperData.MapperContext.ValueConverters.GetConversion(value, targetType); + public static Expression GetMappingCallbackOrNull( + this IBasicMapperData basicData, + CallbackPosition callbackPosition, + IMemberMapperData mapperData) + { + return mapperData + .MapperContext + .UserConfigurations + .GetCallbackOrNull(callbackPosition, basicData, mapperData); + } + + public static ICollection GetDerivedSourceTypes(this IMemberMapperData mapperData) => GlobalContext.Instance.DerivedTypes.GetTypesDerivedFrom(mapperData.SourceType); @@ -298,7 +321,7 @@ public static Expression GetTypedContextAccess( { if (contextAccess == mapperData.MappingDataObject) { - return mapperData.MappingDataObject; + return GetFinalContextAccess(contextAccess, contextTypes); } if (contextAccess.Type.IsGenericType()) @@ -308,13 +331,36 @@ public static Expression GetTypedContextAccess( if (contextTypes[0].IsAssignableFrom(contextAccessTypes[0]) && contextTypes[1].IsAssignableFrom(contextAccessTypes[1])) { - return contextAccess; + return GetFinalContextAccess(contextAccess, contextTypes, contextAccessTypes); } } return GetAsCall(contextAccess, contextTypes[0], contextTypes[1]); } + private static Expression GetFinalContextAccess( + Expression contextAccess, + Type[] contextTypes, + Type[] contextAccessTypes = null) + { + if ((contextAccessTypes == null) && !contextAccess.Type.IsGenericType()) + { + return contextAccess; + } + + if (contextAccessTypes == null) + { + contextAccessTypes = contextAccess.Type.GetGenericArguments(); + } + + if (contextAccessTypes.None(t => t.IsValueType())) + { + return contextAccess; + } + + return GetAsCall(contextAccess, contextTypes[0], contextTypes[1]); + } + public static Expression GetTargetMemberPopulation(this IMemberMapperData mapperData, Expression value) { return mapperData.TargetMember.GetPopulation(value, mapperData); @@ -323,23 +369,59 @@ public static Expression GetTargetMemberPopulation(this IMemberMapperData mapper public static Expression GetAsCall(this IMemberMapperData mapperData, Type sourceType, Type targetType) => GetAsCall(mapperData.MappingDataObject, sourceType, targetType); - private static readonly MethodInfo _mappingDataAsMethod = typeof(IMappingData).GetMethod("As"); - private static readonly MethodInfo _objectMappingDataAsMethod = typeof(IObjectMappingDataUntyped).GetMethod("As"); - public static Expression GetAsCall(this Expression subject, params Type[] contextTypes) { - var method = (subject.Type == typeof(IMappingData)) - ? _mappingDataAsMethod - : _objectMappingDataAsMethod; + if (subject.Type.IsGenericType() && + subject.Type.GetGenericArguments().SequenceEqual(contextTypes)) + { + return subject; + } + + if (subject.Type == typeof(IMappingData)) + { + return GetAsCall(subject, typeof(IMappingData).GetMethod("As"), contextTypes); + } + + var sourceIsStruct = contextTypes[0].IsValueType(); - return Expression.Call(subject, method.MakeGenericMethod(contextTypes)); + if (sourceIsStruct) + { + return GetAsCall(subject, subject.Type.GetMethod("WithTargetType"), contextTypes[1]); + } + + var targetIsStruct = contextTypes[1].IsValueType(); + + if (targetIsStruct) + { + return GetAsCall(subject, subject.Type.GetMethod("WithSourceType"), contextTypes[0]); + } + + return GetAsCall(subject, typeof(IObjectMappingDataUntyped).GetMethod("As"), contextTypes); + } + + private static Expression GetAsCall( + Expression subject, + MethodInfo asMethod, + params Type[] typeArguments) + { + return Expression.Call(subject, asMethod.MakeGenericMethod(typeArguments)); } - public static Expression GetSourceAccess(this IMemberMapperData mapperData, Expression contextAccess, Type sourceType) - => GetAccess(mapperData, contextAccess, GetSourceAccess, sourceType, mapperData.SourceObject, 0); + public static Expression GetSourceAccess( + this IMemberMapperData mapperData, + Expression contextAccess, + Type sourceType) + { + return GetAccess(mapperData, contextAccess, GetSourceAccess, sourceType, mapperData.SourceObject, 0); + } - public static Expression GetTargetAccess(this IMemberMapperData mapperData, Expression contextAccess, Type targetType) - => GetAccess(mapperData, contextAccess, GetTargetAccess, targetType, mapperData.TargetObject, 1); + public static Expression GetTargetAccess( + this IMemberMapperData mapperData, + Expression contextAccess, + Type targetType) + { + return GetAccess(mapperData, contextAccess, GetTargetAccess, targetType, mapperData.TargetObject, 1); + } private static Expression GetAccess( IMemberMapperData mapperData, diff --git a/AgileMapper/Members/Population/IMemberPopulation.cs b/AgileMapper/Members/Population/IMemberPopulation.cs index e9133d1fb..a5b2d6015 100644 --- a/AgileMapper/Members/Population/IMemberPopulation.cs +++ b/AgileMapper/Members/Population/IMemberPopulation.cs @@ -8,8 +8,6 @@ internal interface IMemberPopulation bool IsSuccessful { get; } - Expression SourceMemberTypeTest { get; } - Expression GetPopulation(); } } \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulation.cs b/AgileMapper/Members/Population/MemberPopulation.cs index 1fa132772..d0c0e4d17 100644 --- a/AgileMapper/Members/Population/MemberPopulation.cs +++ b/AgileMapper/Members/Population/MemberPopulation.cs @@ -31,7 +31,7 @@ private MemberPopulation( public static IMemberPopulation WithRegistration( IChildMemberMappingData mappingData, DataSourceSet dataSources, - Expression populateCondition = null) + Expression populateCondition) { var memberPopulation = WithoutRegistration(mappingData, dataSources, populateCondition); var mapperData = memberPopulation.MapperData; @@ -71,6 +71,9 @@ private static Expression GetPopulateCondition(Expression populateCondition, ICh return Expression.AndAlso(populateCondition, populationGuard); } + public static IMemberPopulation Unmappable(IMemberMapperData mapperData, string reason) + => CreateNullMemberPopulation(mapperData, targetMember => $"No way to populate {targetMember.Name} ({reason})"); + public static IMemberPopulation IgnoredMember(IMemberMapperData mapperData, ConfiguredIgnoredMember configuredIgnore) => CreateNullMemberPopulation(mapperData, configuredIgnore.GetIgnoreMessage); @@ -94,8 +97,6 @@ private static IMemberPopulation CreateNullMemberPopulation( public bool IsSuccessful => _dataSources.HasValue; - public Expression SourceMemberTypeTest => _dataSources.SourceMemberTypeTest; - public Expression GetPopulation() { if (!IsSuccessful) @@ -103,9 +104,11 @@ public Expression GetPopulation() return _dataSources.GetValueExpression(); } - var population = MapperData.TargetMember.IsReadOnly - ? GetReadOnlyMemberPopulation() - : _dataSources.GetPopulationExpression(MapperData); + var population = MapperData.Context.IsPartOfUserStructMapping + ? GetBinding() + : MapperData.TargetMember.IsReadOnly + ? GetReadOnlyMemberPopulation() + : _dataSources.GetPopulationExpression(MapperData); if (_dataSources.Variables.Any()) { @@ -120,22 +123,21 @@ public Expression GetPopulation() return population; } + private Expression GetBinding() + { + var bindingValue = _dataSources.GetValueExpression(); + var binding = MapperData.GetTargetMemberPopulation(bindingValue); + + return binding; + } + private Expression GetReadOnlyMemberPopulation() { + var dataSourcesValue = _dataSources.GetValueExpression(); var targetMemberAccess = MapperData.GetTargetMemberAccess(); var targetMemberNotNull = targetMemberAccess.GetIsNotDefaultComparison(); - var dataSourcesValue = _dataSources.GetValueExpression(); - if (dataSourcesValue.NodeType != ExpressionType.Conditional) - { - return Expression.IfThen(targetMemberNotNull, dataSourcesValue); - } - - var valueTernary = (ConditionalExpression)dataSourcesValue; - var populationTest = Expression.AndAlso(targetMemberNotNull, valueTernary.Test); - var population = Expression.IfThen(populationTest, valueTernary.IfTrue); - - return population; + return Expression.IfThen(targetMemberNotNull, dataSourcesValue); } #region ExcludeFromCodeCoverage @@ -144,6 +146,6 @@ private Expression GetReadOnlyMemberPopulation() #endif #endregion public override string ToString() - => MapperData.TargetMember + " (" + _dataSources.Count() + " data source(s))"; + => $"{MapperData.TargetMember} ({_dataSources.Count()} data source(s))"; } } \ No newline at end of file diff --git a/AgileMapper/Members/QualifiedMember.cs b/AgileMapper/Members/QualifiedMember.cs index 917d407d8..e82daad35 100644 --- a/AgileMapper/Members/QualifiedMember.cs +++ b/AgileMapper/Members/QualifiedMember.cs @@ -306,13 +306,10 @@ public Expression GetQualifiedAccess(IMemberMapperData mapperData) public virtual BlockExpression GetAccessChecked(IMemberMapperData mapperData) => null; public virtual Expression GetHasDefaultValueCheck(IMemberMapperData mapperData) - => GetAccess(mapperData).GetIsDefaultComparison(); - - private Expression GetAccess(IMemberMapperData mapperData) - => GetAccess(mapperData.InstanceVariable, mapperData); + => this.GetAccess(mapperData).GetIsDefaultComparison(); public virtual Expression GetPopulation(Expression value, IMemberMapperData mapperData) - => LeafMember.GetPopulation(mapperData.InstanceVariable, value); + => LeafMember.GetPopulation(mapperData.TargetInstance, value); public virtual void MapCreating(IQualifiedMember sourceMember) { diff --git a/AgileMapper/ObjectPopulation/CallbackPositionExtensions.cs b/AgileMapper/ObjectPopulation/CallbackPositionExtensions.cs index 3bcdd419f..9b83fa348 100644 --- a/AgileMapper/ObjectPopulation/CallbackPositionExtensions.cs +++ b/AgileMapper/ObjectPopulation/CallbackPositionExtensions.cs @@ -1,12 +1,15 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { + using System.Diagnostics; using Members; internal static class CallbackPositionExtensions { + [DebuggerStepThrough] public static bool IsPriorToObjectCreation(this CallbackPosition? position, QualifiedMember targetMember) => IsPriorToObjectCreation(position.GetValueOrDefault(), targetMember); + [DebuggerStepThrough] public static bool IsPriorToObjectCreation(this CallbackPosition position, QualifiedMember targetMember) { if (position != CallbackPosition.Before) diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ClassPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ClassPopulationExpressionFactory.cs new file mode 100644 index 000000000..25b1128c8 --- /dev/null +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ClassPopulationExpressionFactory.cs @@ -0,0 +1,45 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using Members; + using Members.Population; + using static CallbackPosition; + + internal class ClassPopulationExpressionFactory : PopulationExpressionFactoryBase + { + public ClassPopulationExpressionFactory(ComplexTypeConstructionFactory constructionFactory) + : base(constructionFactory) + { + } + + protected override IEnumerable GetPopulationExpressionsFor( + IMemberPopulation memberPopulation, + IObjectMappingData mappingData) + { + var prePopulationCallback = GetPopulationCallbackOrNull(Before, memberPopulation, mappingData); + + if (prePopulationCallback != null) + { + yield return prePopulationCallback; + } + + yield return memberPopulation.GetPopulation(); + + var postPopulationCallback = GetPopulationCallbackOrNull(After, memberPopulation, mappingData); + + if (postPopulationCallback != null) + { + yield return postPopulationCallback; + } + } + + private static Expression GetPopulationCallbackOrNull( + CallbackPosition position, + IMemberPopulation memberPopulation, + IObjectMappingData mappingData) + { + return memberPopulation.MapperData.GetMappingCallbackOrNull(position, mappingData.MapperData); + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index c73aa650f..7bc8742a5 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -45,11 +45,12 @@ public Expression GetNewObjectCreation(IObjectMappingData mappingData) return null; } - var compositeConstruction = new Construction(constructions, key); + var construction = Construction.For(constructions, key); + key.AddSourceMemberTypeTesterIfRequired(); key.MappingData = null; - return compositeConstruction; + return construction; }); if (objectCreation == null) @@ -96,16 +97,26 @@ private static void AddNewingConstruction(ICollection construction { var mapperData = key.MappingData.MapperData; - var greediestAvailableConstructor = mapperData.InstanceVariable.Type + var constructors = mapperData.TargetInstance.Type .GetPublicInstanceConstructors() - .Where(IsNotCopyConstructor) - .Select(ctor => CreateConstructorData(ctor, key)) - .Where(ctor => ctor.CanBeConstructed) - .OrderByDescending(ctor => ctor.NumberOfParameters) - .FirstOrDefault(); + .ToArray(); + + var greediestAvailableConstructor = constructors.Any() + ? constructors + .Where(IsNotCopyConstructor) + .Select(ctor => CreateConstructorData(ctor, key)) + .Where(ctor => ctor.CanBeConstructed) + .OrderByDescending(ctor => ctor.NumberOfParameters) + .FirstOrDefault() + : null; if (greediestAvailableConstructor == null) { + if (constructors.None() && mapperData.TargetMemberIsUserStruct()) + { + constructions.Add(Construction.NewStruct(mapperData.TargetInstance.Type)); + } + return; } @@ -159,7 +170,7 @@ private static ConstructorData CreateConstructorData(ConstructorInfo ctor, Const #region Helper Classes - private class ConstructionKey + private class ConstructionKey : SourceMemberTypeDependentKeyBase { private readonly MappingRuleSet _ruleSet; private readonly IQualifiedMember _sourceMember; @@ -173,8 +184,6 @@ public ConstructionKey(IObjectMappingData mappingData) _targetMember = mappingData.MapperData.TargetMember; } - public IObjectMappingData MappingData { get; set; } - public override bool Equals(object obj) { var otherKey = (ConstructionKey)obj; @@ -182,12 +191,13 @@ public override bool Equals(object obj) // ReSharper disable once PossibleNullReferenceException return (otherKey._ruleSet == _ruleSet) && (otherKey._sourceMember == _sourceMember) && - (otherKey._targetMember == _targetMember); + (otherKey._targetMember == _targetMember) && + SourceHasRequiredTypes(otherKey); } #region ExcludeFromCodeCoverage #if !NET_STANDARD - [ExcludeFromCodeCoverage] + [ExcludeFromCodeCoverage] #endif #endregion public override int GetHashCode() => 0; @@ -237,13 +247,12 @@ public ConstructorData( private class Construction : IConditionallyChainable { private readonly Expression _construction; - private readonly ParameterExpression _mappingDataObject; + private ParameterExpression _mappingDataObject; - public Construction(IList constructions, ConstructionKey key) + private Construction(IList constructions) : this(constructions.ReverseChain()) { UsesMappingDataObjectParameter = constructions.Any(c => c.UsesMappingDataObjectParameter); - _mappingDataObject = key.MappingData.MapperData.MappingDataObject; } public Construction(ConfiguredObjectFactory configuredFactory, IMemberMapperData mapperData) @@ -258,6 +267,32 @@ public Construction(Expression construction, Expression condition = null) Condition = condition; } + #region Factory Methods + + public static Construction NewStruct(Type type) + { + var parameterlessNew = Expression.New(type); + + return new Construction(parameterlessNew); + } + + public static Construction For(IList constructions, ConstructionKey key) + { + var construction = constructions.HasOne() + ? constructions.First() + : new Construction(constructions); + + return construction.With(key); + } + + private Construction With(ConstructionKey key) + { + _mappingDataObject = key.MappingData.MapperData.MappingDataObject; + return this; + } + + #endregion + public Expression PreCondition => null; public bool IsUnconditional => Condition == null; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs index 7853747ee..4d5e8c2d9 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs @@ -1,24 +1,29 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { - using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +#if NET_STANDARD using System.Reflection; +#endif using Extensions; using Members; - using Members.Population; + using NetStandardPolyfills; using ReadableExpressions; using ReadableExpressions.Extensions; internal class ComplexTypeMappingExpressionFactory : MappingExpressionFactoryBase { private readonly ComplexTypeConstructionFactory _constructionFactory; + private readonly PopulationExpressionFactoryBase _structPopulationFactory; + private readonly PopulationExpressionFactoryBase _classPopulationFactory; private readonly IEnumerable _shortCircuitFactories; public ComplexTypeMappingExpressionFactory(MapperContext mapperContext) { _constructionFactory = new ComplexTypeConstructionFactory(mapperContext); + _structPopulationFactory = new StructPopulationExpressionFactory(_constructionFactory); + _classPopulationFactory = new ClassPopulationExpressionFactory(_constructionFactory); _shortCircuitFactories = new[] { @@ -65,7 +70,6 @@ protected override IEnumerable GetShortCircuitReturns(GotoExpression } var alreadyMappedShortCircuit = GetAlreadyMappedObjectShortCircuitOrNull(mapperData); - if (alreadyMappedShortCircuit != null) { yield return alreadyMappedShortCircuit; @@ -84,6 +88,11 @@ private static bool SourceObjectCouldBeNull(IMemberMapperData mapperData) return false; } + if (mapperData.SourceType.IsValueType()) + { + return false; + } + if (mapperData.TargetMemberIsEnumerableElement()) { return !mapperData.HasSameSourceAsParent(); @@ -105,11 +114,11 @@ private static Expression GetAlreadyMappedObjectShortCircuitOrNull(ObjectMapperD mapperData.EntryPointMapperData.MappingDataObject, tryGetMethod.MakeGenericMethod(mapperData.SourceType, mapperData.TargetType), mapperData.SourceObject, - mapperData.InstanceVariable); + mapperData.TargetInstance); var ifTryGetReturn = Expression.IfThen( tryGetCall, - Expression.Return(mapperData.ReturnLabelTarget, mapperData.InstanceVariable)); + Expression.Return(mapperData.ReturnLabelTarget, mapperData.TargetInstance)); return ifTryGetReturn; } @@ -127,132 +136,14 @@ protected override Expression GetDerivedTypeMappings(IObjectMappingData mappingD protected override IEnumerable GetObjectPopulation(IObjectMappingData mappingData) { - var mapperData = mappingData.MapperData; - var preCreationCallback = GetCreationCallbackOrNull(CallbackPosition.Before, mapperData); - var postCreationCallback = GetCreationCallbackOrNull(CallbackPosition.After, mapperData); - var populationsAndCallbacks = GetPopulationsAndCallbacks(mappingData).ToArray(); - - if (preCreationCallback != null) - { - yield return preCreationCallback; - } - - var assignCreatedObject = postCreationCallback != null; - var hasMemberPopulations = MemberPopulationsExist(populationsAndCallbacks); - - var instanceVariableValue = TargetObjectResolutionFactory.GetObjectResolution( - md => _constructionFactory.GetNewObjectCreation(md), - mappingData, - assignCreatedObject, - hasMemberPopulations: hasMemberPopulations); - - var instanceVariableAssignment = mapperData.InstanceVariable.AssignTo(instanceVariableValue); - yield return instanceVariableAssignment; - - if (postCreationCallback != null) - { - yield return postCreationCallback; - } - - var registrationCall = GetObjectRegistrationCallOrNull(mapperData); - if (registrationCall != null) - { - yield return registrationCall; - } - - foreach (var population in populationsAndCallbacks) - { - yield return population; - } - } - - private static Expression GetCreationCallbackOrNull(CallbackPosition callbackPosition, IMemberMapperData mapperData) - => mapperData.MapperContext.UserConfigurations.GetCreationCallbackOrNull(callbackPosition, mapperData); - - #region Object Registration - - private static readonly MethodInfo _registerMethod = typeof(IObjectMappingDataUntyped).GetMethod("Register"); - - private static Expression GetObjectRegistrationCallOrNull(ObjectMapperData mapperData) - { - if (!mapperData.MappedObjectCachingNeeded || mapperData.TargetTypeWillNotBeMappedAgain) - { - return null; - } - - return Expression.Call( - mapperData.EntryPointMapperData.MappingDataObject, - _registerMethod.MakeGenericMethod(mapperData.SourceType, mapperData.TargetType), - mapperData.SourceObject, - mapperData.InstanceVariable); - } - - #endregion - - private static IEnumerable GetPopulationsAndCallbacks(IObjectMappingData mappingData) - { - var sourceMemberTypeTests = new List(); - - foreach (var memberPopulation in MemberPopulationFactory.Default.Create(mappingData)) - { - if (!memberPopulation.IsSuccessful) - { - yield return memberPopulation.GetPopulation(); - continue; - } - - var prePopulationCallback = GetPopulationCallbackOrNull(CallbackPosition.Before, memberPopulation, mappingData); - - if (prePopulationCallback != null) - { - yield return prePopulationCallback; - } - - yield return memberPopulation.GetPopulation(); - - var postPopulationCallback = GetPopulationCallbackOrNull(CallbackPosition.After, memberPopulation, mappingData); + var expressionFactory = mappingData.MapperData.TargetMemberIsUserStruct() + ? _structPopulationFactory + : _classPopulationFactory; - if (postPopulationCallback != null) - { - yield return postPopulationCallback; - } - - if (memberPopulation.SourceMemberTypeTest != null) - { - sourceMemberTypeTests.Add(memberPopulation.SourceMemberTypeTest); - } - } - - CreateSourceMemberTypeTesterIfRequired(sourceMemberTypeTests, mappingData); - } - - private static Expression GetPopulationCallbackOrNull( - CallbackPosition position, - IMemberPopulation memberPopulation, - IObjectMappingData mappingData) - { - return GetMappingCallbackOrNull(position, memberPopulation.MapperData, mappingData.MapperData); - } - - private static void CreateSourceMemberTypeTesterIfRequired( - IList typeTests, - IObjectMappingData mappingData) - { - if (typeTests.None()) - { - return; - } - - var typeTest = typeTests.AndTogether(); - var typeTestLambda = Expression.Lambda>(typeTest, Parameters.MappingData); - - mappingData.MapperKey.AddSourceMemberTypeTester(typeTestLambda.Compile()); + return expressionFactory.GetPopulation(mappingData); } - private static bool MemberPopulationsExist(IEnumerable populationsAndCallbacks) - => populationsAndCallbacks.Any(population => population.NodeType != ExpressionType.Constant); - - protected override Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.InstanceVariable; + protected override Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.TargetInstance; public override void Reset() => _constructionFactory.Reset(); } diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs new file mode 100644 index 000000000..22c6ff59b --- /dev/null +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -0,0 +1,118 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes +{ + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Extensions; + using Members; + using Members.Population; + using static CallbackPosition; + + internal abstract class PopulationExpressionFactoryBase + { + private readonly ComplexTypeConstructionFactory _constructionFactory; + + protected PopulationExpressionFactoryBase(ComplexTypeConstructionFactory constructionFactory) + { + _constructionFactory = constructionFactory; + } + + public IEnumerable GetPopulation(IObjectMappingData mappingData) + { + var mapperData = mappingData.MapperData; + var preCreationCallback = GetCreationCallbackOrNull(Before, mapperData); + var postCreationCallback = GetCreationCallbackOrNull(After, mapperData); + var populationsAndCallbacks = GetPopulationsAndCallbacks(mappingData).ToList(); + + yield return preCreationCallback; + + if (mapperData.Context.UseLocalVariable) + { + var assignCreatedObject = postCreationCallback != null; + + yield return GetLocalVariableInstantiation(assignCreatedObject, populationsAndCallbacks, mappingData); + } + + yield return postCreationCallback; + + yield return GetObjectRegistrationCallOrNull(mapperData); + + foreach (var population in populationsAndCallbacks) + { + yield return population; + } + + mappingData.MapperKey.AddSourceMemberTypeTesterIfRequired(mappingData); + } + + private static Expression GetCreationCallbackOrNull(CallbackPosition callbackPosition, IMemberMapperData mapperData) + => mapperData.MapperContext.UserConfigurations.GetCreationCallbackOrNull(callbackPosition, mapperData); + + private IEnumerable GetPopulationsAndCallbacks(IObjectMappingData mappingData) + { + foreach (var memberPopulation in MemberPopulationFactory.Default.Create(mappingData)) + { + if (!memberPopulation.IsSuccessful) + { + yield return memberPopulation.GetPopulation(); + continue; + } + + foreach (var expression in GetPopulationExpressionsFor(memberPopulation, mappingData)) + { + yield return expression; + } + } + } + + protected abstract IEnumerable GetPopulationExpressionsFor( + IMemberPopulation memberPopulation, + IObjectMappingData mappingData); + + private Expression GetLocalVariableInstantiation( + bool assignCreatedObject, + IList memberPopulations, + IObjectMappingData mappingData) + { + var localVariableValue = TargetObjectResolutionFactory.GetObjectResolution( + GetNewObjectCreation, + mappingData, + memberPopulations, + assignCreatedObject); + + var localVariableAssignment = mappingData.MapperData.LocalVariable.AssignTo(localVariableValue); + + return localVariableAssignment; + } + + protected virtual Expression GetNewObjectCreation( + IObjectMappingData mappingData, + IList memberPopulations) + { + return _constructionFactory.GetNewObjectCreation(mappingData); + } + + #region Object Registration + + private static Expression GetObjectRegistrationCallOrNull(ObjectMapperData mapperData) + { + if (!mapperData.MappedObjectCachingNeeded || mapperData.TargetTypeWillNotBeMappedAgain) + { + return null; + } + + var registerMethod = typeof(IObjectMappingDataUntyped) + .GetMethod("Register") + .MakeGenericMethod(mapperData.SourceType, mapperData.TargetType); + + return Expression.Call( + mapperData.EntryPointMapperData.MappingDataObject, + registerMethod, + mapperData.SourceObject, + mapperData.TargetInstance); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs new file mode 100644 index 000000000..8ab09bbdf --- /dev/null +++ b/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs @@ -0,0 +1,110 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using Extensions; + using Members.Population; + + internal class StructPopulationExpressionFactory : PopulationExpressionFactoryBase + { + public StructPopulationExpressionFactory(ComplexTypeConstructionFactory constructionFactory) + : base(constructionFactory) + { + } + + protected override IEnumerable GetPopulationExpressionsFor( + IMemberPopulation memberPopulation, + IObjectMappingData mappingData) + { + yield return memberPopulation.GetPopulation(); + } + + protected override Expression GetNewObjectCreation(IObjectMappingData mappingData, IList memberPopulations) + { + var objectCreation = base.GetNewObjectCreation(mappingData, memberPopulations); + + if (memberPopulations.None()) + { + return objectCreation; + } + + var memberBindings = GetMemberBindingsFrom(memberPopulations); + + if (memberBindings.None()) + { + return objectCreation; + } + + var objectNewings = NewExpressionFinder.FindIn(objectCreation); + var newingReplacements = new Dictionary(objectNewings.Count); + + foreach (var objectNewing in objectNewings) + { + var objectInit = Expression.MemberInit(objectNewing, memberBindings); + + newingReplacements.Add(objectNewing, objectInit); + } + + var fullObjectInit = objectCreation.Replace(newingReplacements); + + return fullObjectInit; + } + + private static ICollection GetMemberBindingsFrom(IList memberPopulations) + { + var memberBindings = new List(memberPopulations.Count); + + for (var i = memberPopulations.Count - 1; i >= 0; --i) + { + var population = memberPopulations[i]; + + if (population.NodeType != ExpressionType.Assign) + { + continue; + } + + var assignment = (BinaryExpression)population; + var assignedMember = (MemberExpression)assignment.Left; + var memberBinding = Expression.Bind(assignedMember.Member, assignment.Right); + + memberBindings.Add(memberBinding); + memberPopulations.RemoveAt(i); + } + + return memberBindings; + } + + #region Helper Class + + private class NewExpressionFinder : ExpressionVisitor + { + private readonly List _newings; + + private NewExpressionFinder() + { + _newings = new List(); + } + + public static ICollection FindIn(Expression objectCreation) + { + var finder = new NewExpressionFinder(); + + finder.Visit(objectCreation); + + return finder._newings; + } + + protected override Expression VisitNew(NewExpression newing) + { + if (!_newings.Contains(newing)) + { + _newings.Add(newing); + } + + return base.VisitNew(newing); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs index 8d9484215..922fa1d46 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs @@ -1,6 +1,8 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System; + using System.Collections.Generic; + using System.Linq; using System.Linq.Expressions; using Extensions; using Members; @@ -8,11 +10,24 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes internal static class TargetObjectResolutionFactory { public static Expression GetObjectResolution( - Func constructionFactory, + Expression construction, IObjectMappingData mappingData, + bool assignTargetObject = false) + { + return GetObjectResolution( + (md, mps) => construction, + mappingData, + null, + false, + assignTargetObject); + } + + public static Expression GetObjectResolution( + Func, Expression> constructionFactory, + IObjectMappingData mappingData, + IList memberPopulations, bool assignCreatedObject = false, - bool assignTargetObject = false, - bool hasMemberPopulations = true) + bool assignTargetObject = false) { var mapperData = mappingData.MapperData; @@ -21,7 +36,7 @@ public static Expression GetObjectResolution( return mapperData.TargetObject; } - var objectValue = constructionFactory.Invoke(mappingData); + var objectValue = constructionFactory.Invoke(mappingData, memberPopulations); if (objectValue == null) { @@ -31,24 +46,23 @@ public static Expression GetObjectResolution( return mapperData.TargetObject; } - if (UseNullFallbackValue(mapperData, objectValue, hasMemberPopulations)) + if (UseNullFallbackValue(mapperData, objectValue, memberPopulations)) { objectValue = mapperData.TargetMember.Type.ToDefaultExpression(); - - assignCreatedObject = - assignTargetObject = - mapperData.Context.UsesMappingDataObjectAsParameter = false; + mapperData.Context.UsesMappingDataObjectAsParameter = false; } - - if (assignCreatedObject) - { - mapperData.Context.UsesMappingDataObjectAsParameter = true; - objectValue = mapperData.CreatedObject.AssignTo(objectValue); - } - - if (assignTargetObject || mapperData.Context.UsesMappingDataObjectAsParameter) + else { - objectValue = mapperData.TargetObject.AssignTo(objectValue); + if (assignCreatedObject) + { + mapperData.Context.UsesMappingDataObjectAsParameter = true; + objectValue = mapperData.CreatedObject.AssignTo(objectValue); + } + + if (assignTargetObject || mapperData.Context.UsesMappingDataObjectAsParameter) + { + objectValue = mapperData.TargetObject.AssignTo(objectValue); + } } objectValue = AddExistingTargetCheckIfAppropriate(objectValue, mappingData); @@ -59,11 +73,16 @@ public static Expression GetObjectResolution( private static bool UseNullFallbackValue( IMemberMapperData mapperData, Expression objectConstruction, - bool hasMemberPopulations) + IList memberPopulations) { - if (hasMemberPopulations || - (objectConstruction.NodeType != ExpressionType.New) || - mapperData.SourceMember.Matches(mapperData.TargetMember)) + if (memberPopulations == null) + { + return false; + } + + if ((objectConstruction.NodeType != ExpressionType.New) || + MemberPopulationsExist(memberPopulations) || + mapperData.SourceMember.Matches(mapperData.TargetMember)) { return false; } @@ -73,14 +92,22 @@ private static bool UseNullFallbackValue( return objectNewing.Arguments.None(); } + private static bool MemberPopulationsExist(IEnumerable populationsAndCallbacks) + => populationsAndCallbacks.Any(population => population.NodeType != ExpressionType.Constant); + private static Expression AddExistingTargetCheckIfAppropriate(Expression value, IObjectMappingData mappingData) { - if (mappingData.MapperData.TargetCouldBePopulated()) + var mapperData = mappingData.MapperData; + + if (mapperData.TargetMemberIsUserStruct() || + mapperData.TargetIsDefinitelyUnpopulated()) { - return Expression.Coalesce(mappingData.MapperData.TargetObject, value); + return value; } - return value; + return Expression.Coalesce(mapperData.TargetObject, value); } + + } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs index 7011cf47e..4db1c1995 100644 --- a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs @@ -208,7 +208,7 @@ private static Expression GetClonedDictionaryAssignment(IMemberMapperData mapper var cloneConstructor = GetDictionaryCloneConstructor(mapperData.TargetMember.Type); var comparer = Expression.Property(mapperData.SourceObject, "Comparer"); var cloneDictionary = Expression.New(cloneConstructor, mapperData.SourceObject, comparer); - var assignment = mapperData.InstanceVariable.AssignTo(cloneDictionary); + var assignment = mapperData.TargetInstance.AssignTo(cloneDictionary); return assignment; } @@ -270,19 +270,27 @@ private static bool UseParameterlessConstructor( private static Expression GetParameterlessDictionaryAssignment(IObjectMappingData mappingData) { - var newDictionary = mappingData.MapperData.TargetType.GetEmptyInstanceCreation(); + var valueType = mappingData.MapperData.EnumerablePopulationBuilder.TargetTypeHelper.ElementType; + var newDictionary = mappingData.MapperData.TargetType.GetEmptyInstanceCreation(valueType); return GetDictionaryAssignment(newDictionary, mappingData); } private static Expression GetDictionaryAssignment(Expression value, IObjectMappingData mappingData) { + var mapperData = mappingData.MapperData; + + if (mapperData.TargetMember.IsReadOnly) + { + return null; + } + var valueResolution = TargetObjectResolutionFactory.GetObjectResolution( - md => value, + value, mappingData, - assignTargetObject: mappingData.MapperData.HasMapperFuncs); + mapperData.HasMapperFuncs); - return mappingData.MapperData.InstanceVariable.AssignTo(valueResolution); + return mapperData.TargetInstance.AssignTo(valueResolution); } private Expression GetDictionaryPopulation(IObjectMappingData mappingData) @@ -324,6 +332,6 @@ private static Expression GetEnumerableToDictionaryMapping(IObjectMappingData ma } protected override Expression GetReturnValue(ObjectMapperData mapperData) - => mapperData.InstanceVariable; + => mapperData.TargetInstance; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index dab4f00d5..d1ed5f30e 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -63,11 +63,6 @@ public EnumerablePopulationBuilder(ObjectMapperData mapperData) public static implicit operator BlockExpression(EnumerablePopulationBuilder builder) { - if (builder._populationExpressions.None()) - { - return null; - } - var variables = new List(2); if (builder._sourceVariable != null) @@ -128,6 +123,12 @@ private bool DetermineIfElementsAreIdentifiable() return false; } + if ((Context.SourceElementType.IsValueType()) || + (Context.TargetElementType.IsValueType())) + { + return false; + } + var typeIdsCache = MapperData.MapperContext.Cache.CreateScoped(); var sourceElementId = GetIdentifierOrNull(Context.SourceElementType, _sourceElementParameter, MapperData, typeIdsCache); @@ -291,6 +292,11 @@ public void AssignTargetVariable() private Expression GetTargetVariableValue() { + if (!MapperData.TargetMemberHasInitAccessibleValue()) + { + return TargetTypeHelper.GetEmptyInstanceCreation(); + } + if (_sourceAdapter.UseReadOnlyTargetWrapper) { return GetCopyIntoWrapperConstruction(); @@ -325,9 +331,7 @@ private Expression GetTargetVariableValue() return nonNullTargetVariableValue; } - var nullTargetVariableType = nonNullTargetVariableValue.Type.IsInterface() - ? TargetTypeHelper.ListType - : nonNullTargetVariableValue.Type; + var nullTargetVariableType = GetNullTargetVariableType(nonNullTargetVariableValue.Type); var nullTargetVariableValue = SourceTypeHelper.IsEnumerableInterface || TargetTypeHelper.IsCollection ? Expression.New(nullTargetVariableType) @@ -383,6 +387,13 @@ private Expression GetCopyIntoListConstruction() MapperData.TargetObject); } + private Type GetNullTargetVariableType(Type nonNullTargetVariableType) + { + return nonNullTargetVariableType.IsInterface() + ? TargetTypeHelper.ListType + : nonNullTargetVariableType; + } + private bool TargetCouldBeUnusable() { if (MapperData.TargetMember.LeafMember.IsWriteable) diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index 63a3a2dc4..c18fd1b3c 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -5,6 +5,8 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Collections.ObjectModel; using System.Linq.Expressions; using Extensions; + using NetStandardPolyfills; + #if NET_STANDARD using System.Reflection; #endif @@ -67,6 +69,16 @@ private Type GetEnumerableType(ref Type typeField, Type openGenericEnumerableTyp public Type WrapperType => typeof(ReadOnlyCollectionWrapper<>).MakeGenericType(ElementType); + public Expression GetEmptyInstanceCreation() + { + if (IsReadOnly || EnumerableType.IsInterface()) + { + return Expression.New(ListType); + } + + return EnumerableType.GetEmptyInstanceCreation(ElementType); + } + public Expression GetWrapperConstruction(Expression existingItems, Expression newItemsCount) { // ReSharper disable once AssignNullToNotNullAttribute diff --git a/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs index 50f8b0ed7..dd7b473f0 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/IPopulationLoopData.cs @@ -26,6 +26,7 @@ public static Expression BuildPopulationLoop( 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); diff --git a/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs b/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs index d9995e98c..0a1c21c9d 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs @@ -18,7 +18,8 @@ public class ReadOnlyCollectionWrapper : IList { private static readonly ReadOnlyCollection _emptyReadOnlyCollection = new ReadOnlyCollection(Enumerable.EmptyArray); - private readonly T[] _items; + private readonly int _numberOfNewItems; + private T[] _items; private int _index; /// @@ -30,6 +31,8 @@ public class ReadOnlyCollectionWrapper : IList /// The number of new items to be added to the existing items. public ReadOnlyCollectionWrapper(IList existingItems, int numberOfNewItems) { + _numberOfNewItems = numberOfNewItems; + var hasExistingItems = existingItems != null; if (hasExistingItems) @@ -182,12 +185,11 @@ public void Add(T item) /// /// Removes all items from the collection. /// - #region ExcludeFromCodeCoverage -#if !NET_STANDARD - [ExcludeFromCodeCoverage] -#endif - #endregion - public void Clear() => ((ICollection)_items).Clear(); + public void Clear() + { + _items = new T[_numberOfNewItems]; + _index = 0; + } #endregion diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs index 2a8096ec7..fe4464c9f 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/SourceInstanceDictionaryAdapter.cs @@ -25,7 +25,8 @@ public SourceInstanceDictionaryAdapter( public override Expression GetSourceValue() { - var emptyTarget = DictionaryVariables.SourceMember.ValueType.GetEmptyInstanceCreation(); + var elementType = DictionaryVariables.SourceMember.EntryMember.ElementType; + var emptyTarget = DictionaryVariables.SourceMember.ValueType.GetEmptyInstanceCreation(elementType); var returnLabel = Expression.Label(emptyTarget.Type, "Return"); var returnEmpty = Expression.Return(returnLabel, emptyTarget); diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs index 192d69b1c..fe453473e 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/SourceObjectDictionaryAdapter.cs @@ -23,7 +23,7 @@ public SourceObjectDictionaryAdapter( _instanceDictionaryAdapter = new SourceInstanceDictionaryAdapter(sourceMember, builder); var targetEnumerableType = TargetTypeHelper.EnumerableInterfaceType; - _emptyTarget = targetEnumerableType.GetEmptyInstanceCreation(); + _emptyTarget = targetEnumerableType.GetEmptyInstanceCreation(TargetTypeHelper.ElementType); } public override Expression GetSourceValue() diff --git a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs index eb59c6e21..b3a675ec7 100644 --- a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs +++ b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs @@ -28,7 +28,7 @@ private static Expression GetValue(IMemberMapperData mapperData) : mapperData.GetFallbackCollectionValue(); } - if (mapperData.TargetMember.IsReadable) + if (mapperData.TargetMember.IsReadable && !mapperData.TargetMemberIsUserStruct()) { return mapperData.GetTargetMemberAccess(); } diff --git a/AgileMapper/ObjectPopulation/IObjectMappingData.cs b/AgileMapper/ObjectPopulation/IObjectMappingData.cs index 54688afb8..635816031 100644 --- a/AgileMapper/ObjectPopulation/IObjectMappingData.cs +++ b/AgileMapper/ObjectPopulation/IObjectMappingData.cs @@ -93,5 +93,29 @@ TTargetElement Map( TSourceElement sourceElement, TTargetElement targetElement, int enumerableIndex); + + /// + /// Gets the typed as a + /// . + /// + /// The type of source object being mapped in the current context. + /// + /// The typed as a + /// . + /// + IObjectMappingData WithSourceType() + where TNewSource : class; + + /// + /// Gets the typed as a + /// . + /// + /// The type of target object being mapped in the current context. + /// + /// The typed as a + /// . + /// + IObjectMappingData WithTargetType() + where TNewTarget : class; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/IObjectMappingDataFactoryBridge.cs b/AgileMapper/ObjectPopulation/IObjectMappingDataFactoryBridge.cs index f14ff826d..1676db6cf 100644 --- a/AgileMapper/ObjectPopulation/IObjectMappingDataFactoryBridge.cs +++ b/AgileMapper/ObjectPopulation/IObjectMappingDataFactoryBridge.cs @@ -34,8 +34,7 @@ public interface IObjectMappingDataFactoryBridge /// An object representing the parent . /// /// An element instance. - object ForElement(object membersSource, object parent) - where TSource : class where TTarget : class; + object ForElement(object membersSource, object parent); /// /// Creates an instance. diff --git a/AgileMapper/ObjectPopulation/MapperDataContext.cs b/AgileMapper/ObjectPopulation/MapperDataContext.cs index 3b3573298..146101e04 100644 --- a/AgileMapper/ObjectPopulation/MapperDataContext.cs +++ b/AgileMapper/ObjectPopulation/MapperDataContext.cs @@ -14,21 +14,46 @@ public MapperDataContext(IMemberMapperData childMapperData) : this( childMapperData.Parent, IsForStandaloneMapping(childMapperData), - childMapperData.Parent.Context.IsForDerivedType) + childMapperData.Parent.Context.IsForDerivedType, + childMapperData) { } private static bool IsForStandaloneMapping(IBasicMapperData mapperData) => mapperData.SourceType.RuntimeTypeNeeded() || mapperData.TargetType.RuntimeTypeNeeded(); - public MapperDataContext( + public MapperDataContext(ObjectMapperData mapperData, bool isStandalone, bool isForDerivedType) + : this(mapperData, isStandalone, isForDerivedType, mapperData) + { + } + + private MapperDataContext( ObjectMapperData mapperData, bool isStandalone, - bool isForDerivedType) + bool isForDerivedType, + IBasicMapperData basicMapperData) { _mapperData = mapperData; IsStandalone = isStandalone; IsForDerivedType = isForDerivedType; + UseLocalVariable = isForDerivedType || ShouldUseLocalVariable(basicMapperData); + } + + private static bool ShouldUseLocalVariable(IBasicMapperData mapperData) + { + if (mapperData.TargetMember.IsSimple) + { + return false; + } + + if (mapperData.TargetMember.IsComplex && + (mapperData.TargetMember.IsReadOnly || mapperData.TargetIsDefinitelyPopulated()) && + !mapperData.TargetMemberIsUserStruct()) + { + return false; + } + + return true; } public bool IsStandalone { get; } @@ -52,18 +77,34 @@ public void SubMappingNeeded() private void BubbleMappingNeededToParent() { - if (_mapperData.IsRoot) + if (!_mapperData.IsRoot) { - return; + _mapperData.Parent.Context.SubMappingNeeded(); } + } + + public bool UseLocalVariable { get; } - if (_mapperData.TargetMemberIsEnumerableElement()) + public bool UseMappingTryCatch => _mapperData.IsRoot || !IsPartOfUserStructMapping; + + public bool IsPartOfUserStructMapping + { + get { - _mapperData.Parent.Context.SubMappingNeeded(); - return; - } + var mapperData = _mapperData; - _mapperData.Parent.Context.SubMappingNeeded(); + while (mapperData != null) + { + if (mapperData.TargetMemberIsUserStruct()) + { + return true; + } + + mapperData = mapperData.Parent; + } + + return false; + } } public bool UsesMappingDataObjectAsParameter diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index d0c672294..4d6657841 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -10,6 +10,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation #endif using Members; using NetStandardPolyfills; + using static CallbackPosition; internal abstract class MappingExpressionFactoryBase { @@ -41,13 +42,17 @@ public Expression Create(IObjectMappingData mappingData) mappingExpressions.AddUnlessNullOrEmpty(derivedTypeMappings); mappingExpressions.AddUnlessNullOrEmpty(mappingExtras.PreMappingCallback); - mappingExpressions.AddRange(GetObjectPopulation(mappingData)); + mappingExpressions.AddRange(GetObjectPopulation(mappingData).WhereNotNull()); mappingExpressions.AddUnlessNullOrEmpty(mappingExtras.PostMappingCallback); var mappingBlock = GetMappingBlock(mappingExpressions, mappingExtras); - var mappingBlockWithTryCatch = WrapInTryCatch(mappingBlock, mapperData); - return mappingBlockWithTryCatch; + if (mapperData.Context.UseMappingTryCatch) + { + mappingBlock = WrapInTryCatch(mappingBlock, mapperData); + } + + return mappingBlock; } protected abstract bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock); @@ -73,8 +78,8 @@ private bool MappingAlwaysBranchesToDerivedType(IObjectMappingData mappingData, private static MappingExtras GetMappingExtras(ObjectMapperData mapperData) { var basicMapperData = mapperData.WithNoTargetMember(); - var preMappingCallback = GetMappingCallbackOrNull(CallbackPosition.Before, basicMapperData, mapperData); - var postMappingCallback = GetMappingCallbackOrNull(CallbackPosition.After, basicMapperData, mapperData); + var preMappingCallback = basicMapperData.GetMappingCallbackOrNull(Before, mapperData); + var postMappingCallback = basicMapperData.GetMappingCallbackOrNull(After, mapperData); var mapToNullCondition = GetMapToNullConditionOrNull(mapperData); return new MappingExtras( @@ -84,14 +89,6 @@ private static MappingExtras GetMappingExtras(ObjectMapperData mapperData) mapToNullCondition); } - protected static Expression GetMappingCallbackOrNull( - CallbackPosition callbackPosition, - IBasicMapperData basicData, - IMemberMapperData mapperData) - { - return mapperData.MapperContext.UserConfigurations.GetCallbackOrNull(callbackPosition, basicData, mapperData); - } - private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperData) => mapperData.MapperContext.UserConfigurations.GetMapToNullConditionOrNull(mapperData); @@ -99,10 +96,15 @@ private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperDa private Expression GetMappingBlock(IList mappingExpressions, MappingExtras mappingExtras) { - Expression returnExpression; - var mapperData = mappingExtras.MapperData; + if (mappingExpressions.None()) + { + goto CreateFullMappingBlock; + } + + Expression returnExpression; + AdjustForSingleExpressionBlockIfApplicable(ref mappingExpressions); if (mappingExpressions[0].NodeType != ExpressionType.Block) @@ -112,11 +114,16 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping return GetReturnExpression(mappingExpressions[0], mappingExtras); } - var objectAssignment = mappingExpressions.First(exp => exp.NodeType == ExpressionType.Assign); + if (!mapperData.Context.UseLocalVariable) + { + goto CreateFullMappingBlock; + } - if (mappingExpressions.Last() == objectAssignment) + var localVariableAssignment = mappingExpressions.First(exp => exp.NodeType == ExpressionType.Assign); + + if (mappingExpressions.Last() == localVariableAssignment) { - var assignedValue = ((BinaryExpression)objectAssignment).Right; + var assignedValue = ((BinaryExpression)localVariableAssignment).Right; returnExpression = GetReturnExpression(assignedValue, mappingExtras); if (mappingExpressions.HasOne()) @@ -130,11 +137,15 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping } } + CreateFullMappingBlock: + returnExpression = GetReturnExpression(GetReturnValue(mapperData), mappingExtras); mappingExpressions.Add(mapperData.GetReturnLabel(returnExpression)); - var mappingBlock = Expression.Block(new[] { mapperData.InstanceVariable }, mappingExpressions); + var mappingBlock = mapperData.Context.UseLocalVariable + ? Expression.Block(new[] { mapperData.LocalVariable }, mappingExpressions) + : Expression.Block(mappingExpressions); return mappingBlock; } @@ -148,9 +159,9 @@ private static void AdjustForSingleExpressionBlockIfApplicable(ref IList(block.Expressions); } } diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index 208c2e42a..de48a49c8 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -167,11 +167,6 @@ public static Expression GetInlineMappingBlock( { var mapper = mappingData.Mapper; - if (mapper.MappingExpression.NodeType != ExpressionType.Try) - { - return mapper.MappingExpression; - } - if (mapper.MapperData.Context.UsesMappingDataObject) { return UseLocalSourceValueVariable( diff --git a/AgileMapper/ObjectPopulation/MemberPopulationFactory.cs b/AgileMapper/ObjectPopulation/MemberPopulationFactory.cs index 6e487167f..8de7a6d2b 100644 --- a/AgileMapper/ObjectPopulation/MemberPopulationFactory.cs +++ b/AgileMapper/ObjectPopulation/MemberPopulationFactory.cs @@ -26,17 +26,20 @@ public MemberPopulationFactory(Func Create(IObjectMappingData mappingData) { - var memberPopulations = _targetMembersFactory + return _targetMembersFactory .Invoke(mappingData.MapperData) .Select(tm => Create(tm, mappingData)); - - return memberPopulations; } private static IMemberPopulation Create(QualifiedMember targetMember, IObjectMappingData mappingData) { var childMapperData = new ChildMemberMapperData(targetMember, mappingData.MapperData); + if (targetMember.IsUnmappable(out var reason)) + { + return MemberPopulation.Unmappable(childMapperData, reason); + } + if (TargetMemberIsUnconditionallyIgnored( childMapperData, out var configuredIgnore, diff --git a/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs b/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs index 4edcb3763..3f3a0dda3 100644 --- a/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs @@ -8,6 +8,7 @@ using Configuration; using Extensions; using Members; + using NetStandardPolyfills; internal class ObjectCreationCallbackFactory : MappingCallbackFactory { @@ -30,7 +31,7 @@ protected override Expression GetConditionOrNull(IMemberMapperData mapperData, C { var condition = base.GetConditionOrNull(mapperData, position); - if (CallbackPosition != CallbackPosition.After) + if ((CallbackPosition != CallbackPosition.After) || mapperData.TargetMemberIsUserStruct()) { return condition; } diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index 5aca906cc..b9ad258e2 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -26,6 +26,7 @@ internal class ObjectMapperData : BasicMapperData, IMemberMapperData private readonly MethodInfo _mapElementMethod; private readonly Dictionary _dataSourcesByTargetMemberName; private ObjectMapperData _entryPointMapperData; + private Expression _targetInstance; private ParameterExpression _instanceVariable; private bool? _mappedObjectCachingNeeded; @@ -367,7 +368,13 @@ private bool IsMappedObjectCachingNeeded() public Expression EnumerableIndex { get; } - public ParameterExpression InstanceVariable + public Expression TargetInstance + => _targetInstance ?? (_targetInstance = GetTargetInstance()); + + private Expression GetTargetInstance() + => Context.UseLocalVariable ? LocalVariable : TargetObject; + + public ParameterExpression LocalVariable => _instanceVariable ?? (_instanceVariable = CreateInstanceVariable()); private ParameterExpression CreateInstanceVariable() @@ -456,7 +463,7 @@ public MethodCallExpression GetMapCall( MappingDataObject, _mapChildMethod.MakeGenericMethod(sourceObject.Type, targetMember.Type), sourceObject, - targetMember.GetAccess(InstanceVariable, this), + targetMember.GetAccess(this), targetMember.RegistrationName.ToConstantExpression(), dataSourceIndex.ToConstantExpression()); @@ -494,7 +501,7 @@ public MethodCallExpression GetMapRecursionCall( EntryPointMapperData.MappingDataObject, _mapRecursionMethod.MakeGenericMethod(sourceObject.Type, targetMember.Type), sourceObject, - targetMember.GetAccess(InstanceVariable, this), + targetMember.GetAccess(this), EnumerableIndex, targetMember.RegistrationName.ToConstantExpression(), dataSourceIndex.ToConstantExpression()); diff --git a/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs b/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs index c35816bc3..2a50f0f2e 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs @@ -1,13 +1,10 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { - using System; using Members; using Members.Sources; - internal abstract class ObjectMapperKeyBase + internal abstract class ObjectMapperKeyBase : SourceMemberTypeDependentKeyBase { - private Func _sourceMemberTypeTester; - protected ObjectMapperKeyBase(MappingTypes mappingTypes) { MappingTypes = mappingTypes; @@ -15,16 +12,8 @@ protected ObjectMapperKeyBase(MappingTypes mappingTypes) public MappingTypes MappingTypes { get; } - public IObjectMappingData MappingData { get; set; } - protected bool TypesMatch(ObjectMapperKeyBase otherKey) => otherKey.MappingTypes.Equals(MappingTypes); - public void AddSourceMemberTypeTester(Func tester) - => _sourceMemberTypeTester = tester; - - protected bool SourceHasRequiredTypes(ObjectMapperKeyBase otherKey) - => (_sourceMemberTypeTester == null) || _sourceMemberTypeTester.Invoke(otherKey.MappingData); - public abstract IMembersSource GetMembersSource(IObjectMappingData parentMappingData); public ObjectMapperKeyBase WithTypes() diff --git a/AgileMapper/ObjectPopulation/ObjectMappingData.cs b/AgileMapper/ObjectPopulation/ObjectMappingData.cs index 0f0bce15e..6f155a999 100644 --- a/AgileMapper/ObjectPopulation/ObjectMappingData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMappingData.cs @@ -232,6 +232,18 @@ public void Register(TKey key, TComplex complexType) _mappedObjectsBySource[key] = new List { complexType }; } + public IObjectMappingData WithSourceType() + where TNewSource : class + { + return As(Source as TNewSource, Target); + } + + public IObjectMappingData WithTargetType() + where TNewTarget : class + { + return As(Source, Target as TNewTarget); + } + public IObjectMappingData WithTypes(Type newSourceType, Type newTargetType) { var typesKey = new SourceAndTargetTypesKey(newSourceType, newTargetType); @@ -255,10 +267,17 @@ public IObjectMappingData WithTypes(Type newSourceType, Type newTargetType) public IObjectMappingData As() where TNewSource : class where TNewTarget : class + { + return As(Source as TNewSource, Target as TNewTarget); + } + + private IObjectMappingData As( + TNewSource typedSource, + TNewTarget typedTarget) { return new ObjectMappingData( - Source as TNewSource, - Target as TNewTarget, + typedSource, + typedTarget, GetEnumerableIndex(), MapperKey.WithTypes(), MappingContext, diff --git a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs index f20abb3e3..0d6489f24 100644 --- a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs @@ -37,6 +37,6 @@ protected override IEnumerable GetObjectPopulation(IObjectMappingDat yield return mapperData.TargetObject; } - protected override Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.InstanceVariable; + protected override Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.TargetInstance; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs b/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs new file mode 100644 index 000000000..a268f0821 --- /dev/null +++ b/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs @@ -0,0 +1,39 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using Extensions; + using Members; + + internal abstract class SourceMemberTypeDependentKeyBase + { + private Func _sourceMemberTypeTester; + + public IObjectMappingData MappingData { get; set; } + + public void AddSourceMemberTypeTesterIfRequired(IObjectMappingData mappingData = null) + { + var typeTests = (mappingData ?? MappingData) + .MapperData + .DataSourcesByTargetMember + .Values + .Select(dataSourceSet => dataSourceSet.SourceMemberTypeTest) + .WhereNotNull() + .ToArray(); + + if (typeTests.None()) + { + return; + } + + var typeTest = typeTests.AndTogether(); + var typeTestLambda = Expression.Lambda>(typeTest, Parameters.MappingData); + + _sourceMemberTypeTester = typeTestLambda.Compile(); + } + + protected bool SourceHasRequiredTypes(SourceMemberTypeDependentKeyBase otherKey) + => (_sourceMemberTypeTester == null) || _sourceMemberTypeTester.Invoke(otherKey.MappingData); + } +} \ No newline at end of file diff --git a/AgileMapper/TypeConversion/ToNumericConverterBase.cs b/AgileMapper/TypeConversion/ToNumericConverterBase.cs index 66bca2d07..c38fb3161 100644 --- a/AgileMapper/TypeConversion/ToNumericConverterBase.cs +++ b/AgileMapper/TypeConversion/ToNumericConverterBase.cs @@ -91,8 +91,10 @@ private static Expression GetNumericValueValidityCheck(Expression sourceValue, T private static bool NonWholeNumberCheckIsNotRequired(Expression sourceValue, Type nonNullableTargetType) { - return sourceValue.Type.IsEnum() || - sourceValue.Type.IsWholeNumberNumeric() || + var sourceType = sourceValue.Type.GetNonNullableType(); + + return sourceType.IsEnum() || + sourceType.IsWholeNumberNumeric() || !nonNullableTargetType.IsWholeNumberNumeric(); } }