diff --git a/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj b/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj index 6d774bf3e..dc1e079ff 100644 --- a/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj +++ b/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj @@ -37,8 +37,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.3.2\lib\net40\AgileObjects.ReadableExpressions.dll ..\packages\AutoMapper.7.0.1\lib\net45\AutoMapper.dll diff --git a/AgileMapper.PerformanceTester.Net45/packages.config b/AgileMapper.PerformanceTester.Net45/packages.config index fcd3dee54..24b54cac7 100644 --- a/AgileMapper.PerformanceTester.Net45/packages.config +++ b/AgileMapper.PerformanceTester.Net45/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.Common/ShouldExtensions.cs b/AgileMapper.UnitTests.Common/ShouldExtensions.cs index 4fd39e6de..9eb41e0c4 100644 --- a/AgileMapper.UnitTests.Common/ShouldExtensions.cs +++ b/AgileMapper.UnitTests.Common/ShouldExtensions.cs @@ -52,7 +52,9 @@ public static void ShouldBe(this TActual value, TExpected ex { if (!AreEqual(expectedValue, value)) { - Asplode(expectedValue.ToString(), value?.ToString()); + Asplode( + expectedValue?.ToString() ?? (typeof(TExpected).CanBeNull() ? "null" : "default"), + value?.ToString() ?? (typeof(TActual).CanBeNull() ? "null" : "default")); } } @@ -405,14 +407,18 @@ public static IDictionary ShouldContainKeyAndValue( return dictionary; } - public static void ShouldBeOfType(this object actual) + public static TExpected ShouldBeOfType(this object actual) { - if (!(actual is TExpected)) + if (actual is TExpected expected) { - Asplode( - "An object of type " + typeof(TExpected).GetFriendlyName(), - actual.GetType().GetFriendlyName()); + return expected; } + + Asplode( + "An object of type " + typeof(TExpected).GetFriendlyName(), + actual.GetType().GetFriendlyName()); + + return default(TExpected); } public static void ShouldContain(this IList actual, T expected) diff --git a/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj b/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj index 7f64f66af..fec9a2f6d 100644 --- a/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj +++ b/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj @@ -37,8 +37,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net35\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net35\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.3.2\lib\net35\AgileObjects.ReadableExpressions.dll ..\packages\DynamicLanguageRuntime.1.1.2\lib\Net35\Microsoft.Dynamic.dll @@ -66,8 +66,599 @@ - - %(RecursiveDir)%(Filename)%(Extension) + + Caching\WhenCachingWithHashCodes.cs + + + Configuration\AssemblyScanningTestClassBase.cs + + + Configuration\Inline\InlineMappingExtensions.cs + + + Configuration\Inline\WhenConfiguringCallbacksInline.cs + + + Configuration\Inline\WhenConfiguringConstructorDataSourcesInline.cs + + + Configuration\Inline\WhenConfiguringDataSourcesInline.cs + + + Configuration\Inline\WhenConfiguringDataSourcesInlineIncorrectly.cs + + + Configuration\Inline\WhenConfiguringDerivedTypesInline.cs + + + Configuration\Inline\WhenConfiguringEntityMappingInline.cs + + + Configuration\Inline\WhenConfiguringEnumMappingInline.cs + + + Configuration\Inline\WhenConfiguringNameMatchingInline.cs + + + Configuration\Inline\WhenConfiguringObjectCreationInline.cs + + + Configuration\Inline\WhenConfiguringObjectTrackingInline.cs + + + Configuration\Inline\WhenConfiguringStringFormattingInline.cs + + + Configuration\Inline\WhenConfiguringTypeIdentifiersInline.cs + + + Configuration\Inline\WhenIgnoringMembersInline.cs + + + Configuration\Inline\WhenIgnoringMembersInlineIncorrectly.cs + + + Configuration\Inline\WhenMappingToNullInline.cs + + + Configuration\Inline\WhenValidatingMappingsInline.cs + + + Configuration\Inline\WhenViewingMappingPlans.cs + + + Configuration\WhenApplyingMapperConfigurations.cs + + + Configuration\WhenApplyingMapperConfigurationsIncorrectly.cs + + + Configuration\WhenConfiguringConstructorDataSources.cs + + + Configuration\WhenConfiguringDataSources.cs + + + Configuration\WhenConfiguringDataSourcesIncorrectly.cs + + + Configuration\WhenConfiguringDerivedTypes.cs + + + Configuration\WhenConfiguringDerivedTypesIncorrectly.cs + + + Configuration\WhenConfiguringEntityMapping.cs + + + Configuration\WhenConfiguringEnumMapping.cs + + + Configuration\WhenConfiguringExceptionHandling.cs + + + Configuration\WhenConfiguringMappingCallbacks.cs + + + Configuration\WhenConfiguringNameMatching.cs + + + Configuration\WhenConfiguringObjectCreation.cs + + + Configuration\WhenConfiguringObjectCreationCallbacks.cs + + + Configuration\WhenConfiguringObjectTracking.cs + + + Configuration\WhenConfiguringObjectTrackingIncorrectly.cs + + + Configuration\WhenConfiguringReverseDataSources.cs + + + Configuration\WhenConfiguringReverseDataSourcesIncorrectly.cs + + + Configuration\WhenConfiguringStringFormatting.cs + + + Configuration\WhenConfiguringTypeIdentifiers.cs + + + Configuration\WhenIgnoringMembers.cs + + + Configuration\WhenIgnoringMembersByFilter.cs + + + Configuration\WhenIgnoringMembersByGlobalFilter.cs + + + Configuration\WhenIgnoringMembersIncorrectly.cs + + + Configuration\WhenMappingToNull.cs + + + Configuration\WhenResolvingServices.cs + + + Configuration\WhenViewingMappingPlans.cs + + + Dictionaries\Configuration\WhenConfiguringDictionaryMappingIncorrectly.cs + + + Dictionaries\Configuration\WhenConfiguringNestedDictionaryMapping.cs + + + Dictionaries\Configuration\WhenConfiguringSourceDictionaryMapping.cs + + + Dictionaries\Configuration\WhenConfiguringTargetDictionaryMapping.cs + + + Dictionaries\WhenCreatingRootDictionaryMembers.cs + + + Dictionaries\WhenFlatteningToDictionaries.cs + + + Dictionaries\WhenMappingFromDictionariesOnToComplexTypes.cs + + + Dictionaries\WhenMappingFromDictionariesOnToEnumerableMembers.cs + + + Dictionaries\WhenMappingFromDictionariesOverComplexTypes.cs + + + Dictionaries\WhenMappingFromDictionariesToNewComplexTypeMembers.cs + + + Dictionaries\WhenMappingFromDictionariesToNewComplexTypes.cs + + + Dictionaries\WhenMappingFromDictionariesToNewEnumerableMembers.cs + + + Dictionaries\WhenMappingFromDictionariesToNewEnumerables.cs + + + Dictionaries\WhenMappingFromDictionaryMembers.cs + + + Dictionaries\WhenMappingOnToDictionaries.cs + + + Dictionaries\WhenMappingOnToDictionaryMembers.cs + + + Dictionaries\WhenMappingOverDictionaries.cs + + + Dictionaries\WhenMappingOverDictionaryMembers.cs + + + Dictionaries\WhenMappingToNewDictionaries.cs + + + Dictionaries\WhenMappingToNewDictionaryMembers.cs + + + Dictionaries\WhenUnflatteningFromDictionaries.cs + + + Dictionaries\WhenViewingDictionaryMappingPlans.cs + + + Extensions\Internal\WhenEquatingExpressions.cs + + + Extensions\Internal\WhenGeneratingVariableNames.cs + + + Extensions\WhenFlatteningToQueryStringViaExtensionMethods.cs + + + Extensions\WhenFlatteningViaExtensionMethods.cs + + + Extensions\WhenMappingViaExtensionMethods.cs + + + Extensions\WhenUnflatteningFromQueryStringsViaExtensionMethods.cs + + + Extensions\WhenUnflatteningViaExtensionMethods.cs + + + MapperCloning\WhenCloningConstructorDataSources.cs + + + MapperCloning\WhenCloningDataSources.cs + + + MapperCloning\WhenCloningDictionarySettings.cs + + + MapperCloning\WhenCloningMemberIgnores.cs + + + MapperCloning\WhenCloningObjectFactories.cs + + + MapperCloning\WhenCloningStringFormatting.cs + + + Members\MemberTestsBase.cs + + + Members\WhenCreatingTargetMembersFromExpressions.cs + + + Members\WhenDeterminingATypeIdentifier.cs + + + Members\WhenDeterminingRecursion.cs + + + Members\WhenFindingDataSources.cs + + + Members\WhenFindingSourceMembers.cs + + + Members\WhenFindingTargetMembers.cs + + + Members\WhenMatchingSourceToTargetMembers.cs + + + Properties\AssemblyInfo.cs + + + Reflection\WhenAccessingTypeInformation.cs + + + SimpleTypeConversion\WhenConvertingToBools.cs + + + SimpleTypeConversion\WhenConvertingToBytes.cs + + + SimpleTypeConversion\WhenConvertingToCharacters.cs + + + SimpleTypeConversion\WhenConvertingToDateTimes.cs + + + SimpleTypeConversion\WhenConvertingToDecimals.cs + + + SimpleTypeConversion\WhenConvertingToDoubles.cs + + + SimpleTypeConversion\WhenConvertingToEnums.cs + + + SimpleTypeConversion\WhenConvertingToFlagsEnums.cs + + + SimpleTypeConversion\WhenConvertingToGuids.cs + + + SimpleTypeConversion\WhenConvertingToInts.cs + + + SimpleTypeConversion\WhenConvertingToLongs.cs + + + SimpleTypeConversion\WhenConvertingToShorts.cs + + + SimpleTypeConversion\WhenConvertingToStrings.cs + + + Structs\Configuration\WhenConfiguringStructCreationCallbacks.cs + + + Structs\Configuration\WhenConfiguringStructDataSources.cs + + + Structs\Configuration\WhenConfiguringStructMappingCallbacks.cs + + + Structs\Dictionaries\WhenMappingFromDictionariesToStructs.cs + + + Structs\WhenMappingOnToStructMembers.cs + + + Structs\WhenMappingOnToStructs.cs + + + Structs\WhenMappingOverStructMembers.cs + + + Structs\WhenMappingOverStructs.cs + + + Structs\WhenMappingToNewStructMembers.cs + + + Structs\WhenMappingToNewStructs.cs + + + Structs\WhenMappingToStructEnumerables.cs + + + Structs\WhenMappingToUnmappableStructMembers.cs + + + TestClasses\Address.cs + + + TestClasses\CategoryDto.cs + + + TestClasses\CategoryEntity.cs + + + TestClasses\Child.cs + + + TestClasses\Customer.cs + + + TestClasses\CustomerViewModel.cs + + + TestClasses\DtoBase.cs + + + TestClasses\Earthworm.cs + + + TestClasses\EntityBase.cs + + + TestClasses\FacebookUser.cs + + + TestClasses\InternalField.cs + + + TestClasses\IPublicInterface.cs + + + TestClasses\MegaProduct.cs + + + TestClasses\MysteryCustomer.cs + + + TestClasses\MysteryCustomerViewModel.cs + + + TestClasses\Order.cs + + + TestClasses\OrderDto.cs + + + TestClasses\OrderEntity.cs + + + TestClasses\OrderItem.cs + + + TestClasses\OrderItemDto.cs + + + TestClasses\OrderItemEntity.cs + + + TestClasses\OrderUk.cs + + + TestClasses\OrderUs.cs + + + TestClasses\Parent.cs + + + TestClasses\PaymentTypeUk.cs + + + TestClasses\PaymentTypeUs.cs + + + TestClasses\Person.cs + + + TestClasses\PersonViewModel.cs + + + TestClasses\Product.cs + + + TestClasses\ProductDto.cs + + + TestClasses\ProductDtoMega.cs + + + TestClasses\ProductEntity.cs + + + TestClasses\PublicCtor.cs + + + TestClasses\PublicCtorStruct.cs + + + TestClasses\PublicEnumerable.cs + + + TestClasses\PublicField.cs + + + TestClasses\PublicGetMethod.cs + + + TestClasses\PublicImplementation.cs + + + TestClasses\PublicIndex.cs + + + TestClasses\PublicProperty.cs + + + TestClasses\PublicPropertyStruct.cs + + + TestClasses\PublicReadOnlyField.cs + + + TestClasses\PublicReadOnlyProperty.cs + + + TestClasses\PublicSealed.cs + + + TestClasses\PublicSetMethod.cs + + + TestClasses\PublicTwoFields.cs + + + TestClasses\PublicTwoFieldsStruct.cs + + + TestClasses\PublicTwoParamCtor.cs + + + TestClasses\PublicUnconstructable.cs + + + TestClasses\PublicWriteOnlyProperty.cs + + + TestClasses\SaveOrderItemRequest.cs + + + TestClasses\SaveOrderRequest.cs + + + TestClasses\Status.cs + + + TestClasses\StringKeyedDictionary.cs + + + TestClasses\Title.cs + + + TestClasses\TitleShortlist.cs + + + TestClasses\Wedding.cs + + + TestClasses\WeddingDto.cs + + + WhenAnalysingCollections.cs + + + WhenMappingCircularReferences.cs + + + WhenMappingDerivedTypes.cs + + + WhenMappingEntities.cs + + + WhenMappingOnToComplexTypeMembers.cs + + + WhenMappingOnToComplexTypes.cs + + + WhenMappingOnToEnumerableMembers.cs + + + WhenMappingOnToEnumerables.cs + + + WhenMappingOverComplexTypeMembers.cs + + + WhenMappingOverComplexTypes.cs + + + WhenMappingOverEnumerableMembers.cs + + + WhenMappingOverEnumerables.cs + + + WhenMappingToConstructors.cs + + + WhenMappingToMetaMembers.cs + + + WhenMappingToNewComplexTypeMembers.cs + + + WhenMappingToNewComplexTypes.cs + + + WhenMappingToNewEnumerableMembers.cs + + + WhenMappingToNewEnumerables.cs + + + WhenUnflatteningFromQueryStrings.cs + + + WhenUsingFactoryMethods.cs + + + WhenValidatingMappings.cs + + + WhenViewingMappingPlans.cs + + + WhenWorkingWithQueryStrings.cs diff --git a/AgileMapper.UnitTests.Net35/packages.config b/AgileMapper.UnitTests.Net35/packages.config index 737e3b5da..0f40009f4 100644 --- a/AgileMapper.UnitTests.Net35/packages.config +++ b/AgileMapper.UnitTests.Net35/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj index ab86a47df..538211964 100644 --- a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj +++ b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj @@ -42,8 +42,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.3.2\lib\net40\AgileObjects.ReadableExpressions.dll diff --git a/AgileMapper.UnitTests.NonParallel/packages.config b/AgileMapper.UnitTests.NonParallel/packages.config index 4f52878b7..64734ef6e 100644 --- a/AgileMapper.UnitTests.NonParallel/packages.config +++ b/AgileMapper.UnitTests.NonParallel/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs index f7d7f559f..e3ab13636 100644 --- a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs @@ -1,5 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 { + using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; using Common; using Infrastructure; @@ -49,5 +51,22 @@ public Task ShouldReuseACachedProjectionMapper() mapper.RootMapperCountShouldBeOne(); }); } + + [Fact] + public Task ShouldMapAQueryableAsAnEnumerable() + { + return RunTest(async (context, mapper) => + { + await context.BoolItems.AddRangeAsync(new PublicBool { Value = true }, new PublicBool { Value = false }); + await context.SaveChangesAsync(); + + var result = mapper + .Map(context.BoolItems.Where(bi => bi.Value)) + .ToANew>(); + + result.ShouldNotBeNull(); + result.ShouldHaveSingleItem().Value.ShouldBeTrue(); + }); + } } } diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index 8d6904379..ef3d3d7fe 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -44,8 +44,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.3.2\lib\net40\AgileObjects.ReadableExpressions.dll ..\packages\Microsoft.Extensions.Primitives.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll @@ -103,6 +103,9 @@ + + + @@ -111,6 +114,12 @@ + + + + + + @@ -144,6 +153,7 @@ + @@ -219,7 +229,9 @@ + + @@ -301,6 +313,7 @@ + diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs index 1d398662d..250f4af91 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -7,7 +7,9 @@ using Common; using TestClasses; #if !NET35 + using NetStandardPolyfills; using Xunit; + using static System.Linq.Expressions.Expression; #else using Fact = NUnit.Framework.TestAttribute; @@ -317,6 +319,194 @@ public void ShouldApplyDifferingTargetTypeInlineDataSourceMemberConfig() } } + [Fact] + public void ShouldApplyAnInlineNullCheckedArrayIndexDataSource() + { + using (var mapper = Mapper.CreateNew()) + { + var source = new PublicProperty[]> + { + Value = new[] + { + new PublicField
{ Value = new Address { Line1 = "1.1" } }, + new PublicField
{ Value = new Address { Line1 = "1.2" } } + } + }; + + var result = mapper.Map(source).ToANew>(cfg => cfg + .Map(s => s.Value[1].Value, t => t.Value)); + + result.Value.ShouldNotBeNull(); + result.Value.Line1.ShouldBe("1.2"); + + var nullArraySource = new PublicProperty[]> { Value = null }; + + var nullArrayResult = mapper.Map(nullArraySource).ToANew>(cfg => cfg + .Map(s => s.Value[1].Value, t => t.Value)); + + nullArrayResult.Value.ShouldBeNull(); + + var tooSmallArraySource = new PublicProperty[]> + { + Value = new[] + { + new PublicField
{ Value = new Address { Line1 = "1.1" } } + } + }; + + var tooSmallArrayResult = mapper.Map(tooSmallArraySource).ToANew>(cfg => cfg + .Map(s => s.Value[1].Value, t => t.Value)); + + tooSmallArrayResult.Value.ShouldBeNull(); + + var nullArrayObjectSource = new PublicProperty[]> + { + Value = new[] + { + new PublicField
{ Value = new Address { Line1 = "1.1" } }, + new PublicField
{ Value = null } + } + }; + + var nullArrayObjectResult = mapper.Map(nullArrayObjectSource).ToANew>(cfg => cfg + .Map(s => s.Value[1].Value, t => t.Value)); + + nullArrayObjectResult.Value.ShouldBeNull(); + } + } + +#if !NET35 + // System.Linq.Expressions.Expression.MakeIndex() is missing in .NET 3.5, so not much danger of this configuration: + [Fact] + public void ShouldApplyAnInlineNullCheckedIntKeyedIndexDataSource() + { + using (var mapper = Mapper.CreateNew()) + { + var source = new PublicProperty>> + { + Value = new PublicIndex> + { + [0] = new PublicField
{ Value = new Address { Line1 = "1.1" } }, + [1] = new PublicField
{ Value = new Address { Line1 = "1.2" } } + } + }; + + var sourceParameter = Parameter(source.GetType(), "s"); + var sourceValueProperty = Property(sourceParameter, "Value"); + var sourceValueIndexer = sourceValueProperty.Type.GetPublicInstanceProperty("Item"); + var sourceValueIndex = MakeIndex(sourceValueProperty, sourceValueIndexer, new[] { Constant(1) }); + + var sourceLambda = Lambda>>, Address>>( + Field(sourceValueIndex, "Value"), + sourceParameter); + + var result = mapper.Map(source).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + result.Value.ShouldNotBeNull(); + result.Value.Line1.ShouldBe("1.2"); + + var nullIndexerSource = new PublicProperty>> { Value = null }; + + var nullIndexerResult = mapper.Map(nullIndexerSource).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + nullIndexerResult.Value.ShouldBeNull(); + + var noEntrySource = new PublicProperty>> + { + Value = new PublicIndex> + { + [0] = new PublicField
{ Value = new Address { Line1 = "1.1" } } + } + }; + + var noEntryResult = mapper.Map(noEntrySource).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + noEntryResult.Value.ShouldBeNull(); + + var nullIndexedObjectSource = new PublicProperty>> + { + Value = new PublicIndex> + { + [0] = new PublicField
{ Value = new Address { Line1 = "1.1" } }, + [1] = new PublicField
{ Value = null } + } + }; + + var nullIndexedObjectResult = mapper.Map(nullIndexedObjectSource).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + nullIndexedObjectResult.Value.ShouldBeNull(); + } + } + + [Fact] + public void ShouldApplyAnInlineNullCheckedStringKeyedIndexDataSource() + { + using (var mapper = Mapper.CreateNew()) + { + var source = new PublicProperty>> + { + Value = new PublicIndex> + { + ["A"] = new PublicField
{ Value = new Address { Line1 = "1.1" } }, + ["B"] = new PublicField
{ Value = new Address { Line1 = "1.2" } } + } + }; + + var sourceParameter = Parameter(source.GetType(), "s"); + var sourceValueProperty = Property(sourceParameter, "Value"); + var sourceValueIndexer = sourceValueProperty.Type.GetPublicInstanceProperty("Item"); + var sourceValueIndex = MakeIndex(sourceValueProperty, sourceValueIndexer, new[] { Constant("B") }); + + var sourceLambda = Lambda>>, Address>>( + Field(sourceValueIndex, "Value"), + sourceParameter); + + var result = mapper.Map(source).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + result.Value.ShouldNotBeNull(); + result.Value.Line1.ShouldBe("1.2"); + + var nullIndexerSource = new PublicProperty>> { Value = null }; + + var nullIndexerResult = mapper.Map(nullIndexerSource).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + nullIndexerResult.Value.ShouldBeNull(); + + var noEntrySource = new PublicProperty>> + { + Value = new PublicIndex> + { + ["A"] = new PublicField
{ Value = new Address { Line1 = "1.1" } } + } + }; + + var noEntryResult = mapper.Map(noEntrySource).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + noEntryResult.Value.ShouldBeNull(); + + var nullIndexedObjectSource = new PublicProperty>> + { + Value = new PublicIndex> + { + ["A"] = new PublicField
{ Value = new Address { Line1 = "1.1" } }, + ["B"] = new PublicField
{ Value = null } + } + }; + + var nullIndexedObjectResult = mapper.Map(nullIndexedObjectSource).ToANew>(cfg => cfg + .Map(sourceLambda, t => t.Value)); + + nullIndexedObjectResult.Value.ShouldBeNull(); + } + } +#endif [Fact] public void ShouldHandleANullSourceMember() { diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringStringFormattingInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringStringFormattingInline.cs index 0f19d9ceb..c923a849a 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringStringFormattingInline.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringStringFormattingInline.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.UnitTests.Configuration.Inline { + using System; using AgileMapper.Configuration; using Common; using TestClasses; @@ -43,6 +44,30 @@ public void ShouldFormatDoublesWithDecimalPlacesInline() } } + // See https://github.com/agileobjects/AgileMapper/issues/149 + [Fact] + public void ShouldFormatNullableDateTimesInline() + { + using (var mapper = Mapper.CreateNew()) + { + var result1 = mapper + .Map(new PublicProperty { Value = DateTime.Today }) + .ToANew>(cfg => cfg + .WhenMapping + .StringsFrom(c => c.FormatUsing("yyyy MM dd"))); + + result1.Value.ShouldBe(DateTime.Today.ToString("yyyy MM dd")); + + var result2 = mapper + .Map(new PublicProperty { Value = null }) + .ToANew>(); + + result2.Value.ShouldBeNull(); + + mapper.InlineContexts().ShouldHaveSingleItem(); + } + } + [Fact] public void ShouldErrorIfUnformattableTypeSpecifiedInline() { diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMemberInlineIncorrectly.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMemberInlineIncorrectly.cs new file mode 100644 index 000000000..ca466a297 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMemberInlineIncorrectly.cs @@ -0,0 +1,36 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration.Inline +{ + using AgileMapper.Configuration; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMemberInlineIncorrectly + { + [Fact] + public void ShouldErrorIfDuplicateSourceIgnoreIsConfiguredInline() + { + var ignoreEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSource(pf => pf.Value); + + mapper + .Map(new PublicField { Value = 123 }) + .ToANew>(cfg => cfg + .IgnoreSource(pf => pf.Value)); + } + }); + + ignoreEx.Message.ShouldContain("has already been ignored"); + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs new file mode 100644 index 000000000..9509688e9 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersByValueFilterInline.cs @@ -0,0 +1,301 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration.Inline +{ + using System; + using System.Collections.Generic; + using System.Linq; + using AgileMapper.Extensions.Internal; + using AgileObjects.AgileMapper.Configuration; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersByValueFilterInline + { + [Fact] + public void ShouldIgnoreSourceValuesByMultiClauseTypedValueFiltersOnline() + { + using (var mapper = Mapper.CreateNew()) + { + var matchingIntResult = mapper + .Map(new PublicField { Value = 123 }) + .ToANew>(cfg => cfg + .IgnoreSources(c => + c.If(str => str == "123") || c.If(i => i == 123) || + (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); + + matchingIntResult.ShouldNotBeNull(); + matchingIntResult.Value.ShouldBeDefault(); + + var matchingStringResult = mapper + .Map(new PublicField { Value = "123" }) + .ToANew>(cfg => cfg + .IgnoreSources(c => + c.If(str => str == "123") || c.If(i => i == 123) || + (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); + + matchingStringResult.ShouldNotBeNull(); + matchingStringResult.Value.ShouldBeNull(); + + var nonMatchingIntResult = mapper + .Map(new PublicField { Value = 456 }) + .ToANew>(cfg => cfg + .IgnoreSources(c => + c.If(str => str == "123") || c.If(i => i == 123) || + (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); + + nonMatchingIntResult.ShouldNotBeNull(); + nonMatchingIntResult.Value.ShouldBe(456); + + var nonMatchingStringResult = mapper + .Map(new PublicField { Value = "999" }) + .ToANew>(cfg => cfg + .IgnoreSources(c => + c.If(str => str == "123") || c.If(i => i == 123) || + (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); + + nonMatchingStringResult.ShouldNotBeNull(); + nonMatchingStringResult.Value.ShouldBe("999"); + + var nonMatchingTypeResult = mapper + .Map(new PublicField { Value = 123L }) + .ToANew>(cfg => cfg + .IgnoreSources(c => + c.If(str => str == "123") || c.If(i => i == 123) || + (c.If(str => str != "999") && !c.If(dt => dt == DateTime.Today)))); + + nonMatchingTypeResult.ShouldNotBeNull(); + nonMatchingTypeResult.Value.ShouldBe("123"); + } + } + + [Fact] + public void ShouldHandleNullMemberInANestedSourceValueFilterInline() + { + using (var mapper = Mapper.CreateNew()) + { + var result = mapper + .Map(new List + { + new Customer { Name = "Customer 1", Address = new Address { Line1 = "1 Street" } }, + new MysteryCustomer { Name = "Customer 2"} + }) + .ToANew>(cfg => cfg + .IgnoreSources(s => s.If(c => c.Address.Line1.Length < 2))); + + result.ShouldNotBeNull(); + result.ShouldHaveSingleItem(); + result.First().Name.ShouldBe("Customer 1"); + result.First().AddressLine1.ShouldBe("1 Street"); + } + } + + [Fact] + public void ShouldFilterAnEnumerableSourceValueConditionallyInline() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .Over>>() + .Map(ctx => ctx.Source.Value1) + .To(t => t.Value) + .But + .If(ctx => ctx.Source.Value1.None()) + .Map(ctx => ctx.Source.Value2) + .To(t => t.Value); + + var target = new PublicProperty>(); + + var bothValuesSource = new PublicTwoFields + { + Value1 = new[] { new Product { ProductId = "111" } }, + Value2 = new[] { new Product { ProductId = "222" } } + }; + + mapper.Map(bothValuesSource).Over(target); + + target.Value.ShouldHaveSingleItem().ProductId.ShouldBe("111"); + target.Value.Clear(); + + var emptyValue1Source = new PublicTwoFields + { + Value1 = Enumerable.EmptyArray, + Value2 = new[] { new Product { ProductId = "222" } } + }; + + mapper.Map(emptyValue1Source).Over(target); + + target.Value.ShouldHaveSingleItem().ProductId.ShouldBe("222"); + target.Value.Clear(); + + mapper + .Map(bothValuesSource) + .Over(target, cfg => cfg + .IgnoreSources(s => s.If(ps => ps[0].ProductId == "111"))); + + target.Value.ShouldBeEmpty(); + target.Value = null; + + mapper + .Map(bothValuesSource) + .Over(target, cfg => cfg + .IgnoreSources(s => s.If(ps => ps[0].ProductId == "111"))); + + target.Value.ShouldBeNull(); + } + } + + [Fact] + public void ShouldExtendSourceValueFilterConfiguration() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .Over>() + .IgnoreSources(c => c.If(i => i < 10)); + + var result1 = mapper + .Map(new PublicTwoFieldsStruct { Value1 = 4, Value2 = 12L }) + .Over(new PublicTwoFields(), cfg => cfg + .IgnoreSources(c => c.If(l => l > 10L))); + + result1.Value1.ShouldBeDefault(); // int < 10 + result1.Value2.ShouldBeDefault(); // long > 10 + + var result2 = mapper + .Map(new PublicTwoFieldsStruct { Value1 = 20, Value2 = 15L }) + .Over(new PublicTwoFields(), cfg => cfg + .IgnoreSources(c => c.If(l => l > 10L))); + + result2.Value1.ShouldBe(20); + result2.Value2.ShouldBeDefault(); // long > 10 + + mapper.InlineContexts().ShouldHaveSingleItem(); + + var result3 = mapper + .Map(new PublicTwoFieldsStruct { Value1 = 20, Value2 = 11L }) + .Over(new PublicTwoFields(), cfg => cfg + .IgnoreSources(c => c.If(i => i < 25)) + .And + .IgnoreSources(c => c.If(l => l > 12L))); + + result3.Value1.ShouldBeDefault(); // int < 25 + result3.Value2.ShouldBe(11); + + mapper.InlineContexts().Count.ShouldBe(2); + } + } + + [Fact] + public void ShouldReplaceASourceValueFilterWithAConditionalFilterInline() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(value => value % 2 == 0)); + + var evenValueSource = new PublicField { Value = 8 }; + var eventValueResult = mapper.Map(evenValueSource).ToANew>(); + + eventValueResult.ShouldNotBeNull(); + eventValueResult.Value.ShouldBeDefault(); + + var oddValueSource = new PublicField { Value = 5 }; + var oddValueResult = mapper.Map(oddValueSource).ToANew>(); + + oddValueResult.ShouldNotBeNull(); + oddValueResult.Value.ShouldBe(5); + + var inlineFilterSmallEvenValueResult = mapper + .Map(evenValueSource) + .ToANew>(cfg => cfg + .If(ctx => ctx.Source.Value > 10) + .IgnoreSources(c => c.If(value => value % 2 == 0))); + + inlineFilterSmallEvenValueResult.ShouldNotBeNull(); + inlineFilterSmallEvenValueResult.Value.ShouldBe(8); + + var inlineFilterLargeEvenValueResult = mapper + .Map(new PublicField { Value = 16 }) + .ToANew>(cfg => cfg + .If(ctx => ctx.Source.Value > 10) + .IgnoreSources(c => c.If(value => value % 2 == 0))); + + inlineFilterLargeEvenValueResult.ShouldNotBeNull(); + inlineFilterLargeEvenValueResult.Value.ShouldBeDefault(); + + mapper.InlineContexts().ShouldHaveSingleItem(); + } + } + + [Fact] + public void ShouldReplaceAMultiClauseSourceValueFilterWithAConditionalFilterInline() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(value => value > 3) && c.If(value => value < 8)); + + var filteredValueSource = new PublicField { Value = 6 }; + var filteredValueResult = mapper.Map(filteredValueSource).ToANew>(); + + filteredValueResult.ShouldNotBeNull(); + filteredValueResult.Value.ShouldBeDefault(); + + var unfilteredValueSource = new PublicField { Value = 2 }; + var unfilteredValueResult = mapper.Map(unfilteredValueSource).ToANew>(); + + unfilteredValueResult.ShouldNotBeNull(); + unfilteredValueResult.Value.ShouldBe(2); + + var inlineUnfilteredValueResult = mapper + .Map(filteredValueSource) + .ToANew>(cfg => cfg + .If(ctx => ctx.Source.Value != 6) + .IgnoreSources(c => c.If(value => value > 3) && c.If(value => value < 8))); + + inlineUnfilteredValueResult.ShouldNotBeNull(); + inlineUnfilteredValueResult.Value.ShouldBe(6); + + var inlineFilteredValueResult = mapper + .Map(new PublicField { Value = 7 }) + .ToANew>(cfg => cfg + .If(ctx => ctx.Source.Value != 6) + .IgnoreSources(c => c.If(value => value > 3) && c.If(value => value < 8))); + + inlineFilteredValueResult.ShouldNotBeNull(); + inlineFilteredValueResult.Value.ShouldBeDefault(); + + mapper.InlineContexts().ShouldHaveSingleItem(); + } + } + + [Fact] + public void ShouldErrorIfDuplicateSourceValueFilterConfiguredInline() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(value => value == 555)); + + mapper + .Map(new PublicField()) + .ToANew>(cfg => cfg + .IgnoreSources(c => c.If(value => value == 555))); + } + }); + + configEx.Message.ShouldContain("Source filter"); + configEx.Message.ShouldContain("If(value => value == 555)"); + configEx.Message.ShouldContain("already been configured"); + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersInline.cs new file mode 100644 index 000000000..b4cf2f8d5 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenIgnoringSourceMembersInline.cs @@ -0,0 +1,86 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration.Inline +{ + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersInline + { + [Fact] + public void ShouldFilterASourceMemberConditionallyInline() + { + using (var mapper = Mapper.CreateNew()) + { + var matchingResult = mapper + .Map(new CustomerViewModel { Discount = 0.5 }) + .ToANew(cfg => cfg + .If(ctx => ctx.Source.Discount > 0.3) + .IgnoreSource(cvm => cvm.Discount)); + + matchingResult.Discount.ShouldBeDefault(); + + var nonMatchingResult = mapper + .Map(new CustomerViewModel { Discount = 0.2 }) + .ToANew(cfg => cfg + .If(ctx => ctx.Source.Discount > 0.3) + .IgnoreSource(cvm => cvm.Discount)); + + nonMatchingResult.Discount.ShouldBe(0.2m); + + mapper.InlineContexts().ShouldHaveSingleItem(); + } + } + + [Fact] + public void ShouldExtendSourceMemberFilterConfiguration() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .If((sptf, tptf) => sptf.Value1 < 5) + .IgnoreSource(sptf => sptf.Value1); // Ignore source.Value1 < 5 + + var result1 = mapper + .Map(new PublicTwoFieldsStruct { Value1 = 4, Value2 = 8 }) + .OnTo(new PublicTwoFields(), c => c + .If((sptf, tptf) => sptf.Value2 <= 10) + .IgnoreSource(sptf => sptf.Value2)); // Ignore source.Value2 <= 10 + + result1.Value1.ShouldBeDefault(); + result1.Value2.ShouldBeDefault(); + + var result2 = mapper + .Map(new PublicTwoFieldsStruct { Value1 = 5, Value2 = 7 }) + .OnTo(new PublicTwoFields(), c => c + .If((sptf, tptf) => sptf.Value2 <= 10) + .IgnoreSource(sptf => sptf.Value2)); // Ignore source.Value2 <= 10 + + result2.Value1.ShouldBe(5); + result2.Value2.ShouldBeDefault(); + + mapper.InlineContexts().ShouldHaveSingleItem(); + + var result3 = mapper + .Map(new PublicTwoFieldsStruct { Value1 = 5, Value2 = 11 }) + .OnTo(new PublicTwoFields(), c => c + .If((sptf, tptf) => sptf.Value1 >= 3) + .IgnoreSource(sptf => sptf.Value1) // Ignore source.Value1 >= 3 + .And + .If((sptf, tptf) => sptf.Value2 <= 10) + .IgnoreSource(sptf => sptf.Value2)); // Ignore source.Value2 < 10 + + result3.Value1.ShouldBeDefault(); + result3.Value2.ShouldBe(11); + + mapper.InlineContexts().Count.ShouldBe(2); + } + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs index be0810cc8..635d041ce 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSources.cs @@ -148,9 +148,36 @@ public void ShouldApplyMultipleConfiguredMembersBySourceType() } } + [Fact] + public void ShouldAllowConditionTypeTestsWhenMappingFromAnInterface() + { + using (var mapper = Mapper.CreateNew()) + { + var sourceData = default(Issue146.Source.Data); + + mapper.WhenMapping + .From().To() + .Map(s => s.Empty, t => t.Info); + + mapper.WhenMapping + .From().To() + .After + .MappingEnds + .If(ctx => ctx.Source is Issue146.Source.Data) + .Call(ctx => sourceData = (Issue146.Source.Data)ctx.Source); + + var source = new Issue146.Source.Container("xxx"); + var result = mapper.Map(source).ToANew(); + + result.ShouldNotBeNull(); + sourceData.ShouldNotBeNull(); + sourceData.ShouldBeSameAs(source.Empty); + } + } + // See https://github.com/agileobjects/AgileMapper/issues/111 [Fact] - public void ShouldConditionallyApplyAToTargetConfiguredSimpleTypeConstant() + public void ShouldConditionallyApplyAToTargetSimpleTypeConstant() { using (var mapper = Mapper.CreateNew()) { @@ -168,7 +195,7 @@ public void ShouldConditionallyApplyAToTargetConfiguredSimpleTypeConstant() } [Fact] - public void ShouldApplyAToTargetConfiguredSimpleTypeConstant() + public void ShouldConditionallyApplyAToTargetSimpleType() { using (var mapper = Mapper.CreateNew()) { @@ -185,7 +212,7 @@ public void ShouldApplyAToTargetConfiguredSimpleTypeConstant() } [Fact] - public void ShouldConditionallyApplyAToTargetConfiguredNestedSimpleTypeExpression() + public void ShouldConditionallyApplyAToTargetNestedSimpleTypeExpression() { using (var mapper = Mapper.CreateNew()) { @@ -209,7 +236,7 @@ public void ShouldConditionallyApplyAToTargetConfiguredNestedSimpleTypeExpressio } [Fact] - public void ShouldConditionallyApplyAToTargetConfiguredSimpleTypeExpressionInAComplexTypeList() + public void ShouldConditionallyApplyAToTargetSimpleTypeExpressionToAComplexTypeListMember() { using (var mapper = Mapper.CreateNew()) { @@ -258,6 +285,27 @@ public void ShouldConditionallyApplyAConfiguredMember() } } + [Fact] + public void ShouldNotOverwriteATargetWithNoMatchingSourceMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .If(ctx => ctx.Source.Value1 > 100) + .Map((ptf, pf) => ptf.Value1) + .To(pf => pf.Value); + + var source = new PublicTwoFieldsStruct { Value1 = 50 }; + var target = new PublicField { Value = "Value!" }; + + mapper.Map(source).Over(target); + + target.Value.ShouldBe("Value!"); + } + } + [Fact] public void ShouldConditionallyApplyMultipleConfiguredMembers() { @@ -509,7 +557,7 @@ public void ShouldApplyAConfiguredExpressionToAnArray() .From>() .To>() #if NETCOREAPP2_0 - .Map(ctx => ctx.Source.Value.Split(':', System.StringSplitOptions.None)) + .Map(ctx => ctx.Source.Value.Split(':', StringSplitOptions.None)) #else .Map(ctx => ctx.Source.Value.Split(':')) #endif @@ -1161,9 +1209,32 @@ public void ShouldAllowIdAndIdentifierConfiguration() } } + // See https://github.com/agileobjects/AgileMapper/issues/146 + [Fact] + public void ShouldApplyAConfiguredSourceInterfaceMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To() + .Map(ctx => ctx.Source.Empty).To(tgt => tgt.Info); + + var source = new Issue146.Source.Container("12321") { Name = "input" }; + var result = mapper.Map(source).ToANew(); + + result.ShouldNotBeNull(); + result.Name.ShouldBe("input"); + result.Info.ShouldNotBeNull(); + result.Info.Id.ShouldBe("12321"); + + // Source has a .Value member, but we don't runtime-type interfaces + result.Info.Value.ShouldBeNull(); + } + } + // See https://github.com/agileobjects/AgileMapper/issues/64 [Fact] - public void ShouldApplyAConfiguredRootSource() + public void ShouldApplyAConfiguredToTargetDataSource() { using (var mapper = Mapper.CreateNew()) { @@ -1185,7 +1256,7 @@ public void ShouldApplyAConfiguredRootSource() } [Fact] - public void ShouldApplyANestedOverwriteConfiguredRootSource() + public void ShouldApplyANestedOverwriteConfiguredToTargetDataSource() { using (var mapper = Mapper.CreateNew()) { @@ -1345,7 +1416,7 @@ public void ShouldApplyAConfiguredRootSourceToANestedMember() } [Fact] - public void ShouldApplyAConfiguredRootSourceToAnEnumerableElement() + public void ShouldApplyAToTargetComplexTypeToAComplexTypeEnumerableElement() { using (var mapper = Mapper.CreateNew()) { @@ -1376,7 +1447,7 @@ public void ShouldApplyAConfiguredRootSourceToAnEnumerableElement() } [Fact] - public void ShouldApplyAConfiguredEnumerableRootSource() + public void ShouldApplyAToTargetComplexTypeEnumerable() { using (var mapper = Mapper.CreateNew()) { @@ -1413,7 +1484,7 @@ public void ShouldApplyAConfiguredEnumerableRootSource() } [Fact] - public void ShouldApplyMultipleConfiguredComplexTypeRootSources() + public void ShouldApplyMultipleToTargetComplexTypes() { using (var mapper = Mapper.CreateNew()) { @@ -1440,7 +1511,7 @@ public void ShouldApplyMultipleConfiguredComplexTypeRootSources() } [Fact] - public void ShouldApplyMultipleConfiguredEnumerableRootSources() + public void ShouldApplyMultipleToTargetSimpleTypeEnumerables() { using (var mapper = Mapper.CreateNew()) { @@ -1465,6 +1536,30 @@ public void ShouldApplyMultipleConfiguredEnumerableRootSources() } } + // See https://github.com/agileobjects/AgileMapper/issues/145 + [Fact] + public void ShouldHandleNullToTargetDataSourceNestedMembers() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To() + .Map((srcData, tgtData) => srcData.cont).ToTarget(); + + var source = new Issue145.DataSource + { + cont = new Issue145.DataSourceContainer() + }; + + var result = mapper.Map(source).ToANew(); + + result.ShouldNotBeNull(); + result.ids.ShouldBeNull(); + result.res.ShouldBeNull(); + result.oth.ShouldBeNull(); + } + } + // See https://github.com/agileobjects/AgileMapper/issues/125 [Fact] public void ShouldHandleDeepNestedRuntimeTypedMembersWithACachedMappingPlan() @@ -1545,6 +1640,42 @@ public void ShouldHandleDeepNestedRuntimeTypedMembersWithACachedMappingPlan() } } + [Fact] + public void ShouldApplyAToTargetSimpleTypeToANestedComplexTypeMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .Map(ctx => PublicEnumerable.Parse(ctx.Source)).ToTarget(); + + mapper.GetPlanFor>().ToANew>>(); + + var source = new PublicField { Value = "1,2,3" }; + var result = mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldNotBeNull(); + result.Value.ShouldBe(1, 2, 3); + } + } + + [Fact] + public void ShouldConditionallyApplyAToTargetSimpleTypeToANestedComplexTypeMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .If(cxt => cxt.Source.Contains(',')) + .Map(ctx => PublicEnumerable.Parse(ctx.Source)).ToTarget(); + + mapper.GetPlanFor>().ToANew>>(); + } + } + + #region Helper Classes + internal class IdTester { public int ClassId { get; set; } @@ -1718,5 +1849,111 @@ public class ParamDef // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local // ReSharper restore MemberCanBePrivate.Local // ReSharper restore CollectionNeverQueried.Local + + // ReSharper disable InconsistentNaming + internal static class Issue145 + { + public class IdsSource + { + public string Ids { get; set; } + } + + public class ResultSource + { + public string Result { get; set; } + } + + public class OtherDataSource + { + public string COD { get; set; } + } + + public class DataSourceContainer + { + + public IdsSource ids; + public ResultSource res; + public OtherDataSource oth; + } + + public class DataSource + { + public DataSourceContainer cont; + } + + public class IdsTarget + { + public string Ids { get; set; } + } + + public class ResultTarget + { + public string Result { get; set; } + } + + public class OtherDataTarget + { + public string COD { get; set; } + } + + public class DataTarget + { + public IdsTarget ids; + public ResultTarget res; + public OtherDataTarget oth; + } + } + // ReSharper restore InconsistentNaming + + internal static class Issue146 + { + public static class Source + { + public interface IData + { + string Id { get; set; } + } + + public interface IEmpty : IData { } + + public class Data : IEmpty + { + public string Id { get; set; } + + public string Value => "Data.Value!"; + } + + public class Container + { + public Container(string infoId) + { + Empty = new Data { Id = infoId }; + } + + public string Name { get; set; } + + public IEmpty Empty { get; } + } + } + + public static class Target + { + public class Data + { + public string Id { get; set; } + + public string Value { get; set; } + } + + public class Cont + { + public Data Info { get; set; } + + public string Name { get; set; } + } + } + } + + #endregion } } diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringEntityMapping.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringEntityMapping.cs index 558c457fd..c15b593b6 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringEntityMapping.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringEntityMapping.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.UnitTests.Configuration { + using System; using AgileMapper.Configuration; using Common; using TestClasses; @@ -73,6 +74,43 @@ public void ShouldIgnoreEntityKeysForASpecificSourceAndTargetType() } } + [Fact] + public void ShouldIgnoreEntityKeysForSpecificSourceAndTargetTypes() + { + using (var mapper = Mapper.CreateNew()) + { + var sourceId = new { Id = 999 }; + var sourceIdAndDate = new { Id = 999, DateCreated = DateTime.Now }; + + mapper.WhenMapping.MapEntityKeys(); + + mapper.WhenMapping + .From(sourceId).To() + .IgnoreEntityKeys(); + + mapper.WhenMapping + .From(sourceIdAndDate).To() + .IgnoreEntityKeys(); + + var sourceIdResult = mapper.Map(sourceId).ToANew(); + + sourceIdResult.Id.ShouldBeDefault(); + + var sourceIdAndDateResult = mapper.Map(sourceIdAndDate).ToANew(); + + sourceIdAndDateResult.Id.ShouldBeDefault(); + sourceIdAndDateResult.DateCreated.ShouldBe(sourceIdAndDate.DateCreated); + + var nonMatchingSourceResult = mapper.Map(new { Id = 987, Name = "Fred" }).ToANew(); + + nonMatchingSourceResult.Id.ShouldBe(987); + + var nonMatchingTargetResult = mapper.Map(sourceId).ToANew(); + + nonMatchingTargetResult.Id.ShouldBe(999); + } + } + [Fact] public void ShouldErrorIfDuplicateMapKeysConfigured() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs index f0f313008..d8b322d8b 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs @@ -33,6 +33,43 @@ public void ShouldPairEnumMembers() } } + // See https://github.com/agileobjects/AgileMapper/issues/138 + [Fact] + public void ShouldApplyEnumPairsToRootMappings() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check); + + var ukChequeResult = mapper.Map(PaymentTypeUk.Cheque).ToANew(); + + ukChequeResult.ShouldBe(PaymentTypeUs.Check); + + var usCheckResult = mapper.Map(PaymentTypeUs.Check).ToANew(); + + usCheckResult.ShouldBe(PaymentTypeUk.Cheque); + } + } + + [Fact] + public void ShouldApplyEnumPairsToNullableRootMappings() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check); + + var ukChequeResult = mapper.Map(PaymentTypeUk.Cheque).ToANew(); + + ukChequeResult.ShouldBe(PaymentTypeUs.Check); + + var usCheckResult = mapper.Map((PaymentTypeUs)1234).ToANew(); + + usCheckResult.ShouldBeNull(); + } + } + [Fact] public void ShouldAllowMultipleSourceToSingleTargetPairing() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs index 9d1aad259..24d48f843 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringMappingCallbacks.cs @@ -264,6 +264,28 @@ public void ShouldExecuteAPostMappingCallbackForASpecifiedTargetTypeConditionall } } + [Fact] + public void ShouldExecuteGlobalPreAndPostMappingCallbacksInARootNullableEnumMapping() + { + using (var mapper = Mapper.CreateNew()) + { + var counter = 0; + + mapper.Before + .MappingBegins + .Call(ctx => ++counter) + .And + .After + .MappingEnds + .Call(ctx => ++counter); + + var result = mapper.Map("Mrs").ToANew(); + + result.ShouldBe(Title.Mrs); + counter.ShouldBe(2); + } + } + [Fact] public void ShouldRestrictAPostMappingCallbackByTargetType() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectTrackingIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectTrackingIncorrectly.cs index 43bc28176..6164042ae 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectTrackingIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectTrackingIncorrectly.cs @@ -125,13 +125,12 @@ public void ShouldErrorIfGlobalObjectTrackingDisabledTwice() } [Fact] - public void ShouldErrorIfObjectTrackingDisabledTwice() + public void ShouldErrorIfDuplicateObjectTrackingDisabled() { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() - .To() + .From().To() .DisableObjectTracking(); var configEx = Should.Throw(() => @@ -146,5 +145,27 @@ public void ShouldErrorIfObjectTrackingDisabledTwice() configEx.Message.ShouldContain("PersonViewModel -> Person"); } } + + [Fact] + public void ShouldErrorIfRedundantObjectTrackingDisabled() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .To() + .DisableObjectTracking(); + + var configEx = Should.Throw(() => + { + mapper.WhenMapping + .From() + .To() + .DisableObjectTracking(); + }); + + configEx.Message.ShouldContain("Object tracking is already disabled"); + configEx.Message.ShouldContain("to Person"); + } + } } } diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs index 6a0b23ff0..1df1ff1bf 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSources.cs @@ -42,12 +42,10 @@ public void ShouldReverseAConfiguredMemberByMappingScope() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() - .To>() + .From().To>() .AutoReverseConfiguredDataSources() .And - .Map(ctx => ctx.Source.Id) - .To(pp => pp.Value); + .Map(ctx => ctx.Source.Id).To(pp => pp.Value); var source = new Person { Id = Guid.NewGuid() }; var result = mapper.Map(source).ToANew>(); @@ -60,6 +58,39 @@ public void ShouldReverseAConfiguredMemberByMappingScope() } } + [Fact] + public void ShouldReverseConfiguredMembersByMappingScope() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources() + .And + .Map(ctx => ctx.Source.Id).To(pp => pp.Value); + + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources() + .And + .Map(ctx => ctx.Source.Id).To(pp => pp.Value); + + var source = new Person { Id = Guid.NewGuid() }; + + var propertyResult = mapper.Map(source).ToANew>(); + propertyResult.Value.ShouldBe(source.Id); + + var reversePropertyResult = mapper.Map(propertyResult).ToANew(); + reversePropertyResult.Id.ShouldBe(source.Id); + + var fieldResult = mapper.Map(source).ToANew>(); + fieldResult.Value.ShouldBe(source.Id); + + var reverseFieldResult = mapper.Map(fieldResult).ToANew(); + reverseFieldResult.Id.ShouldBe(source.Id); + } + } + [Fact] public void ShouldReverseAConfiguredMemberByMemberScope() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs index b45c36b90..02fdecd06 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs @@ -14,7 +14,7 @@ public class WhenConfiguringReverseDataSourcesIncorrectly { [Fact] - public void ShouldErrorIfRedundantMappingScopeOptInConfigured() + public void ShouldErrorIfGlobalScopeRedundantMappingScopeOptInConfigured() { var configEx = Should.Throw(() => { @@ -23,8 +23,7 @@ public void ShouldErrorIfRedundantMappingScopeOptInConfigured() mapper.WhenMapping .AutoReverseConfiguredDataSources() .AndWhenMapping - .From() - .To>() + .From().To>() .AutoReverseConfiguredDataSources(); } }); @@ -34,7 +33,7 @@ public void ShouldErrorIfRedundantMappingScopeOptInConfigured() } [Fact] - public void ShouldErrorIfRedundantMemberScopeOptInConfigured() + public void ShouldErrorIfGlobalScopeRedundantMemberScopeOptInConfigured() { var configEx = Should.Throw(() => { @@ -43,10 +42,28 @@ public void ShouldErrorIfRedundantMemberScopeOptInConfigured() mapper.WhenMapping .AutoReverseConfiguredDataSources() .AndWhenMapping - .From() - .To>() - .Map(ctx => ctx.Source.Id) - .To(pp => pp.Value) + .From().To>() + .Map(ctx => ctx.Source.Id).To(pp => pp.Value) + .AndViceVersa(); + } + }); + + configEx.Message.ShouldContain("reversed"); + configEx.Message.ShouldContain("enabled by default"); + } + + [Fact] + public void ShouldErrorIfMappingScopeRedundantMemberScopeOptInConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources() + .And + .Map(ctx => ctx.Source.Id).To(pp => pp.Value) .AndViceVersa(); } }); @@ -56,7 +73,63 @@ public void ShouldErrorIfRedundantMemberScopeOptInConfigured() } [Fact] - public void ShouldErrorIfRedundantMappingScopeOptOutConfigured() + public void ShouldErrorIfMappingScopeRedundantDerivedMappingScopeOptInConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("already enabled"); + configEx.Message.ShouldContain("Product -> PublicProperty"); + } + + [Fact] + public void ShouldErrorIfGlobalScopeRedundantMappingScopeOptOutConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .DoNotAutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("data source reversal"); + configEx.Message.ShouldContain("disabled by default"); + } + + [Fact] + public void ShouldErrorIfGlobalScopeRedundantMemberScopeOptOutConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .Map(ctx => ctx.Source.Id).To(pp => pp.Value) + .ButNotViceVersa(); + } + }); + + configEx.Message.ShouldContain("reverse"); + configEx.Message.ShouldContain("disabled by default"); + } + + [Fact] + public void ShouldErrorIfMappingScopeRedundantMemberScopeOptOutConfigured() { var configEx = Should.Throw(() => { @@ -78,27 +151,94 @@ public void ShouldErrorIfRedundantMappingScopeOptOutConfigured() } [Fact] - public void ShouldErrorIfRedundantMemberScopeOptOutConfigured() + public void ShouldErrorIfDuplicateMappingScopeOptInConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("already enabled"); + configEx.Message.ShouldContain("Person -> PublicProperty"); + } + + [Fact] + public void ShouldErrorIfAllSourcesConflictingMappingScopeOptInConfigured() { var configEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() .To>() - .Map(ctx => ctx.Source.Id) - .To(pp => pp.Value) - .ButNotViceVersa(); + .AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); } }); - configEx.Message.ShouldContain("reverse"); - configEx.Message.ShouldContain("disabled by default"); + configEx.Message.ShouldContain("already enabled"); + configEx.Message.ShouldContain("to PublicProperty"); } [Fact] - public void ShouldErrorIfRedundantReverseDataSourceConfigured() + public void ShouldErrorIfConflictingMappingScopeOptInConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .DoNotAutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("cannot be disabled"); + configEx.Message.ShouldContain("already been enabled"); + configEx.Message.ShouldContain("Person -> PublicProperty"); + } + + [Fact] + public void ShouldErrorIfDuplicateMappingScopeOptOutConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .DoNotAutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .DoNotAutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("already disabled"); + configEx.Message.ShouldContain("Person -> PublicProperty"); + } + + [Fact] + public void ShouldErrorIfAllSourcesConflictingMappingScopeOptOutConfigured() { var configEx = Should.Throw(() => { @@ -107,13 +247,58 @@ public void ShouldErrorIfRedundantReverseDataSourceConfigured() mapper.WhenMapping.AutoReverseConfiguredDataSources(); mapper.WhenMapping - .From() .To>() + .DoNotAutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .DoNotAutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("already disabled"); + configEx.Message.ShouldContain("to PublicProperty"); + } + + [Fact] + public void ShouldErrorIfConflictingMappingScopeOptOutConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .DoNotAutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() + .AutoReverseConfiguredDataSources(); + } + }); + + configEx.Message.ShouldContain("cannot be enabled"); + configEx.Message.ShouldContain("already been disabled"); + configEx.Message.ShouldContain("Person -> PublicProperty"); + } + + [Fact] + public void ShouldErrorIfRedundantReverseDataSourceConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.AutoReverseConfiguredDataSources(); + + mapper.WhenMapping + .From().To>() .Map(p => p.Id, pp => pp.Value); mapper.WhenMapping - .From>() - .To() + .From>().To() .Map(pp => pp.Value, p => p.Id); } }); @@ -129,14 +314,12 @@ public void ShouldErrorIfRedundantExplicitReverseDataSourceConfigured() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() - .To>() + .From().To>() .Map(p => p.Id, ptf => ptf.Value1) .AndViceVersa(); mapper.WhenMapping - .From>() - .To() + .From>().To() .Map(pp => pp.Value2, p => p.Id); } }); @@ -152,8 +335,7 @@ public void ShouldErrorOnMemberScopeOptInOfConfiguredConstantDataSource() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() - .To>() + .From().To>() .Map("HELLO!").To(pf => pf.Value) .AndViceVersa(); } @@ -172,8 +354,7 @@ public void ShouldErrorOnMemberScopeOptInOfConfiguredConstantFuncDataSource() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() - .To>() + .From().To>() .Map(p => "HELLO?!", pf => pf.Value) .AndViceVersa(); } @@ -192,8 +373,7 @@ public void ShouldErrorOnMemberScopeOptInOfConfiguredReadOnlySourceMemberDataSou using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From>() - .To() + .From>().To() .Map((prof, cvm) => prof.Value).To(cvm => cvm.AddressLine1) .AndViceVersa(); } @@ -213,8 +393,7 @@ public void ShouldErrorOnMemberScopeOptInOfConfiguredSourceMemberDataSourceForWr using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From>() - .To>() + .From>().To>() .Map((pp, pwop) => pp.Value1).To(pwop => pwop.Value) .AndViceVersa(); } @@ -234,8 +413,7 @@ public void ShouldErrorOnMemberScopeOptInOfConditionalConfiguredDataSource() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From() - .To>() + .From().To>() .If((p, pf) => p.Name.Contains("Rich")) .Map(p => p.Name, pf => pf.Value) .AndViceVersa(); diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs index 1f28c2952..0b791bf1b 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs @@ -4,6 +4,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Configuration using System.Collections.Generic; using System.Linq; using AgileMapper.Configuration; + using AgileMapper.Configuration.MemberIgnores; using Common; using NetStandardPolyfills; using TestClasses; @@ -214,11 +215,12 @@ public void ShouldSupportRedundantIgnoreConflictingWithConditionalIgnore() .Ignore(cvm => cvm.Name); var matchingPersonResult = mapper.Map(new Person { Name = "Frank" }).ToANew(); - var nonMatchingPersonResult = mapper.Map(new Person { Name = "Dennis" }).ToANew(); - var customerResult = mapper.Map(new Customer { Name = "Mac" }).ToANew(); - matchingPersonResult.Name.ShouldBeNull(); + + var nonMatchingPersonResult = mapper.Map(new Person { Name = "Dennis" }).ToANew(); nonMatchingPersonResult.Name.ShouldBe("Dennis"); + + var customerResult = mapper.Map(new Customer { Name = "Mac" }).ToANew(); customerResult.Name.ShouldBeNull(); } } @@ -257,12 +259,12 @@ public void ShouldSupportSamePathIgnoredMembersWithDifferentSourceTypes() mapper.WhenMapping .From>() .To>() - .Ignore(x => x.Value); + .Ignore(pp => pp.Value); mapper.WhenMapping .From>() .To>() - .Ignore(x => x.Value); + .Ignore(pp => pp.Value); } } @@ -294,7 +296,7 @@ public void ShouldCompareIgnoredMembersConsistently() var configurations = ((IMapperInternal)mapper).Context.UserConfigurations; var ignoredMembersProperty = configurations.GetType().GetNonPublicInstanceProperty("IgnoredMembers"); var ignoredMembersValue = ignoredMembersProperty.GetValue(configurations, Enumerable.EmptyArray); - var ignoredMembers = (IList)ignoredMembersValue; + var ignoredMembers = (IList)ignoredMembersValue; ignoredMembers.Count.ShouldBe(2); diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs index 17c088a62..928a1f356 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersByFilter.cs @@ -373,8 +373,6 @@ public void ShouldIgnoreMembersBySourceTypeTargetTypeAndAttribute() .IgnoreTargetMembersWhere(member => member.HasAttribute()); var matchingSource = new PublicTwoFields { Value1 = 10, Value2 = 20 }; - var nonMatchingSource = new { Value1 = "11", Value2 = "21" }; - var matchingResult = mapper.Map(matchingSource).ToANew(); matchingResult.Value1.ShouldBeDefault(); matchingResult.Value2.ShouldBe("20"); @@ -383,6 +381,7 @@ public void ShouldIgnoreMembersBySourceTypeTargetTypeAndAttribute() nonMatchingTargetResult.Value1.ShouldBe("10"); nonMatchingTargetResult.Value2.ShouldBe("20"); + var nonMatchingSource = new { Value1 = "11", Value2 = "21" }; var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew(); nonMatchingSourceResult.Value1.ShouldBe("11"); nonMatchingSourceResult.Value2.ShouldBe("21"); diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs index 00020b925..23b3ce73d 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembersIncorrectly.cs @@ -12,6 +12,23 @@ namespace AgileObjects.AgileMapper.UnitTests.Configuration #endif public class WhenIgnoringMembersIncorrectly { + [Fact] + public void ShouldErrorIfInvalidMemberSpecified() + { + var configurationEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew>() + .Ignore(pwop => 2 * 2); + } + }); + + configurationEx.Message.ShouldContain("Unable to determine target member"); + } + [Fact] public void ShouldErrorIfRedundantIgnoreIsSpecified() { diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembers.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembers.cs new file mode 100644 index 000000000..ba5f4abed --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembers.cs @@ -0,0 +1,215 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembers + { + [Fact] + public void ShouldIgnoreAConfiguredSourceMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .IgnoreSource(id => id.Id); + + var source = new IdTesterSource { Id = "Id!", Identifier = "Identifier!" }; + var result = mapper.Map(source).ToANew(); + + result.Id.ShouldBe("Identifier!"); + } + } + + [Fact] + public void ShouldIgnoreAConfiguredSourceMemberConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew>() + .If(ctx => ctx.Source.Value < 5) + .IgnoreSource(pf => pf.Value); + + var matchingSource = new PublicField { Value = 3 }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + + matchingResult.Value.ShouldBeNull(); + + var nonMatchingSource = new PublicField { Value = 7 }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + + nonMatchingResult.Value.ShouldBe("7"); + } + } + + [Fact] + public void ShouldIgnoreAConfiguredSourceMemberForASpecifiedRuleSetConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .Over
() + .If(ctx => ctx.Source.Name == "Gandalf") + .IgnoreSource(pvm => pvm.AddressLine1); + + var source = new PersonViewModel { Name = "Gandalf", AddressLine1 = "??" }; + var overResult = mapper.Map(source).Over(new Person { Address = new Address() }); + + overResult.Name.ShouldBe("Gandalf"); + overResult.Address.Line1.ShouldBeNull(); + + var createNewResult = mapper.Map(source).ToANew(); + + createNewResult.Name.ShouldBe("Gandalf"); + createNewResult.Address.Line1.ShouldBe("??"); + } + } + + [Fact] + public void ShouldIgnoreMultipleConfiguredSourceMembers() + { + using (var mapper = Mapper.CreateNew()) + { + var source = new + { + Id = Guid.NewGuid().ToString(), + Name = "Bilbo", + AddressLine1 = "House Street", + AddressLine2 = "Town City" + }; + + mapper.WhenMapping + .From(source) + .IgnoreSource(d => d.Name, d => d.AddressLine1); + + var matchingResult = mapper.Map(source).ToANew(); + + matchingResult.Id.ToString().ShouldBe(source.Id); + matchingResult.Name.ShouldBeNull(); + matchingResult.Address.ShouldNotBeNull(); + matchingResult.Address.Line1.ShouldBeNull(); + matchingResult.Address.Line2.ShouldBe("Town City"); + } + } + + [Fact] + public void ShouldIgnoreAConfiguredSourceMemberInACollectionConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .ToANew() + .If((cvm, c) => cvm.Name.StartsWith("F")) + .IgnoreSource(cvm => cvm.Name); + + var source = new[] + { + new CustomerViewModel { Name = "Bilbo" }, + new CustomerViewModel { Name = "Frodo" } + }; + + var result = mapper.Map(source).ToANew>(); + + result.Count().ShouldBe(2); + + result.First().Name.ShouldBe("Bilbo"); + result.Second().Name.ShouldBeNull(); + } + } + + [Fact] + public void ShouldIgnoreAComplexTypeSourceMember() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSource(pfa => pfa.Value); + + var source = new PublicField
{ Value = new Address { Line1 = "Use this!" } }; + var target = new PublicReadOnlyProperty
(new Address { Line1 = "Ignore this!" }); + + mapper.Map(source).Over(target); + + target.Value.Line1.ShouldBe("Ignore this!"); + } + } + + [Fact] + public void ShouldSupportRedundantSourceIgnoreConflictingWithConditionalSourceIgnore() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To() + .If((p, pvm) => p.Name == "Frank") + .IgnoreSource(p => p.Name); + + mapper.WhenMapping + .From() + .To() + .IgnoreSource(c => c.Name); + + var matchingPersonResult = mapper.Map(new Person { Name = "Frank" }).ToANew(); + matchingPersonResult.Name.ShouldBeNull(); + + var nonMatchingPersonResult = mapper.Map(new Person { Name = "Dennis" }).ToANew(); + nonMatchingPersonResult.Name.ShouldBe("Dennis"); + + var customerResult = mapper.Map(new Customer { Name = "Mac" }).ToANew(); + customerResult.Name.ShouldBeNull(); + } + } + + [Fact] + public void ShouldSupportSamePathIgnoredSourceMembersWithDifferentTargetTypes() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .IgnoreSource(pp => pp.Value); + + mapper.WhenMapping + .From>() + .To>() + .IgnoreSource(pp => pp.Value); + } + } + + #region Helper Classes + + private class IdTesterSource + { + public string Id { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public string Identifier { get; set; } + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class IdTesterTarget + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public string Id { get; set; } + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByFilter.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByFilter.cs new file mode 100644 index 000000000..86f075c4c --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByFilter.cs @@ -0,0 +1,297 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using System; + using AgileMapper.Extensions.Internal; + using Common; + using NetStandardPolyfills; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersByFilter + { + [Fact] + public void ShouldIgnoreSourceMembersByTypeAndSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSourceMembersOfType(); + + var matchingSource = new PublicTwoFields { Value1 = 1, Value2 = 2L }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value1.ShouldBeDefault(); + matchingResult.Value2.ShouldBe(2); + + var nonMatchingSource = new PublicTwoFields { Value1 = 1L, Value2 = 2 }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value1.ShouldBe(1L); + nonMatchingResult.Value2.ShouldBe(2); + } + } + + [Fact] + public void ShouldIgnoreSourceMembersByTypeSourceTypeAndTargetType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew>() + .IgnoreSourceMembersOfType(); + + var matchingSource = new PublicProperty { Value = "999" }; + var nonMatchingSource = new { Value = 987 }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value.ShouldBeDefault(); + + var nonMatchingTargetResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingTargetResult.Value.ShouldBe(999); + + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value.ShouldBe(987); + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value.ShouldBe(987); + } + } + + [Fact] + public void ShouldIgnoreSourcePropertiesByMemberTypeAndSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSourceMembersWhere(member => member.IsProperty); + + var nonMatchingSource = new PublicField { Value = "xyz" }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value.ShouldBe("xyz"); + + var matchingSource = new PublicProperty { Value = "zyx" }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value.ShouldBeDefault(); + } + } + + [Fact] + public void ShouldIgnoreSourceFieldsBySourceTypeAndTargetType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .IgnoreSourceMembersWhere(member => member.IsField); + + var matchingSource = new PublicField { Value = 111L }; + var nonMatchingSource = new { Value = 222L }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value.ShouldBeDefault(); + + var nonMatchingTargetResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingTargetResult.Value.ShouldBe(111); + + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value.ShouldBe(222); + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value.ShouldBe(222); + } + } + + [Fact] + public void ShouldIgnoreSourceFieldsBySourceTypeTargetTypeAndFieldInfoMatcher() + { + using (var mapper = Mapper.CreateNew()) + { + var field1 = typeof(PublicTwoFields).GetPublicInstanceField("Value1"); + + mapper.WhenMapping + .From>() + .To>() + .IgnoreSourceMembersWhere(member => + member.IsFieldMatching(f => f == field1)); + + var matchingSource = new PublicTwoFields { Value1 = 111, Value2 = 222 }; + var nonMatchingSource = new { Value1 = 333, Value2 = 444 }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value1.ShouldBeNull(); + matchingResult.Value2.ShouldBe("222"); + + var nonMatchingTargetResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingTargetResult.Value1.ShouldBe("111"); + nonMatchingTargetResult.Value2.ShouldBe(222); + + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value1.ShouldBe("333"); + nonMatchingSourceResult.Value2.ShouldBe("444"); + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value1.ShouldBe(333); + nonMatchingResult.Value2.ShouldBe("444"); + } + } + + [Fact] + public void ShouldIgnoreGetMethodsByMemberTypeAndTargetType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSourceMembersWhere(member => member.IsGetMethod); + + var nonMatchingSource = new PublicProperty { Value = DateTime.Today }; + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value.ShouldBe(DateTime.Today); + + var matchingSource = new PublicGetMethod(DateTime.Today); + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value.ShouldBeDefault(); + } + } + + [Fact] + public void ShouldIgnoreGetMethodsBySourceTypeTargetTypeAndMethodInfoMatcher() + { + using (var mapper = Mapper.CreateNew()) + { + var value = 1; + + // ReSharper disable once AccessToModifiedClosure + mapper.WhenMapping + .From>() + .To>() + .IgnoreSourceMembersWhere(member => + member.IsGetMethodMatching(m => value == 1)); + + var matchingSource = new PublicGetMethod(999); + var nonMatchingSource = new { Value = 111 }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value.ShouldBeDefault(); + + value = 2; + var nonMatchingFilterResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingFilterResult.Value.ShouldBe(999); + + var nonMatchingTargetResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingTargetResult.Value.ShouldBe(999); + + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value.ShouldBe(111); + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value.ShouldBe(111); + } + } + + [Fact] + public void ShouldIgnoreSourceMembersBySourceTypeTargetTypeAndNameMatch() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .IgnoreSourceMembersWhere(member => member.Name.Contains("Value1")); + + var matchingSource = new PublicTwoFields { Value1 = 1, Value2 = 2 }; + var nonMatchingSource = new { Value1 = -1, Value2 = -2 }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value1.ShouldBeDefault(); + matchingResult.Value2.ShouldBe(2); + + var nonMatchingTargetResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingTargetResult.Value1.ShouldBe(1); + nonMatchingTargetResult.Value2.ShouldBe(2); + + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value1.ShouldBe(-1); + nonMatchingSourceResult.Value2.ShouldBe(-2); + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value1.ShouldBe(-1); + nonMatchingResult.Value2.ShouldBe(-2); + } + } + + [Fact] + public void ShouldIgnoreSourceMembersBySourceTypeAndPathMatch() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSourceMembersWhere(member => member.Path.EqualsIgnoreCase("Value.Line2")); + + var matchingSource = new PublicField
{ Value = new Address { Line1 = "Here", Line2 = "Here!" } }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value.Line1.ShouldBe("Here"); + matchingResult.Value.Line2.ShouldBeNull(); + + var nonMatchingSource = new { Value = new Address { Line1 = "There", Line2 = "There!" } }; + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value.Line1.ShouldBe("There"); + nonMatchingSourceResult.Value.Line2.ShouldBe("There!"); + } + } + + [Fact] + public void ShouldIgnoreSourceMembersBySourceTypeTargetTypeAndAttribute() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To>() + .IgnoreSourceMembersWhere(member => member.HasAttribute()); + + var matchingSource = new AttributeHelper { Value1 = 10, Value2 = 20 }; + var matchingResult = mapper.Map(matchingSource).ToANew>(); + matchingResult.Value1.ShouldBeDefault(); + matchingResult.Value2.ShouldBe("20"); + + var nonMatchingTargetResult = mapper.Map(matchingSource).ToANew>(); + nonMatchingTargetResult.Value1.ShouldBe("10"); + nonMatchingTargetResult.Value2.ShouldBe("20"); + + var nonMatchingSource = new { Value1 = "11", Value2 = "21" }; + var nonMatchingSourceResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingSourceResult.Value1.ShouldBe("11"); + nonMatchingSourceResult.Value2.ShouldBe("21"); + + var nonMatchingResult = mapper.Map(nonMatchingSource).ToANew>(); + nonMatchingResult.Value1.ShouldBe("11"); + nonMatchingResult.Value2.ShouldBe("21"); + } + } + + #region Helper Classes + + public struct AttributeHelper + { + [IgnoreMe] + public int Value1 { get; set; } + + public int Value2 { get; set; } + } + + public sealed class IgnoreMeAttribute : Attribute + { + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByGlobalFilter.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByGlobalFilter.cs new file mode 100644 index 000000000..f56dc4f25 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByGlobalFilter.cs @@ -0,0 +1,113 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersByGlobalFilter + { + [Fact] + public void ShouldIgnoreSourceMembersByType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersOfType(); + + var source = new { Value1 = 1, Value2 = 2L }; + var result = mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(1); + result.Value2.ShouldBeDefault(); + } + } + + [Fact] + public void ShouldIgnoreObjectSourceMembersByType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersOfType(); + + var source = new { Value1 = (object)"object?!", Value2 = new { Line1 = "Address!" } }; + var result = mapper.Map(source).ToANew>(); + + result.Value1.ShouldBeNull(); + result.Value2.ShouldNotBeNull(); + result.Value2.Line1.ShouldBe("Address!"); + } + } + + [Fact] + public void ShouldIgnoreDerivedComplexTypeSourceMembersByType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersOfType(); + + var source = new { value1 = 456, Value2 = new CustomerViewModel { Name = "Larry" } }; + var result = mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(456); + result.Value2.ShouldBeNull(); + } + } + + [Fact] + public void ShouldIgnoreMaptimeTypedDerivedComplexTypeSourceMembersByType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersOfType(); + + var source = new { value1 = 456, Value2 = (PersonViewModel)new CustomerViewModel { Name = "Larry" } }; + var result = mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(456); + result.Value2.ShouldBeNull(); + } + } + + [Fact] + public void ShouldIgnoreGetMethodsByMemberType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersWhere(member => member.IsGetMethod); + + var getMethodResult = mapper.Map(new PublicGetMethod(123)).ToANew>(); + getMethodResult.Value.ShouldBeDefault(); + + var fieldResult = mapper.Map(new PublicField { Value = 123 }).ToANew>(); + fieldResult.Value.ShouldBe(123); + } + } + + [Fact] + public void ShouldSupportOverlappingSourceIgnoreFilters() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersOfType
() + .AndWhenMapping + .IgnoreSourceMembersWhere(member => member.IsProperty); + + var source = new { Value = new Address { Line1 = "ONE!", Line2 = "TWO!" } }; + + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldBeNull(); + } + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByValueFilter.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByValueFilter.cs new file mode 100644 index 000000000..1331556f8 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByValueFilter.cs @@ -0,0 +1,479 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using System.Linq; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersByValueFilter + { + [Fact] + public void ShouldIgnoreSourceMemberByUntypedValueFilterGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(value => Equals(value, 123))); + + var source = new PublicTwoFields { Value1 = 123, Value2 = "456" }; + var result = mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value1.ShouldBeDefault(); + result.Value2.ShouldBe(456); + } + } + + [Fact] + public void ShouldIgnoreSourceMemberByStringValueFilterGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(str => str == "456")); + + var source = new PublicTwoFields { Value1 = 123, Value2 = "456" }; + var result = mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value1.ShouldBe("123"); + result.Value2.ShouldBeDefault(); + } + } + + [Fact] + public void ShouldIgnoreSourceMemberArrayElementByIntValueFilterGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(i => i < 10)); + + var source = new PublicField { Value = new[] { 1, 7, 11, 15 } }; + var result = mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldNotBeNull(); + result.Value.ShouldBe(11, 15); + } + } + + [Fact] + public void ShouldIgnoreSourceMemberEnumerableElementByStringValueFilterGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(str => str == "0")); + + var source = new PublicField> { Value = new[] { "1", "7", "0", "11" } }; + var result = mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldNotBeNull(); + result.Value.ShouldBe(1, 7, 11); + } + } + + [Fact] + public void ShouldIgnoreSourceMembersByMultiClauseTypedValueFiltersGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => + c.If(str => str == "123") || c.If(i => i == 123) || + (c.If(str => str == "123") && !c.If(dt => dt == DateTime.Today))); + + var matchingIntSource = new PublicField { Value = 123 }; + var matchingIntResult = mapper.Map(matchingIntSource).ToANew>(); + + matchingIntResult.ShouldNotBeNull(); + matchingIntResult.Value.ShouldBeDefault(); + + var matchingStringSource = new PublicField { Value = "123" }; + var matchingStringResult = mapper.Map(matchingStringSource).ToANew>(); + + matchingStringResult.ShouldNotBeNull(); + matchingStringResult.Value.ShouldBeNull(); + + var nonMatchingIntSource = new PublicField { Value = 456 }; + var nonMatchingIntResult = mapper.Map(nonMatchingIntSource).ToANew>(); + + nonMatchingIntResult.ShouldNotBeNull(); + nonMatchingIntResult.Value.ShouldBe(456); + + var nonMatchingStringSource = new PublicField { Value = "999" }; + var nonMatchingStringResult = mapper.Map(nonMatchingStringSource).ToANew>(); + + nonMatchingStringResult.ShouldNotBeNull(); + nonMatchingStringResult.Value.ShouldBe("999"); + + var nonMatchingTypeSource = new PublicField { Value = 123L }; + var nonMatchingTypeResult = mapper.Map(nonMatchingTypeSource).ToANew>(); + + nonMatchingTypeResult.ShouldNotBeNull(); + nonMatchingTypeResult.Value.ShouldBe("123"); + } + } + + [Fact] + public void ShouldIgnoreSourceMemberByNullableIntValueFilterGlobally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(i => i < 10)); + + var source = new PublicTwoFields { Value1 = 8, Value2 = "456" }; + var result = mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Value1.ShouldBeNull(); + result.Value2.ShouldBe(456); + + var nonMatchingFilterSource = new PublicTwoFields { Value1 = 12, Value2 = "77" }; + var nonMatchingFilterResult = mapper.Map(nonMatchingFilterSource).ToANew>(); + + nonMatchingFilterResult.ShouldNotBeNull(); + nonMatchingFilterResult.Value1.ShouldBe("12"); + nonMatchingFilterResult.Value2.ShouldBe(77L); + + var nullableSource = new PublicTwoFields { Value1 = 8, Value2 = "99" }; + var nullableResult = mapper.Map(nullableSource).ToANew>(); + + nullableResult.ShouldNotBeNull(); + nullableResult.Value1.ShouldBeNull(); + nullableResult.Value2.ShouldBe(99); + } + } + + [Fact] + public void ShouldIgnoreSourceMemberByDateTimeValueFilterAndSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSources(c => c.If(dt => dt < DateTime.Now)); + + var anHourAgo = DateTime.Now.AddHours(-1); + + var matchingSource = new PublicTwoFieldsStruct + { + Value1 = anHourAgo, + Value2 = "456" + }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + + matchingResult.ShouldNotBeNull(); + matchingResult.Value1.ShouldBeNull(); + matchingResult.Value2.ShouldBe("456"); + + var nonMatchingTypeSource = new PublicTwoFieldsStruct + { + Value1 = anHourAgo, + Value2 = "123" + }; + + var nonMatchingTypeResult = mapper.Map(nonMatchingTypeSource).ToANew>(); + + nonMatchingTypeResult.ShouldNotBeNull(); + nonMatchingTypeResult.Value1.ShouldBe(anHourAgo.ToString(CultureInfo.CurrentCulture.DateTimeFormat)); + nonMatchingTypeResult.Value2.ShouldBe("123"); + + var nonMatchingFilterSource = new PublicTwoFieldsStruct + { + Value1 = anHourAgo.AddHours(+2), + Value2 = "123" + }; + + var nonMatchingFilterResult = mapper.Map(nonMatchingFilterSource).ToANew>(); + + nonMatchingFilterResult.ShouldNotBeNull(); + nonMatchingFilterResult.Value1.ShouldBe(anHourAgo.AddHours(+2).ToString(CultureInfo.CurrentCulture.DateTimeFormat)); + nonMatchingFilterResult.Value2.ShouldBe("123"); + } + } + + [Fact] + public void ShouldIgnoreSourceMemberByNullableLongValueFilterRuleSetSourceTypeAndTargetType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew>() + .IgnoreSources(c => c.If(l => l.HasValue && l > 100L)); + + var matchingSource = new PublicTwoFieldsStruct + { + Value1 = 200L, + Value2 = "555" + }; + + var matchingResult = mapper.Map(matchingSource).ToANew>(); + + matchingResult.ShouldNotBeNull(); + matchingResult.Value1.ShouldBeNull(); + matchingResult.Value2.ShouldBe(555L); + + var nonMatchingTargetTypeResult = mapper.Map(matchingSource).ToANew>(); + + nonMatchingTargetTypeResult.ShouldNotBeNull(); + nonMatchingTargetTypeResult.Value1.ShouldBe("200"); + nonMatchingTargetTypeResult.Value2.ShouldBe(555); + + var nonMatchingRuleSetResult = mapper + .Map(matchingSource) + .Over(new PublicTwoFields { Value1 = "100", Value2 = 55L }); + + nonMatchingRuleSetResult.ShouldNotBeNull(); + nonMatchingRuleSetResult.Value1.ShouldBe("200"); + nonMatchingRuleSetResult.Value2.ShouldBe(555L); + + var nullValueSource = new PublicTwoFieldsStruct + { + Value1 = 200, + Value2 = "444" + }; + + var nullValueResult = mapper.Map(nullValueSource).ToANew>(); + + nullValueResult.ShouldNotBeNull(); + nullValueResult.Value1.ShouldBe("200"); + nullValueResult.Value2.ShouldBe(444L); + + var nonMatchingSourceTypeSource = new PublicTwoFieldsStruct + { + Value1 = 200, + Value2 = "444" + }; + + var nonMatchingSourceTypeResult = mapper.Map(nonMatchingSourceTypeSource).ToANew>(); + + nonMatchingSourceTypeResult.ShouldNotBeNull(); + nonMatchingSourceTypeResult.Value1.ShouldBe("200"); + nonMatchingSourceTypeResult.Value2.ShouldBe(444L); + + var nonMatchingFilterSource = new PublicTwoFieldsStruct + { + Value1 = 99L, + Value2 = "123" + }; + + var nonMatchingFilterResult = mapper.Map(nonMatchingFilterSource).ToANew>(); + + nonMatchingFilterResult.ShouldNotBeNull(); + nonMatchingFilterResult.Value1.ShouldBe("99"); + nonMatchingFilterResult.Value2.ShouldBe(123L); + } + } + + [Fact] + public void ShouldIgnoreConfiguredDataSourceByTimeSpanValueFilterSourceAndTargetType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map((ptf, pf) => ptf.Value1) + .To(pf => pf.Value) + .And + .IgnoreSources(c => c.If(ts => ts > TimeSpan.FromHours(1))); + + var matchingSource = new PublicTwoFieldsStruct + { + Value1 = TimeSpan.FromHours(2) + }; + + var matchingTarget = new PublicField + { + Value = TimeSpan.FromHours(1) + }; + + mapper.Map(matchingSource).Over(matchingTarget); + + matchingTarget.Value.ShouldBe(TimeSpan.FromHours(1)); + + mapper.WhenMapping + .From>() + .To>() + .Map((ptf, pf) => ptf.Value1) + .To(pf => pf.Value); + + var nonMatchingTarget = new PublicField + { + Value = TimeSpan.FromHours(1) + }; + + mapper.Map(matchingSource).Over(nonMatchingTarget); + + nonMatchingTarget.Value.ShouldBe(TimeSpan.FromHours(2)); + + mapper.WhenMapping + .From>() + .To>() + .Map((ptf, pf) => ptf.Value1) + .To(pf => pf.Value); + + var nonMatchingSourceTypeSource = new PublicTwoFieldsStruct + { + Value1 = TimeSpan.FromHours(2).ToString() + }; + + mapper.Map(nonMatchingSourceTypeSource).Over(matchingTarget); + + matchingTarget.Value.ShouldBe(TimeSpan.FromHours(2)); + + var nonMatchingFilterSource = new PublicTwoFieldsStruct + { + Value1 = TimeSpan.FromMinutes(30) + }; + + mapper.Map(nonMatchingFilterSource).Over(matchingTarget); + + matchingTarget.Value.ShouldBe(TimeSpan.FromMinutes(30)); + } + } + + [Fact] + public void ShouldIgnoreConfiguredDataSourceByIntValueFilterSourceAndTargetTypeConditionally() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .If(ctx => ctx.Source.Value1 > 100) + .Map((ptf, pf) => ptf.Value1) + .To(pf => pf.Value) + .And + .IgnoreSources(c => c.If(i => !(i < 200))); + + var matchingTarget = new PublicField + { + Value = "Value!" + }; + + var conditionFilteredSource = new PublicTwoFieldsStruct + { + Value1 = 50 + }; + + mapper.Map(conditionFilteredSource).Over(matchingTarget); + + matchingTarget.Value.ShouldBe("Value!"); + + var filterFilteredSource = new PublicTwoFieldsStruct + { + Value1 = 300 + }; + + mapper.Map(filterFilteredSource).Over(matchingTarget); + + matchingTarget.Value.ShouldBe("Value!"); + + var matchingSource = new PublicTwoFieldsStruct + { + Value1 = 150 + }; + + mapper.Map(matchingSource).Over(matchingTarget); + + matchingTarget.Value.ShouldBe("150"); + } + } + + [Fact] + public void ShouldIgnoreRootSourceMaptimeDerivedComplexTypeByFilterAndSourceTypeInMerge() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .IgnoreSources(c => + c.If(mp => mp.HowMega == decimal.Zero) || + c.If(mp => mp.HowMega == decimal.MinValue)); + + Product filteredSource = new MegaProduct { ProductId = "ABC", HowMega = decimal.MinValue }; + var target = new ProductDtoMega { ProductId = "ABC" }; + + mapper.Map(filteredSource).OnTo(target); + + target.HowMega.ShouldBeNull(); + + Product nonFilteredSource = new MegaProduct { ProductId = "ABC", HowMega = 0.25m }; + + mapper.Map(nonFilteredSource).OnTo(target); + + target.HowMega.ShouldBe("0.25"); + } + } + + [Fact] + public void ShouldIgnoreRootSourceMaptimeDerivedComplexTypeByFilterAndSourceTypeInCreateNew() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .IgnoreSources(c => + c.If(mp => mp.HowMega >= 0.1m) && + c.If(mp => mp.HowMega <= 0.5m)); + + Product filteredSource = new MegaProduct { ProductId = "ABC", HowMega = 0.4m }; + var filteredResult = mapper.Map(filteredSource).ToANew(); + + filteredResult.ShouldBeNull(); + + Product nonFilteredSource = new MegaProduct { ProductId = "ABC", HowMega = 0.6m }; + + var nonFilteredResult = mapper.Map(nonFilteredSource).ToANew(); + + nonFilteredResult.ShouldNotBeNull(); + nonFilteredResult.HowMega.ShouldBe("0.6"); + } + } + + [Fact] + public void ShouldIgnoreSourceRuntimeComplexTypeByFilterAndSourceTypeInACollectionMerge() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .IgnoreSources(c => c.If(mp => mp.HowMega < 0.1m)); + + var source = new Collection + { + new MegaProduct { ProductId = "ABC", HowMega = 0.2m }, + new Product { ProductId = "DEF" }, + new MegaProduct { ProductId = "GHI", HowMega = 0.05m } + }; + + var target = new List { new Product { ProductId = "JKL" } }; + + mapper.Map(source).OnTo(target); + + target.Count.ShouldBe(4); + target.First().ProductId.ShouldBe("JKL"); + target.Second().ProductId.ShouldBe("ABC"); + target.Second().ShouldBeOfType().HowMega.ShouldBe(0.2m); + target.Third().ProductId.ShouldBe("DEF"); + target.Fourth().ShouldBeNull(); + } + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByValueFilterIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByValueFilterIncorrectly.cs new file mode 100644 index 000000000..e9dd0606f --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersByValueFilterIncorrectly.cs @@ -0,0 +1,48 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using AgileMapper.Configuration; + using Common; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersByValueFilterIncorrectly + { + [Fact] + public void ShouldErrorIfDuplicateSourceValueFilterConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSources(c => c.If(value => value == 999)); + + mapper.WhenMapping + .IgnoreSources(c => c.If(value => value == 999)); + } + }); + + configEx.Message.ShouldContain("Source filter"); + configEx.Message.ShouldContain("If(value => value == 999)"); + configEx.Message.ShouldContain("already been configured"); + } + + [Fact] + public void ShouldErrorIfNoFiltersAreDefined() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.IgnoreSources(c => true); + } + }); + + configEx.Message.ShouldContain("At least one source filter must be specified"); + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersIncorrectly.cs new file mode 100644 index 000000000..9646b5e19 --- /dev/null +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringSourceMembersIncorrectly.cs @@ -0,0 +1,106 @@ +namespace AgileObjects.AgileMapper.UnitTests.Configuration +{ + using AgileMapper.Configuration; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenIgnoringSourceMembersIncorrectly + { + [Fact] + public void ShouldErrorIfInvalidSourceMemberSpecified() + { + var configurationEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSource(prop => 2 * 2); + } + }); + + configurationEx.Message.ShouldContain("Unable to determine source member"); + } + + [Fact] + public void ShouldErrorIfRedundantSourceIgnoreIsSpecified() + { + var ignoreEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To() + .IgnoreSource(p => p.Name); + + mapper.WhenMapping + .From() + .To() + .IgnoreSource(c => c.Name); + } + }); + + ignoreEx.Message.ShouldContain("has already been ignored"); + } + + [Fact] + public void ShouldErrorIfNonPublicWriteOnlySimpleTypeMemberSpecified() + { + var configurationEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .IgnoreSource(prop => prop.Value); + } + }); + + configurationEx.Message.ShouldContain("not readable"); + } + + [Fact] + public void ShouldErrorIfFilteredSourceMemberIsIgnored() + { + var ignoreEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersWhere(member => member.IsField); + + mapper.WhenMapping + .From>() + .IgnoreSource(pf => pf.Value); + } + }); + + ignoreEx.Message.ShouldContain("already ignored by ignore pattern"); + } + + [Fact] + public void ShouldErrorIfDuplicateFilterIsConfigured() + { + var ignoreEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .IgnoreSourceMembersOfType(); + + mapper.WhenMapping + .IgnoreSourceMembersWhere(member => member.HasType()); + } + }); + + ignoreEx.Message.ShouldContain("has already been configured"); + } + } +} diff --git a/AgileMapper.UnitTests/Configuration/WhenResolvingServices.cs b/AgileMapper.UnitTests/Configuration/WhenResolvingServices.cs index dc6bc49ef..db672d932 100644 --- a/AgileMapper.UnitTests/Configuration/WhenResolvingServices.cs +++ b/AgileMapper.UnitTests/Configuration/WhenResolvingServices.cs @@ -512,6 +512,46 @@ public void ShouldErrorIfNoServiceProviderMethodsAvailable() configEx.Message.ShouldContain("Resolve"); } + [Fact] + public void ShouldErrorIfServiceProviderMethodIsParameterless() + { + var configEx = Should.Throw(() => + Mapper.WhenMapping.UseServiceProvider(new ParameterlessInvalidServiceProvider())); + + configEx.Message.ShouldContain("No supported service provider methods were found"); + configEx.Message.ShouldContain("ParameterlessInvalidServiceProvider"); + } + + [Fact] + public void ShouldErrorIfServiceProviderMethodHasInvalidFirstParameterType() + { + var configEx = Should.Throw(() => + Mapper.WhenMapping.UseServiceProvider(new InvalidFirstParameterTypeInvalidServiceProvider())); + + configEx.Message.ShouldContain("No supported service provider methods were found"); + configEx.Message.ShouldContain("InvalidFirstParameterTypeInvalidServiceProvider"); + } + + [Fact] + public void ShouldErrorIfServiceProviderMethodHasInvalidSecondParameterType() + { + var configEx = Should.Throw(() => + Mapper.WhenMapping.UseServiceProvider(new InvalidSecondParameterTypeInvalidServiceProvider())); + + configEx.Message.ShouldContain("No supported service provider methods were found"); + configEx.Message.ShouldContain("InvalidSecondParameterTypeInvalidServiceProvider"); + } + + [Fact] + public void ShouldErrorIfServiceProviderMethodHasInvalidExtraParameterType() + { + var configEx = Should.Throw(() => + Mapper.WhenMapping.UseServiceProvider(new InvalidExtraParameterTypeInvalidServiceProvider())); + + configEx.Message.ShouldContain("No supported service provider methods were found"); + configEx.Message.ShouldContain("InvalidExtraParameterTypeInvalidServiceProvider"); + } + #region Helper Classes public interface ILogger @@ -611,6 +651,27 @@ public class GetServiceOrInstanceServiceProvider public object GetInstance(Type serviceType) => Activator.CreateInstance(serviceType); } + public class ParameterlessInvalidServiceProvider + { + public object GetService() => Activator.CreateInstance(typeof(object)); + } + + public class InvalidFirstParameterTypeInvalidServiceProvider + { + // ReSharper disable once AssignNullToNotNullAttribute + public object GetService(string serviceTypeName) => Activator.CreateInstance(Type.GetType(serviceTypeName)); + } + + public class InvalidSecondParameterTypeInvalidServiceProvider + { + public object GetService(Type serviceType, int ctorIndex) => Activator.CreateInstance(serviceType); + } + + public class InvalidExtraParameterTypeInvalidServiceProvider + { + public object GetService(Type serviceType, string name, int ctorIndex) => Activator.CreateInstance(serviceType); + } + #endregion } } diff --git a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs index 5a864b4f4..ee067899a 100644 --- a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs +++ b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs @@ -497,5 +497,72 @@ public void ShouldRestrictCustomKeysByDictionaryValueType() nonMatchingResult.Value.ShouldBe("10"); } } + + // See https://github.com/agileobjects/AgileMapper/issues/152 + [Fact] + public void ShouldMapANestedComplexTypeSourceDictionaryToTarget() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From().ToDictionariesWithValueType() + .Map(ctx => ctx.Source.Dict).ToTarget() + .And + .IgnoreTargetMembersWhere(m => m.Name == "Dict"); + + var source = new Issue152.Source.Wrapper("One", "Two", "Three"); + var result = mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.Count.ShouldBe(3); + + result.ShouldContainKey("One"); + result["One"].Value.ShouldBe("One"); + + result.ShouldContainKey("Two"); + result["Two"].Value.ShouldBe("Two"); + + result.ShouldContainKey("Three"); + result["Three"].Value.ShouldBe("Three"); + } + } + + #region Helper Classes + + public static class Issue152 + { + public static class Source + { + public class Wrapper + { + public Wrapper(params string[] inputs) + { + Dict = new Dictionary(); + + foreach (var stringValue in inputs) + { + Dict.Add(stringValue, new Data { Value = stringValue }); + } + } + + public IDictionary Dict { get; } + } + + public class Data + { + public string Value { get; set; } + } + } + + public static class Target + { + public class Data + { + public string Value { get; set; } + } + } + } + + #endregion } } diff --git a/AgileMapper.UnitTests/MapperCloning/WhenCloningDictionarySettings.cs b/AgileMapper.UnitTests/MapperCloning/WhenCloningDictionarySettings.cs new file mode 100644 index 000000000..347f8c936 --- /dev/null +++ b/AgileMapper.UnitTests/MapperCloning/WhenCloningDictionarySettings.cs @@ -0,0 +1,41 @@ +namespace AgileObjects.AgileMapper.UnitTests.MapperCloning +{ + using System.Collections.Generic; + using Common; + using TestClasses; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenCloningDictionarySettings + { + [Fact] + public void ShouldCloneFullAndMemberKeys() + { + using (var baseMapper = Mapper.CreateNew()) + { + baseMapper.WhenMapping + .FromDictionariesWithValueType().To() + .MapFullKey("BlahBlah").To(p => p.ProductId) + .And + .MapMemberNameKey("ProductPrice").To(p => p.Price); + + using (var clonedMapper = baseMapper.CloneSelf()) + { + var source = new Dictionary + { + ["BlahBlah"] = "DictionaryAdventures.co.uk", + ["ProductPrice"] = 12.00 + }; + var result = clonedMapper.Map(source).ToANew(); + + result.ProductId.ShouldBe("DictionaryAdventures.co.uk"); + result.Price.ShouldBe(12.00); + } + } + } + } +} diff --git a/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs b/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs index 0057b8f72..c2f20b8d1 100644 --- a/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs +++ b/AgileMapper.UnitTests/Members/WhenFindingDataSources.cs @@ -53,7 +53,7 @@ private IQualifiedMember GetMatchingSourceMember( var childMapperData = new ChildMemberMapperData(targetMember, rootMapperData); var childMappingContext = rootMappingData.GetChildMappingData(childMapperData); - return SourceMemberMatcher.GetMatchFor(childMappingContext, out _); + return SourceMemberMatcher.GetMatchFor(childMappingContext).SourceMember; } #region Helper Classes diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs index 220f09979..3e06a3cca 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -120,6 +120,14 @@ public void ShouldMapAMatchingStringOnToAnEnumCaseInsensitively() result.Value.ShouldBe(Miss); } + [Fact] + public void ShouldMapAMatchingStringOnToARootEnum() + { + var result = Mapper.Map(Mrs.ToString()).ToANew(); + + result.ShouldBe(Mrs); + } + [Fact] public void ShouldMapAMatchingNumericStringOverAnEnum() { @@ -176,6 +184,14 @@ public void ShouldMapAnEnumToAnEnum() result.Value.ShouldBe(Mrs); } + [Fact] + public void ShouldMapAnEnumToARootEnum() + { + var result = Mapper.Map(TitleShortlist.Mrs).ToANew<Title>(); + + result.ShouldBe(Mrs); + } + [Fact] public void ShouldMapANonMatchingEnumToANullableEnum() { diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToStrings.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToStrings.cs index c28da7a2f..791678dec 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToStrings.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToStrings.cs @@ -216,5 +216,17 @@ public void ShouldMapAnEnumOnToAString() result.Value.ShouldBe("Dr"); } + + // See https://github.com/agileobjects/AgileMapper/issues/153 + [Fact] + public void ShouldMapAnInterfaceToAString() + { + var source = new { Value = (IPublicInterface<string>)new PublicImplementation<string> { Value = "123" } }; + var result = Mapper.Map(source).ToANew<PublicField<string>>(); + + result.ShouldNotBeNull(); + result.Value.ShouldNotBeNull(); + result.Value.ShouldContain(typeof(PublicImplementation<string>).Name); + } } } diff --git a/AgileMapper.UnitTests/TestClasses/PublicEnumerable.cs b/AgileMapper.UnitTests/TestClasses/PublicEnumerable.cs new file mode 100644 index 000000000..f8d385787 --- /dev/null +++ b/AgileMapper.UnitTests/TestClasses/PublicEnumerable.cs @@ -0,0 +1,34 @@ +namespace AgileObjects.AgileMapper.UnitTests.TestClasses +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + + internal class PublicEnumerable<T> : IEnumerable<T> + { + private readonly List<T> _items; + + public PublicEnumerable() + : this(new List<T>()) + { + } + + private PublicEnumerable(List<T> items) + { + _items = items; + } + + public static PublicEnumerable<T> Parse(string values) + { + return new PublicEnumerable<T>(values + .Split(',') + .Select(v => (T)Convert.ChangeType(v, typeof(T))) + .ToList()); + } + + public IEnumerator<T> GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/TestClasses/PublicIndex.cs b/AgileMapper.UnitTests/TestClasses/PublicIndex.cs new file mode 100644 index 000000000..da4ccfded --- /dev/null +++ b/AgileMapper.UnitTests/TestClasses/PublicIndex.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.TestClasses +{ + using System.Collections.Generic; + + public class PublicIndex<TIndex, TValue> + { + private readonly Dictionary<TIndex, TValue> _data; + + public PublicIndex() + { + _data = new Dictionary<TIndex, TValue>(); + } + + public TValue this[TIndex index] + { + get => _data.TryGetValue(index, out var value) ? value : default(TValue); + set => _data[index] = value; + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests/WhenMappingToConstructors.cs b/AgileMapper.UnitTests/WhenMappingToConstructors.cs index fefc53c98..18ade044b 100644 --- a/AgileMapper.UnitTests/WhenMappingToConstructors.cs +++ b/AgileMapper.UnitTests/WhenMappingToConstructors.cs @@ -98,6 +98,16 @@ public void ShouldIgnoreACopyConstructor() result.StringValue.ShouldBe("Copy!"); } + // See https://github.com/agileobjects/AgileMapper/issues/139 + [Fact] + public void ShouldPopulateMembersMatchingUnusedConstructorParameters() + { + var source = new { Value1 = 123 }; + var result = Mapper.Map(source).ToANew<MultipleUnusedConstructors<int, int>>(); + + result.Value1.ShouldBe(123); + } + #region Helper Classes // ReSharper disable ClassNeverInstantiated.Local @@ -124,6 +134,23 @@ public MultipleConstructors(T1 value1, T2 value2) public T2 Value2 { get; } } + private class MultipleUnusedConstructors<T1, T2> + { + public MultipleUnusedConstructors() + { + } + + public MultipleUnusedConstructors(T1 value1, T2 value2) + { + Value1 = value1; + Value2 = value2; + } + + public T1 Value1 { get; set; } + + public T2 Value2 { get; set; } + } + private class CopyConstructor { public CopyConstructor() diff --git a/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs b/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs index 919f9f61c..8ef332a8a 100644 --- a/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Linq; using AgileMapper.Extensions; using Common; using TestClasses; @@ -144,6 +145,18 @@ public void ShouldPopulateAnEmptyHasEnumerableMemberNameMemberWithFalse() result.HasValue.ShouldBeFalse(); } + [Fact] + public void ShouldPopulateAHasQueryableMemberNameMember() + { + var source = new PublicField<IQueryable<Address>> + { + Value = new[] { new Address { Line1 = "Queryable?!" } }.AsQueryable() + }; + var result = Mapper.Map(source).ToANew<PublicHasValue<Address[]>>(); + + result.HasValue.ShouldBeTrue(); + } + [Fact] public void ShouldPopulateAnIntHasMemberNameMember() { @@ -266,6 +279,16 @@ public void ShouldPopulateAnEmptyFirstArrayMemberNameMemberToNull() result.FirstItem.ShouldBeNull(); } + [Fact] + public void ShouldPopulateAnEmptyFirstQueryableMemberNameMemberToNull() + { + var source = new { Items = Enumerable<Address>.EmptyArray.AsQueryable() }; + var result = Mapper.Map(source).ToANew<PublicFirstItem<Address, IList<Address>>>(); + + result.Items.ShouldBeEmpty(); + result.FirstItem.ShouldBeNull(); + } + [Fact] public void ShouldPopulateAFirstListMemberNameMember() { @@ -545,6 +568,16 @@ public void ShouldPopulateAListLongCountMember() result.ValueCount.ShouldBe(5L); } + [Fact] + public void ShouldPopulateAQueryableLongCountMember() + { + var source = new { Values = new[] { "1", "2", "3", "4" }.AsQueryable() }; + var result = Mapper.Map(source).ToANew<PublicValuesCount<long, Collection<string>>>(); + + result.Values.Count.ShouldBe(4); + result.ValueCount.ShouldBe(4L); + } + [Fact] public void ShouldNotPopulateANonNumericCountMember() { diff --git a/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs b/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs index 3ef2b75f4..979889b96 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs @@ -204,6 +204,19 @@ public void ShouldMapASourcePropertyToMultipleTargets() result.CurrencyId.ShouldBe(1); } + // See https://github.com/agileobjects/AgileMapper/issues/146 + [Fact] + public void ShouldMapToATargetInterfaceMembersImplementedInterfaceMembers() + { + var source = new Issue146.Source.Container("999") { Name = "Source" }; + var result = Mapper.Map(source).ToANew<Issue146.Target.Cont>(); + + result.ShouldNotBeNull(); + result.Name.ShouldBe("Source"); + result.Data.ShouldNotBeNull(); + result.Data.Id.ShouldBe("999"); + } + [Fact] public void ShouldAccessAParentContextInAStandaloneMapper() { @@ -363,7 +376,7 @@ public void ShouldHandleRuntimeTypedComplexAndEnumerableElementMembers() result.Value2.Count.ShouldBe(2); result.Value2.First().ShouldBeOfType<PublicProperty<string>>(); ((PublicProperty<string>)result.Value2.First()).Value.ShouldBe("ikjhfeslkjdw"); - + result.Value2.Second().ShouldBeOfType<PublicField<string>>(); ((PublicField<string>)result.Value2.Second()).Value.ShouldBe("ldkjkdhusdiuoji"); } @@ -385,6 +398,56 @@ private class Currency public int Id { get; set; } } + internal static class Issue146 + { + public static class Source + { + public class Data + { + public string Id { get; set; } + } + + public class Container + { + public Container(string dataId) + { + Data = new Data { Id = dataId }; + } + + public string Name { get; set; } + + public Data Data { get; } + } + } + + public static class Target + { + public interface IData + { + string Id { get; set; } + } + + public interface IEmpty : IData { } + + public class Empty : IEmpty + { + public string Id { get; set; } + } + + public class Cont + { + public Cont() + { + Data = new Empty(); + } + + public IEmpty Data { get; set; } + + public string Name { get; set; } + } + } + } + #endregion } } diff --git a/AgileMapper.UnitTests/WhenValidatingMappings.cs b/AgileMapper.UnitTests/WhenValidatingMappings.cs index d98ac94b6..325e0a420 100644 --- a/AgileMapper.UnitTests/WhenValidatingMappings.cs +++ b/AgileMapper.UnitTests/WhenValidatingMappings.cs @@ -17,255 +17,369 @@ public class WhenValidatingMappings [Fact] public void ShouldSupportCachedMapperValidation() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.GetPlanFor<PublicProperty<string>>().ToANew<PublicProperty<int>>(); + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlanFor<PublicProperty<string>>().ToANew<PublicProperty<int>>(); - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] public void ShouldErrorIfCachedMappingMembersHaveNoDataSources() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper.GetPlanFor(new { Thingy = default(string) }).ToANew<PublicProperty<long>>(); - - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("AnonymousType<string> -> PublicProperty<long>"); - validationEx.Message.ShouldContain("Rule set: CreateNew"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicProperty<long>.Value"); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlanFor(new { Thingy = default(string) }).ToANew<PublicProperty<long>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("AnonymousType<string> -> PublicProperty<long>"); + validationEx.Message.ShouldContain("Rule set: CreateNew"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("PublicProperty<long>.Value"); } [Fact] public void ShouldErrorIfCachedNestedMappingMembersHaveNoDataSources() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper - .GetPlanFor(new + using (var mapper = Mapper.CreateNew()) + { + var exampleSource = new { Id = default(string), Title = default(int), Name = default(string) - }) - .Over<Person>(); - - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain(" -> Person"); - validationEx.Message.ShouldNotContain(" -> Person.Address"); - validationEx.Message.ShouldContain("Rule set: Overwrite"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("Person.Address"); - validationEx.Message.ShouldContain("Person.Address.Line1"); - validationEx.Message.ShouldContain("Person.Address.Line2"); - } + }; + + mapper.GetPlanFor(exampleSource).Over<Person>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain(" -> Person"); + validationEx.Message.ShouldNotContain(" -> Person.Address"); + validationEx.Message.ShouldContain("Rule set: Overwrite"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("Person.Address"); + validationEx.Message.ShouldContain("Person.Address.Line1"); + validationEx.Message.ShouldContain("Person.Address.Line2"); } [Fact] public void ShouldNotErrorIfCachedMappingMemberIsIgnored() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .From<PublicProperty<string>>().To<PublicField<int>>() - .Ignore(pf => pf.Value); - - string plan = mapper.GetPlanFor<PublicProperty<string>>().OnTo<PublicField<int>>(); - - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From<PublicProperty<string>>().To<PublicField<int>>() + .Ignore(pf => pf.Value); + + // ReSharper disable once UnusedVariable + string plan = mapper.GetPlanFor<PublicProperty<string>>().OnTo<PublicField<int>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] public void ShouldNotErrorIfUnmappedCachedMappingMemberIsIgnored() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .From(new { LaLaLa = default(int) }).To<PublicField<int>>() - .Ignore(pf => pf.Value); + var exampleSource = new { LaLaLa = default(int) }; - mapper.GetPlanFor(new { LaLaLa = default(int) }).OnTo<PublicField<int>>(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From(exampleSource).To<PublicField<int>>() + .Ignore(pf => pf.Value); - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + mapper.GetPlanFor(exampleSource).OnTo<PublicField<int>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] public void ShouldErrorIfComplexTypeMemberIsUnconstructable() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper - .GetPlanFor<PublicField<PublicField<int>>>() - .ToANew<PublicProperty<PublicTwoParamCtor<int, int>>>(); - - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("PublicField<PublicField<int>> -> PublicProperty<PublicTwoParamCtor<int, int>>"); - validationEx.Message.ShouldContain("Unconstructable target Types"); - validationEx.Message.ShouldContain("PublicField<int> -> PublicTwoParamCtor<int, int>"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicProperty<PublicTwoParamCtor<int, int>>.Value"); - } + using (var mapper = Mapper.CreateNew()) + { + var exampleSource = new { Value = new { Value2 = default(int) } }; + + mapper + .GetPlanFor(exampleSource) + .ToANew<PublicProperty<PublicTwoParamCtor<int, int>>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("AnonymousType<AnonymousType<int>> -> PublicProperty<PublicTwoParamCtor<int, int>>"); + validationEx.Message.ShouldContain("Unconstructable target Types"); + validationEx.Message.ShouldContain("AnonymousType<int> -> PublicTwoParamCtor<int, int>"); + } + + [Fact] + public void ShouldRecogniseConstructableComplexTypeMembersWithNoMappableMembers() + { + var validationEx = Should.Throw<MappingValidationException>(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper + .GetPlanFor<PublicProperty<PublicTwoFieldsStruct<int, int>>>() + .ToANew<PublicField<PublicField<int>>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("PublicProperty<PublicTwoFieldsStruct<int, int>> -> PublicField<PublicField<int>>"); + validationEx.Message.ShouldNotContain("Unconstructable target Types"); + validationEx.Message.ShouldNotContain("PublicTwoFieldsStruct<int, int> -> PublicField<int>"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("- PublicField<PublicField<int>>.Value.Value"); } [Fact] public void ShouldNotErrorIfConstructableComplexTypeMemberHasNoMatchingSource() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .From<CustomerViewModel>() - .To<Customer>() - .Ignore(c => c.Title, c => c.Address.Line2); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From<CustomerViewModel>() + .To<Customer>() + .Ignore(c => c.Title, c => c.Address.Line2); + + mapper.GetPlanFor<CustomerViewModel>().ToANew<Customer>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + } - mapper.GetPlanFor<CustomerViewModel>().ToANew<Customer>(); + [Fact] + public void ShouldNotErrorIfUnconstructableComplexTypeMemberHasFactoryMethod() + { + Should.NotThrow(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlanFor<PublicField<string>>().ToANew<UnconstructableFactoryMethod>(); - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] - public void ShouldErrorIfEnumerableMemberHasNonEnumerableSource() + public void ShouldNotErrorIfUnconstructableComplexTypeMemberHasConfiguredFactoryMethod() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper - .GetPlanFor<PublicField<Address>>() - .ToANew<PublicProperty<ICollection<string>>>(); - - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From<Address>() + .ToANew<PublicCtor<string>>() + .CreateInstancesUsing(ctx => new PublicCtor<string>(ctx.Source.Line1)); + + mapper + .GetPlanFor<PublicField<Address>>() + .ToANew<PublicField<PublicCtor<string>>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + } - validationEx.Message.ShouldContain("PublicField<Address> -> PublicProperty<ICollection<string>>"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicProperty<ICollection<string>>.Value"); - } + [Fact] + public void ShouldErrorIfEnumerableMemberHasNonEnumerableSource() + { + var validationEx = Should.Throw<MappingValidationException>(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper + .GetPlanFor<PublicField<Address>>() + .ToANew<PublicProperty<ICollection<string>>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("PublicField<Address> -> PublicProperty<ICollection<string>>"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("PublicProperty<ICollection<string>>.Value"); } [Fact] public void ShouldErrorIfEnumerableMemberHasUnconstructableElements() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper - .GetPlanFor<Address[]>() - .ToANew<PublicCtor<string>[]>(); - - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("Address[] -> PublicCtor<string>[]"); - validationEx.Message.ShouldContain("Unconstructable target Types"); - validationEx.Message.ShouldContain("Address -> PublicCtor<string>"); - } + using (var mapper = Mapper.CreateNew()) + { + mapper + .GetPlanFor<Address[]>() + .ToANew<PublicCtor<string>[]>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("Address[] -> PublicCtor<string>[]"); + validationEx.Message.ShouldContain("Unconstructable target Types"); + validationEx.Message.ShouldContain("Address -> PublicCtor<string>"); } [Fact] public void ShouldShowMultipleIncompleteCachedMappingPlans() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper.GetPlansFor<Person>().To<ProductDto>(); - mapper.GetPlansFor<Product>().To<PersonViewModel>(); - - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("Person -> ProductDto"); - validationEx.Message.ShouldNotContain("ProductDto.ProductId"); // <- Because PVM has a PersonId - validationEx.Message.ShouldContain("ProductDto.Price"); - - validationEx.Message.ShouldContain("Product -> PersonViewModel"); - validationEx.Message.ShouldContain("PersonViewModel.Name"); - validationEx.Message.ShouldContain("PersonViewModel.AddressLine1"); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlansFor<Person>().To<ProductDto>(); + mapper.GetPlansFor<Product>().To<PersonViewModel>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("Person -> ProductDto"); + validationEx.Message.ShouldNotContain("ProductDto.ProductId"); // <- Because PVM has a PersonId + validationEx.Message.ShouldContain("ProductDto.Price"); + + validationEx.Message.ShouldContain("Product -> PersonViewModel"); + validationEx.Message.ShouldContain("PersonViewModel.Name"); + validationEx.Message.ShouldContain("PersonViewModel.AddressLine1"); } [Fact] public void ShouldValidateMappingPlanMemberMappingByDefault() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.GetPlanFor(new { Head = "Spinning" }).ToANew<PublicField<string>>()); + mapper.GetPlanFor(new { Head = "Spinning" }).ToANew<PublicField<string>>(); + } + }); - validationEx.Message.ShouldContain("AnonymousType<string> -> PublicField<string>"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicField<string>.Value"); - } + validationEx.Message.ShouldContain("AnonymousType<string> -> PublicField<string>"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("PublicField<string>.Value"); } [Fact] public void ShouldNotErrorIfUnmappedMemberHasConfiguredDataSourceWhenValidatingMappingPlansByDefault() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .ThrowIfAnyMappingPlanIsIncomplete() - .AndWhenMapping - .From<Product>().To<PublicProperty<Guid>>() - .Map((p, pp) => p.ProductId) - .To(p => p.Value); - - Should.NotThrow(() => - mapper.GetPlansFor<Product>().To<PublicProperty<Guid>>()); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ThrowIfAnyMappingPlanIsIncomplete() + .AndWhenMapping + .From<Product>().To<PublicProperty<Guid>>() + .Map((p, pp) => p.ProductId) + .To(p => p.Value); + + mapper.GetPlansFor<Product>().To<PublicProperty<Guid>>(); + } + }); } [Fact] public void ShouldValidateMappingPlanEnumMatchingByDefault() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw<MappingValidationException>(() => { - mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); - var validationEx = Should.Throw<MappingValidationException>(() => - mapper.Map(new PublicField<PaymentTypeUk>()).ToANew<PublicField<PaymentTypeUs>>()); + mapper.Map(new PublicField<PaymentTypeUk>()).ToANew<PublicField<PaymentTypeUs>>(); + } + }); - validationEx.Message.ShouldContain("PublicField<PaymentTypeUk> -> PublicField<PaymentTypeUs>"); - validationEx.Message.ShouldContain("Unpaired enum values"); - validationEx.Message.ShouldContain("PaymentTypeUk.Cheque matches no PaymentTypeUs"); - } + validationEx.Message.ShouldContain("PublicField<PaymentTypeUk> -> PublicField<PaymentTypeUs>"); + validationEx.Message.ShouldContain("Unpaired enum values"); + validationEx.Message.ShouldContain("PaymentTypeUk.Cheque matches no PaymentTypeUs"); } [Fact] public void ShouldNotErrorIfEnumMismatchesAreAllTargetToSource() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); - Should.NotThrow(() => - mapper.GetPlanFor<PublicField<PaymentTypeUk>>().ToANew<PublicField<PaymentType>>()); - } + mapper.GetPlanFor<PublicField<PaymentTypeUk>>().ToANew<PublicField<PaymentType>>(); + } + }); } [Fact] public void ShouldNotErrorIfEnumValuesArePairedWhenValidatingMappingPlansByDefault() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .ThrowIfAnyMappingPlanIsIncomplete() - .AndWhenMapping - .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ThrowIfAnyMappingPlanIsIncomplete() + .AndWhenMapping + .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check); + + mapper.Map(new PublicField<PaymentTypeUk>()).ToANew<PublicField<PaymentTypeUs>>(); + } + }); + } - Should.NotThrow(() => - mapper.Map(new PublicField<PaymentTypeUk>()).ToANew<PublicField<PaymentTypeUs>>()); + #region Helper Classes + + private class UnconstructableFactoryMethod + { + private UnconstructableFactoryMethod(string valueString) + { + Value = valueString; } + + // ReSharper disable once UnusedMember.Local + public static UnconstructableFactoryMethod Create(string value) + => new UnconstructableFactoryMethod(value); + + // ReSharper disable once MemberCanBePrivate.Local + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public string Value { get; } } + + #endregion } } diff --git a/AgileMapper.UnitTests/WhenViewingMappingPlans.cs b/AgileMapper.UnitTests/WhenViewingMappingPlans.cs index 802d8a5fc..38a754202 100644 --- a/AgileMapper.UnitTests/WhenViewingMappingPlans.cs +++ b/AgileMapper.UnitTests/WhenViewingMappingPlans.cs @@ -327,6 +327,17 @@ public void ShouldNotAttemptUnnecessaryObjectCreationCallbacks() } } + // See https://github.com/agileobjects/AgileMapper/issues/146 + [Fact] + public void ShouldUseBaseInterfaceTypeSourceMembersWithoutRuntimeTyping() + { + string plan = Mapper + .GetPlanFor<Issue146.Source.Container>() + .ToANew<Issue146.Target.Cont>(); + + plan.ShouldContain("data.Id = cToCData.Source.Info.Id;"); + } + [Fact] public void ShouldShowEnumMismatches() { @@ -408,5 +419,56 @@ public void ShouldShowAllCachedMappingPlans() mapper.RootMapperCountShouldBe(5); } } + + #region Helper Classes + + internal static class Issue146 + { + public static class Source + { + public interface IData + { + string Id { get; set; } + } + + public interface IEmpty : IData { } + + public class Data : IEmpty + { + public string Id { get; set; } + } + + public class Container + { + public Container(string infoId) + { + Info = new Data { Id = infoId }; + } + + public string Name { get; set; } + + public IEmpty Info { get; } + } + } + + public static class Target + { + public class Data + { + public string Id { get; set; } + + public string Value { get; set; } + } + + public class Cont + { + public Data Info { get; set; } + + public string Name { get; set; } + } + } + } + + #endregion } } diff --git a/AgileMapper.UnitTests/WhenWorkingWithQueryStrings.cs b/AgileMapper.UnitTests/WhenWorkingWithQueryStrings.cs new file mode 100644 index 000000000..dbf1eab14 --- /dev/null +++ b/AgileMapper.UnitTests/WhenWorkingWithQueryStrings.cs @@ -0,0 +1,144 @@ +namespace AgileObjects.AgileMapper.UnitTests +{ + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using Common; +#if !NET35 + using Xunit; +#else + using Fact = NUnit.Framework.TestAttribute; + + [NUnit.Framework.TestFixture] +#endif + public class WhenWorkingWithQueryStrings + { + [Fact] + public void ShouldParseANameValueString() + { + var qs = QueryString.Parse("data=value"); + + qs.ShouldHaveSingleItem(); + qs.ShouldContainKeyAndValue("data", "value"); + } + + [Fact] + public void ShouldHandleALeadingQuestionMark() + { + var qs = QueryString.Parse("?key=value"); + + qs.ShouldHaveSingleItem(); + qs.ShouldContainKeyAndValue("key", "value"); + } + + [Fact] + public void ShouldExplicitlyConvertToString() + { + var qs = QueryString.Parse("key=value"); + + var stringQs = (string)qs; + + stringQs.ShouldBe("key=value"); + } + + [Fact] + public void ShouldEnumerateKeysAndValues() + { + var qs = QueryString.Parse("?key1=value1&key2=value2&key3=value3"); + + var i = 1; + + foreach (var keyAndValue in qs) + { + keyAndValue.Key.ShouldBe("key" + i); + keyAndValue.Value.ShouldBe("value" + i); + + ++i; + } + } + + [Fact] + public void ShouldActAsAnIDictionary() + { + IDictionary<string, string> qs = QueryString.Parse("key1=value1"); + + qs.ShouldHaveSingleItem().Key.ShouldBe("key1"); + + qs.Add("key2", "value2"); + + qs.Keys.Count.ShouldBe(2); + qs.Keys.First().ShouldBe("key1"); + qs.Keys.Second().ShouldBe("key2"); + + qs.Values.Count.ShouldBe(2); + qs.Values.First().ShouldBe("value1"); + qs.Values.Second().ShouldBe("value2"); + + qs.Count.ShouldBe(2); + qs.First().Key.ShouldBe("key1"); + qs.First().Value.ShouldBe("value1"); + qs.Second().Key.ShouldBe("key2"); + qs.Second().Value.ShouldBe("value2"); + + qs.ContainsKey("key1").ShouldBeTrue(); + qs.Remove("key1"); + qs.ContainsKey("key1").ShouldBeFalse(); + + qs.ShouldHaveSingleItem().Key.ShouldBe("key2"); + + qs.TryGetValue("key2", out var value2).ShouldBeTrue(); + value2.ShouldBe("value2"); + + qs["key2"].ShouldBe("value2"); + qs["key2"] = "magic!"; + qs["key2"].ShouldBe("magic!"); + } + + [Fact] + public void ShouldActAsAnICollection() + { + ICollection<KeyValuePair<string, string>> qs = QueryString.Parse("key1=value1"); + + qs.IsReadOnly.ShouldBeFalse(); + qs.ShouldHaveSingleItem().Key.ShouldBe("key1"); + + qs.Add(new KeyValuePair<string, string>("key2", "value2")); + + qs.Count.ShouldBe(2); + qs.First().Key.ShouldBe("key1"); + qs.First().Value.ShouldBe("value1"); + qs.Second().Key.ShouldBe("key2"); + qs.Second().Value.ShouldBe("value2"); + + var keyValue1 = new KeyValuePair<string, string>("key1", "value1"); + + qs.Contains(keyValue1).ShouldBeTrue(); + qs.Remove(keyValue1); + qs.Contains(keyValue1).ShouldBeFalse(); + + qs.ShouldHaveSingleItem().Key.ShouldBe("key2"); + + var copyCollection = new KeyValuePair<string, string>[1]; + + qs.CopyTo(copyCollection, 0); + + copyCollection[0].Key.ShouldBe("key2"); + copyCollection[0].Value.ShouldBe("value2"); + + qs.Clear(); + qs.ShouldBeEmpty(); + } + + [Fact] + public void ShouldActAsAnIEnumerable() + { + IEnumerable qs = QueryString.Parse("key1=value1"); + + foreach (KeyValuePair<string, string> keyAndValue in qs) + { + keyAndValue.Key.ShouldBe("key1"); + keyAndValue.Value.ShouldBe("value1"); + } + } + } +} diff --git a/AgileMapper.UnitTests/packages.config b/AgileMapper.UnitTests/packages.config index d7c2d5300..71b5a86a6 100644 --- a/AgileMapper.UnitTests/packages.config +++ b/AgileMapper.UnitTests/packages.config @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <packages> <package id="AgileObjects.NetStandardPolyfills" version="1.4.0" targetFramework="net461" /> - <package id="AgileObjects.ReadableExpressions" version="2.1.1" targetFramework="net461" /> + <package id="AgileObjects.ReadableExpressions" version="2.3.2" targetFramework="net461" /> <package id="Microsoft.Extensions.Primitives" version="2.0.0" targetFramework="net461" /> <package id="System.Runtime.CompilerServices.Unsafe" version="4.5.2" targetFramework="net461" /> <package id="xunit" version="2.4.1" targetFramework="net461" /> diff --git a/AgileMapper/AgileMapper.csproj b/AgileMapper/AgileMapper.csproj index c50ec3d3a..dfb549dec 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -18,16 +18,21 @@ <GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute> <RootNamespace>AgileObjects.AgileMapper</RootNamespace> <Copyright>Copyright © AgileObjects Ltd 2019</Copyright> - <PackageReleaseNotes>- Support for mapping callbacks in .ToTarget() data sources -- Fixing constructability check for .ToTarget() data sources re: #129 -- Fixing .ToTarget() calls with source Dictionaries, re: #133 -- Fixing complex type Dictionary merging + <PackageReleaseNotes>- Support for ignoring source members by member, member filter and value filter, re: #137 +- Support for root enum mapping, re: #138 +- Support for mapping to and from interface member implemented interface members, re: #146 +- Support for mapping (not projecting) queryables +- Improved detection of constructor-populated members, re: #139 +- Fixing nullable DateTime to string custom format string configuration, re: #149 +- Fixing nested .ToTarget() source member null-checks, re: #145 +- Fixing configured member conflict detection bug #152 +- Fixing interface-to-string mapping using ToString(), re #153 - Performance improvements</PackageReleaseNotes> </PropertyGroup> <ItemGroup> <PackageReference Include="AgileObjects.NetStandardPolyfills" Version="1.4.0" /> - <PackageReference Include="AgileObjects.ReadableExpressions" Version="2.1.1" /> + <PackageReference Include="AgileObjects.ReadableExpressions" Version="2.3.2" /> </ItemGroup> <ItemGroup Condition="'$(TargetFramework)' == 'net35'"> diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index 5b0ea7525..6a04e1c7f 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -7,7 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; - using DataSources; + using DataSources.Factories; using Extensions; using Extensions.Internal; using Members; @@ -24,7 +24,7 @@ #endif internal class CustomDataSourceTargetMemberSpecifier<TSource, TTarget> : - ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget>, + ICustomDataSourceTargetMemberSpecifier<TSource, TTarget>, ICustomProjectionDataSourceTargetMemberSpecifier<TSource, TTarget> { private readonly MappingConfigInfo _configInfo; @@ -116,29 +116,27 @@ private void ThrowIfRedundantSourceMember(ConfiguredLambdaInfo valueLambdaInfo, var targetMemberMapperData = new ChildMemberMapperData(targetMember, mappingData.MapperData); var targetMemberMappingData = mappingData.GetChildMappingData(targetMemberMapperData); - var bestMatchingSourceMember = SourceMemberMatcher.GetMatchFor(targetMemberMappingData, out _); + var bestSourceMemberMatch = SourceMemberMatcher.GetMatchFor(targetMemberMappingData); - if (bestMatchingSourceMember == null) + if (!bestSourceMemberMatch.IsUseable) { return; } - var sourceMember = sourceMemberLambda.ToSourceMember(MapperContext); + var configuredSourceMember = sourceMemberLambda.ToSourceMember(MapperContext); - if (!bestMatchingSourceMember.Matches(sourceMember)) + if (!bestSourceMemberMatch.SourceMember.Matches(configuredSourceMember)) { return; } - var targetMemberType = (targetMember.LeafMember.MemberType == MemberType.ConstructorParameter) - ? "constructor parameter" - : "member"; + var targetMemberType = targetMember.IsConstructorParameter() ? "constructor parameter" : "member"; throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, "Source member {0} will automatically be mapped to target {1} {2}, " + "and does not need to be configured", - GetSourceMemberDescription(sourceMember), + GetSourceMemberDescription(configuredSourceMember), targetMemberType, targetMember.GetFriendlyTargetPath(_configInfo))); } @@ -278,19 +276,22 @@ private MappingConfigContinuation<TSource, TTarget> RegisterNamedContructorParam private static ParameterInfo GetUniqueConstructorParameterOrThrow<TParam>(string name = null) { - var ignoreParameterType = typeof(TParam) == typeof(AnyParameterType); - var ignoreParameterName = name == null; + var settings = new + { + IgnoreParameterType = typeof(TParam) == typeof(AnyParameterType), + IgnoreParameterName = name == null + }; var matchingParameters = typeof(TTarget) .GetPublicInstanceConstructors() - .Project(ctor => new + .Project(settings, (so, ctor) => new { Ctor = ctor, MatchingParameters = ctor .GetParameters() - .Filter(p => - (ignoreParameterType || (p.ParameterType == typeof(TParam))) && - (ignoreParameterName || (p.Name == name))) + .Filter(so, (si, p) => + (si.IgnoreParameterType || (p.ParameterType == typeof(TParam))) && + (si.IgnoreParameterName || (p.Name == name))) .ToArray() }) .Filter(d => d.MatchingParameters.Any()) @@ -298,14 +299,14 @@ private static ParameterInfo GetUniqueConstructorParameterOrThrow<TParam>(string if (matchingParameters.Length == 0) { - throw MissingParameterException(GetParameterMatchInfo<TParam>(name, !ignoreParameterType)); + throw MissingParameterException(GetParameterMatchInfo<TParam>(name, !settings.IgnoreParameterType)); } var matchingParameterData = matchingParameters.First(); if (matchingParameterData.MatchingParameters.Length > 1) { - throw AmbiguousParameterException(GetParameterMatchInfo<TParam>(name, !ignoreParameterType)); + throw AmbiguousParameterException(GetParameterMatchInfo<TParam>(name, !settings.IgnoreParameterType)); } var matchingParameter = matchingParameterData.MatchingParameters.First(); diff --git a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs index d4493a5b0..66c0cfabf 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/CustomDictionaryMappingTargetMemberSpecifier.cs @@ -4,7 +4,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries using System.Linq.Expressions; using AgileMapper.Configuration; using AgileMapper.Configuration.Dictionaries; - using DataSources; + using DataSources.Factories; #if FEATURE_DYNAMIC using Dynamics; #endif diff --git a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs index 85ec2477b..cc9f63dd0 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/DictionaryMappingConfigurator.cs @@ -17,12 +17,9 @@ internal class DictionaryMappingConfigurator<TValue> : ISourceDynamicTargetTypeSelector #endif { - private readonly MappingConfigInfo _configInfo; - internal DictionaryMappingConfigurator(MappingConfigInfo configInfo) : base(configInfo) { - _configInfo = configInfo; } #region Mapping Settings @@ -102,24 +99,24 @@ private DictionaryMappingConfigurator<TValue> RegisterElementKeyPattern( private MappingConfigInfo GetConfigInfo(DictionaryContext context) { - return (_configInfo.TargetType != typeof(object)) - ? _configInfo.Copy().Set(context) + return (ConfigInfo.TargetType != typeof(object)) + ? ConfigInfo.Copy().Set(context) : GetGlobalConfigInfo(context); } private MappingConfigInfo GetGlobalConfigInfo(DictionaryContext context) - => _configInfo.Copy().ForAllRuleSets().ForAllTargetTypes().Set(context); + => ConfigInfo.Copy().ForAllRuleSets().ForAllTargetTypes().Set(context); #region AndWhenMapping MappingConfigStartingPoint IGlobalDictionarySettings<TValue>.AndWhenMapping - => new MappingConfigStartingPoint(_configInfo.MapperContext); + => new MappingConfigStartingPoint(MapperContext); public ISourceDictionaryTargetTypeSelector<TValue> AndWhenMapping => this; #if FEATURE_DYNAMIC MappingConfigStartingPoint IGlobalDynamicSettings.AndWhenMapping - => new MappingConfigStartingPoint(_configInfo.MapperContext); + => new MappingConfigStartingPoint(MapperContext); ISourceDynamicTargetTypeSelector ISourceDynamicSettings.AndWhenMapping => this; #endif @@ -130,7 +127,7 @@ MappingConfigStartingPoint IGlobalDynamicSettings.AndWhenMapping #region Dictionaries public ISourceDictionaryMappingConfigurator<TValue, TTarget> To<TTarget>() - => CreateDictionaryConfigurator<TTarget>(_configInfo.ForAllRuleSets()); + => CreateDictionaryConfigurator<TTarget>(ConfigInfo.ForAllRuleSets()); public ISourceDictionaryMappingConfigurator<TValue, TTarget> ToANew<TTarget>() => CreateDictionaryConfigurator<TTarget>(Constants.CreateNew); @@ -143,7 +140,7 @@ public ISourceDictionaryMappingConfigurator<TValue, TTarget> Over<TTarget>() private SourceDictionaryMappingConfigurator<TValue, TTarget> CreateDictionaryConfigurator<TTarget>( string ruleSetName) - => CreateDictionaryConfigurator<TTarget>(_configInfo.ForRuleSet(ruleSetName)); + => CreateDictionaryConfigurator<TTarget>(ConfigInfo.ForRuleSet(ruleSetName)); private static SourceDictionaryMappingConfigurator<TValue, TTarget> CreateDictionaryConfigurator<TTarget>( MappingConfigInfo configInfo) @@ -155,7 +152,7 @@ private static SourceDictionaryMappingConfigurator<TValue, TTarget> CreateDictio #region Dynamics ISourceDynamicMappingConfigurator<TTarget> ISourceDynamicTargetTypeSelector.To<TTarget>() - => CreateDynamicConfigurator<TTarget>(_configInfo.ForAllRuleSets()); + => CreateDynamicConfigurator<TTarget>(ConfigInfo.ForAllRuleSets()); ISourceDynamicMappingConfigurator<TTarget> ISourceDynamicTargetTypeSelector.ToANew<TTarget>() => CreateDynamicConfigurator<TTarget>(Constants.CreateNew); @@ -168,7 +165,7 @@ ISourceDynamicMappingConfigurator<TTarget> ISourceDynamicTargetTypeSelector.Over private SourceDynamicMappingConfigurator<TTarget> CreateDynamicConfigurator<TTarget>( string ruleSetName) - => CreateDynamicConfigurator<TTarget>(_configInfo.ForRuleSet(ruleSetName)); + => CreateDynamicConfigurator<TTarget>(ConfigInfo.ForRuleSet(ruleSetName)); private static SourceDynamicMappingConfigurator<TTarget> CreateDynamicConfigurator<TTarget>( MappingConfigInfo configInfo) diff --git a/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs index 2b4d0d185..181d05661 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/ISourceDictionaryTargetTypeSelector.cs @@ -9,7 +9,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dictionaries public interface ISourceDictionaryTargetTypeSelector<TValue> : ISourceDictionarySettings<TValue> { /// <summary> - /// Configure how this mapper performs mappings from Dictionaries in all MappingRuleSets + /// Configure how this mapper performs mappings from Dictionaries in all mapping rule sets /// (create new, overwrite, etc), to the target type specified by the type argument. /// </summary> /// <typeparam name="TTarget">The target type to which the configuration will apply.</typeparam> diff --git a/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs index e318ae5f9..8963140f9 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs @@ -52,7 +52,7 @@ public ICustomTargetDictionaryKeySpecifier<TSource, TValue> MapMember<TSourceMem private QualifiedMember GetSourceMemberOrThrow(LambdaExpression lambda) { - var sourceMember = lambda.Body.ToSourceMember(ConfigInfo.MapperContext); + var sourceMember = lambda.Body.ToSourceMember(MapperContext); if (sourceMember != null) { diff --git a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs index 79ab32f70..8518bb0f6 100644 --- a/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs +++ b/AgileMapper/Api/Configuration/Dynamics/ISourceDynamicTargetTypeSelector.cs @@ -7,7 +7,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration.Dynamics public interface ISourceDynamicTargetTypeSelector : ISourceDynamicSettings { /// <summary> - /// Configure how this mapper performs mappings from ExpandoObjects in all MappingRuleSets + /// Configure how this mapper performs mappings from ExpandoObjects in all mapping rule sets /// (create new, overwrite, etc), to the target type specified by the type argument. /// </summary> /// <typeparam name="TTarget">The target type to which the configuration will apply.</typeparam> diff --git a/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs index 2e39943b8..699b30c58 100644 --- a/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dynamics/TargetDynamicMappingConfigurator.cs @@ -57,7 +57,7 @@ public ICustomTargetDynamicMemberNameSpecifier<TSource> MapMember<TSourceMember> private QualifiedMember GetSourceMemberOrThrow(LambdaExpression lambda) { - var sourceMember = lambda.Body.ToSourceMember(ConfigInfo.MapperContext); + var sourceMember = lambda.Body.ToSourceMember(MapperContext); if (sourceMember != null) { diff --git a/AgileMapper/Api/Configuration/EnumPairSpecifier.cs b/AgileMapper/Api/Configuration/EnumPairSpecifier.cs index 5a2c6316b..61d3695b4 100644 --- a/AgileMapper/Api/Configuration/EnumPairSpecifier.cs +++ b/AgileMapper/Api/Configuration/EnumPairSpecifier.cs @@ -143,7 +143,7 @@ private void ThrowIfAlreadyPaired<TPairedEnum>(TPairingEnum pairingEnumValue) var pairingEnumValueName = pairingEnumValue.ToString(); var confictingPairing = relevantPairings - .FirstOrDefault(ep => ep.PairingEnumMemberName == pairingEnumValueName); + .FirstOrDefault(pairingEnumValueName, (pevn, ep) => ep.PairingEnumMemberName == pevn); if (confictingPairing == null) { @@ -178,7 +178,7 @@ private bool ValueIsNotAlreadyPaired<TPairedEnum>(TPairedEnum pairedEnumValue) var pairedEnumMemberName = pairedEnumValue.ToString(); - return relevantPairings.None(pair => pair.PairingEnumMemberName == pairedEnumMemberName); + return relevantPairings.None(pairedEnumMemberName, (pemn, pair) => pair.PairingEnumMemberName == pemn); } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/MappingConfigStartingPointBase.cs b/AgileMapper/Api/Configuration/EventConfigStartingPointBase.cs similarity index 87% rename from AgileMapper/Api/Configuration/MappingConfigStartingPointBase.cs rename to AgileMapper/Api/Configuration/EventConfigStartingPointBase.cs index e31729ffa..b6e32f417 100644 --- a/AgileMapper/Api/Configuration/MappingConfigStartingPointBase.cs +++ b/AgileMapper/Api/Configuration/EventConfigStartingPointBase.cs @@ -10,12 +10,12 @@ /// </summary> /// <typeparam name="TSource">The source type to which the configuration should apply.</typeparam> /// <typeparam name="TTarget">The target type to which the configuration should apply.</typeparam> - public abstract class MappingConfigStartingPointBase<TSource, TTarget> + public abstract class EventConfigStartingPointBase<TSource, TTarget> { private readonly MappingConfigInfo _configInfo; private readonly CallbackPosition _callbackPosition; - internal MappingConfigStartingPointBase(MappingConfigInfo configInfo, CallbackPosition callbackPosition) + internal EventConfigStartingPointBase(MappingConfigInfo configInfo, CallbackPosition callbackPosition) { _configInfo = configInfo; _callbackPosition = callbackPosition; diff --git a/AgileMapper/Api/Configuration/ICustomMappingDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs similarity index 98% rename from AgileMapper/Api/Configuration/ICustomMappingDataSourceTargetMemberSpecifier.cs rename to AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs index b4fe2f6ad..5c7a0b03f 100644 --- a/AgileMapper/Api/Configuration/ICustomMappingDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/ICustomDataSourceTargetMemberSpecifier.cs @@ -8,7 +8,7 @@ /// </summary> /// <typeparam name="TSource">The source type to which the configuration should apply.</typeparam> /// <typeparam name="TTarget">The target type to which the configuration should apply.</typeparam> - public interface ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> + public interface ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> { /// <summary> /// Apply the configuration to the given <paramref name="targetMember"/>. diff --git a/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs b/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs index 240926c36..d5e010f06 100644 --- a/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs @@ -66,18 +66,72 @@ IMappingConfigContinuation<TSource, TTarget> CreateInstancesUsing<TFactory>(TFac IMappingFactorySpecifier<TSource, TTarget, TObject> CreateInstancesOf<TObject>(); /// <summary> - /// Ignore the given <paramref name="targetMembers"/> when mappingfrom and to the source and target types - /// being configured. + /// Ignore all source members with a value matching the <paramref name="valuesFilter"/>, when + /// mapping from and to the source and target types being configured. Matching member values + /// will not be used to populate target members. + /// </summary> + /// <param name="valuesFilter"> + /// The matching function with which to test source values to determine if they should be + /// ignored. + /// </param> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. + /// </returns> + IMappingConfigContinuation<TSource, TTarget> IgnoreSources( + Expression<Func<SourceValueFilterSpecifier, bool>> valuesFilter); + + /// <summary> + /// Ignore the given <paramref name="sourceMembers"/> when mapping from and to the source and + /// target types being configured. The given member(s) will not be used to populate target + /// members. + /// </summary> + /// <param name="sourceMembers">The source member(s) which should be ignored.</param> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. + /// </returns> + IMappingConfigContinuation<TSource, TTarget> IgnoreSource( + params Expression<Func<TSource, object>>[] sourceMembers); + + /// <summary> + /// Ignore all source members of the given <typeparamref name="TMember">Type</typeparamref> + /// when mapping from and to the source and target types being configured. Source members of + /// this type will not be used to populate target members. + /// </summary> + /// <typeparam name="TMember">The Type of source member to ignore.</typeparam> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target types being configured. + /// </returns> + IMappingConfigContinuation<TSource, TTarget> IgnoreSourceMembersOfType<TMember>(); + + /// <summary> + /// Ignore all source members matching the given <paramref name="memberFilter"/> when mapping + /// from and to the source and target types being configured. Source members matching the filter + /// will not be used to populate target members. + /// </summary> + /// <param name="memberFilter">The matching function with which to select source members to ignore.</param> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target types being configured. + /// </returns> + IMappingConfigContinuation<TSource, TTarget> IgnoreSourceMembersWhere( + Expression<Func<SourceMemberSelector, bool>> memberFilter); + + /// <summary> + /// Ignore the given <paramref name="targetMembers"/> when mapping from and to the source and + /// target types being configured. The given member(s) will not be populated. /// </summary> /// <param name="targetMembers">The target member(s) which should be ignored.</param> /// <returns> - /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. /// </returns> IMappingConfigContinuation<TSource, TTarget> Ignore(params Expression<Func<TTarget, object>>[] targetMembers); /// <summary> - /// Ignore all target member(s) of the given <typeparamref name="TMember">Type</typeparamref> when mapping + /// Ignore all target members of the given <typeparamref name="TMember">Type</typeparamref> when mapping /// from and to the source and target types being configured. /// </summary> /// <typeparam name="TMember">The Type of target member to ignore.</typeparam> @@ -88,7 +142,7 @@ IMappingConfigContinuation<TSource, TTarget> CreateInstancesUsing<TFactory>(TFac IMappingConfigContinuation<TSource, TTarget> IgnoreTargetMembersOfType<TMember>(); /// <summary> - /// Ignore all target member(s) matching the given <paramref name="memberFilter"/> when mapping + /// Ignore all target members matching the given <paramref name="memberFilter"/> when mapping /// from and to the source and target types being configured. /// </summary> /// <param name="memberFilter">The matching function with which to select target members to ignore.</param> @@ -127,7 +181,7 @@ ICustomDataSourceMappingConfigContinuation<TSource, TTarget> Map<TSourceValue, T /// An ICustomDataSourceMappingConfigContinuation with which to control the reverse configuration, or further /// configure mappings from and to the source and target type being configured. /// </returns> - ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( + ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( Expression<Func<IMappingData<TSource, TTarget>, TSourceValue>> valueFactoryExpression); /// <summary> @@ -138,10 +192,10 @@ ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue /// <typeparam name="TSourceValue">The type of the custom value being configured.</typeparam> /// <param name="valueFactoryExpression">The expression to map to the configured target member.</param> /// <returns> - /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the /// custom value should be applied. /// </returns> - ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( + ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( Expression<Func<TSource, TTarget, TSourceValue>> valueFactoryExpression); /// <summary> @@ -152,10 +206,10 @@ ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue /// <typeparam name="TSourceValue">The type of the custom value being configured.</typeparam> /// <param name="valueFactoryExpression">The expression to map to the configured target member.</param> /// <returns> - /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the /// custom value should be applied. /// </returns> - ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( + ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( Expression<Func<TSource, TTarget, int?, TSourceValue>> valueFactoryExpression); /// <summary> @@ -164,10 +218,10 @@ ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue /// <typeparam name="TSourceValue">The type of value returned by the given Func.</typeparam> /// <param name="valueFunc">The Func object to map to the configured target member.</param> /// <returns> - /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the /// custom value should be applied. /// </returns> - ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> MapFunc<TSourceValue>( + ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> MapFunc<TSourceValue>( Func<TSource, TSourceValue> valueFunc); /// <summary> @@ -177,10 +231,10 @@ ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> MapFunc<TSourceV /// <typeparam name="TSourceValue">The type of the custom constant value being configured.</typeparam> /// <param name="value">The constant value to map to the configured target member.</param> /// <returns> - /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// An ICustomDataSourceTargetMemberSpecifier with which to specify the target member to which the /// custom constant value should be applied. /// </returns> - ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>(TSourceValue value); + ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>(TSourceValue value); /// <summary> /// Configure a constant value for the given <paramref name="targetMember"/> when mapping from and to the diff --git a/AgileMapper/Api/Configuration/MapperConfigurationSpecifier.cs b/AgileMapper/Api/Configuration/MapperConfigurationSpecifier.cs index 43bef710f..2b2fca489 100644 --- a/AgileMapper/Api/Configuration/MapperConfigurationSpecifier.cs +++ b/AgileMapper/Api/Configuration/MapperConfigurationSpecifier.cs @@ -192,7 +192,7 @@ private MapperConfigurationSpecifier ApplyConfigurationsIn(IEnumerable<Type> con var orderedConfigurations = configurationIndexesByType .OrderBy(kvp => kvp.Value) - .Project(kvp => configurationDataByType[kvp.Key].Configuration); + .Project(configurationDataByType, (cdbt, kvp) => cdbt[kvp.Key].Configuration); Apply(orderedConfigurations); return this; diff --git a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs index a83b81ff2..2dddd99d4 100644 --- a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs @@ -8,6 +8,8 @@ using System.Reflection; using AgileMapper.Configuration; using AgileMapper.Configuration.Dictionaries; + using AgileMapper.Configuration.MemberIgnores; + using AgileMapper.Configuration.MemberIgnores.SourceValueFilters; using Dictionaries; #if FEATURE_DYNAMIC using Dynamics; @@ -35,6 +37,8 @@ internal MappingConfigStartingPoint(MapperContext mapperContext) private MapperContext MapperContext => _configInfo.MapperContext; + private UserConfigurationSet UserConfigurations => MapperContext.UserConfigurations; + #region Global Settings /// <summary> @@ -66,7 +70,7 @@ public IGlobalMappingSettings UseServiceProvider<TServiceProvider>(TServiceProvi { foreach (var provider in ConfiguredServiceProvider.CreateFromOrThrow(serviceProvider)) { - MapperContext.UserConfigurations.Add(provider); + UserConfigurations.Add(provider); } return this; @@ -82,9 +86,7 @@ public IGlobalMappingSettings UseServiceProvider<TServiceProvider>(TServiceProvi /// </returns> public IGlobalMappingSettings UseServiceProvider(Func<Type, object> serviceFactory) { - var provider = new ConfiguredServiceProvider(serviceFactory); - - MapperContext.UserConfigurations.Add(provider); + UserConfigurations.Add(new ConfiguredServiceProvider(serviceFactory)); return this; } @@ -98,9 +100,7 @@ public IGlobalMappingSettings UseServiceProvider(Func<Type, object> serviceFacto /// </returns> public IGlobalMappingSettings UseServiceProvider(Func<Type, string, object> serviceFactory) { - var provider = new ConfiguredServiceProvider(serviceFactory); - - MapperContext.UserConfigurations.Add(provider); + UserConfigurations.Add(new ConfiguredServiceProvider(serviceFactory)); return this; } @@ -130,9 +130,7 @@ public IGlobalMappingSettings UseServiceProvider(Func<Type, string, object> serv /// </returns> public IGlobalMappingSettings PassExceptionsTo(Action<IMappingExceptionData> callback) { - var exceptionCallback = new ExceptionCallback(GlobalConfigInfo, callback.ToConstantExpression()); - - MapperContext.UserConfigurations.Add(exceptionCallback); + UserConfigurations.Add(new ExceptionCallback(GlobalConfigInfo, callback.ToConstantExpression())); return this; } @@ -231,7 +229,7 @@ public IGlobalMappingSettings UseNamePatterns(params string[] patterns) /// </returns> public IGlobalMappingSettings MaintainIdentityIntegrity() { - MapperContext.UserConfigurations.Add(MappedObjectCachingSetting.CacheAll); + UserConfigurations.Add(MappedObjectCachingSetting.CacheAll); return this; } @@ -247,7 +245,7 @@ public IGlobalMappingSettings MaintainIdentityIntegrity() /// </returns> public IGlobalMappingSettings DisableObjectTracking() { - MapperContext.UserConfigurations.Add(MappedObjectCachingSetting.CacheNone); + UserConfigurations.Add(MappedObjectCachingSetting.CacheNone); return this; } @@ -259,7 +257,7 @@ public IGlobalMappingSettings DisableObjectTracking() /// </returns> public IGlobalMappingSettings MapNullCollectionsToNull() { - MapperContext.UserConfigurations.Add(new NullCollectionsSetting(GlobalConfigInfo)); + UserConfigurations.Add(new NullCollectionsSetting(GlobalConfigInfo)); return this; } @@ -271,7 +269,7 @@ public IGlobalMappingSettings MapNullCollectionsToNull() /// </returns> public IGlobalMappingSettings MapEntityKeys() { - MapperContext.UserConfigurations.Add(EntityKeyMappingSetting.MapAllKeys); + UserConfigurations.Add(EntityKeyMappingSetting.MapAllKeys); return this; } @@ -286,7 +284,7 @@ public IGlobalMappingSettings MapEntityKeys() /// </returns> public IGlobalMappingSettings AutoReverseConfiguredDataSources() { - MapperContext.UserConfigurations.Add(DataSourceReversalSetting.ReverseAll); + UserConfigurations.Add(DataSourceReversalSetting.ReverseAll); return this; } @@ -300,7 +298,7 @@ public IGlobalMappingSettings AutoReverseConfiguredDataSources() /// </returns> public IGlobalMappingSettings ThrowIfAnyMappingPlanIsIncomplete() { - MapperContext.UserConfigurations.ValidateMappingPlans = true; + UserConfigurations.ValidateMappingPlans = true; return this; } @@ -374,35 +372,78 @@ internal static void SetDerivedTypeAssemblies(Assembly[] assemblies) #region Ignoring Members /// <summary> - /// Ignore all target member(s) of the given <typeparamref name="TMember">Type</typeparamref>. Members will be - /// ignored in mappings between all types and MappingRuleSets (create new, overwrite, etc). + /// Ignore all source members with a value matching the <paramref name="valuesFilter"/>. Matching + /// members will not be used to populate target members in mappings between all types and + /// mapping rule sets (create new, overwrite, etc). /// </summary> - /// <typeparam name="TMember">The Type of target member to ignore.</typeparam> + /// <param name="valuesFilter"> + /// The matching function with which to test source values to determine if they should be + /// ignored. + /// </param> /// <returns> - /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping aspects. + /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping + /// aspects. /// </returns> - public IGlobalMappingSettings IgnoreTargetMembersOfType<TMember>() + public IGlobalMappingSettings IgnoreSources(Expression<Func<SourceValueFilterSpecifier, bool>> valuesFilter) + { + UserConfigurations.Add(ConfiguredSourceValueFilter.Create(GlobalConfigInfo, valuesFilter)); + return this; + } + + /// <summary> + /// Ignore all source members of the given <typeparamref name="TMember">Type</typeparamref>. + /// Matching members will not be used to populate target members in mappings between all types + /// and mapping rule sets (create new, overwrite, etc). + /// </summary> + /// <typeparam name="TMember">The Type of source member to ignore.</typeparam> + /// <returns> + /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping + /// aspects. + /// </returns> + public IGlobalMappingSettings IgnoreSourceMembersOfType<TMember>() + => IgnoreSourceMembersWhere(member => member.HasType<TMember>()); + + /// <summary> + /// Ignore all source members matching the given <paramref name="memberFilter"/>. Matching + /// members will not be used to populate a target member in mappings between all types and + /// mapping rule sets (create new, overwrite, etc). + /// </summary> + /// <param name="memberFilter">The matching function with which to select source members to ignore.</param> + /// <returns> + /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping + /// aspects. + /// </returns> + public IGlobalMappingSettings IgnoreSourceMembersWhere(Expression<Func<SourceMemberSelector, bool>> memberFilter) { - return IgnoreTargetMembersWhere(member => member.HasType<TMember>()); + UserConfigurations.Add(new ConfiguredSourceMemberFilter(GlobalConfigInfo, memberFilter)); + return this; } /// <summary> - /// Ignore all target member(s) matching the given <paramref name="memberFilter"/>. Members will be - /// ignored in mappings between all types and MappingRuleSets (create new, overwrite, etc). + /// Ignore all target members of the given <typeparamref name="TMember">Type</typeparamref>. + /// Matching members will be ignored in mappings between all types and mapping rule sets (create + /// new, overwrite, etc). + /// </summary> + /// <typeparam name="TMember">The Type of target member to ignore.</typeparam> + /// <returns> + /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping + /// aspects. + /// </returns> + public IGlobalMappingSettings IgnoreTargetMembersOfType<TMember>() + => IgnoreTargetMembersWhere(member => member.HasType<TMember>()); + + /// <summary> + /// Ignore all target members matching the given <paramref name="memberFilter"/>. Members will + /// be ignored in mappings between all types and mapping rule sets (create new, overwrite, etc). /// </summary> /// <param name="memberFilter">The matching function with which to select target members to ignore.</param> /// <returns> - /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping aspects. + /// This <see cref="IGlobalMappingSettings"/>, with which to globally configure other mapping + /// aspects. /// </returns> public IGlobalMappingSettings IgnoreTargetMembersWhere(Expression<Func<TargetMemberSelector, bool>> memberFilter) { -#if NET35 - var configuredIgnoredMember = new ConfiguredIgnoredMember(GlobalConfigInfo, memberFilter.ToDlrExpression()); -#else - var configuredIgnoredMember = new ConfiguredIgnoredMember(GlobalConfigInfo, memberFilter); -#endif - - MapperContext.UserConfigurations.Add(configuredIgnoredMember); + UserConfigurations.Add(new ConfiguredMemberFilter(GlobalConfigInfo, memberFilter)); return this; } diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index 94a20e8a8..b752a9ae2 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -5,6 +5,8 @@ using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; + using AgileMapper.Configuration.MemberIgnores; + using AgileMapper.Configuration.MemberIgnores.SourceValueFilters; using AgileMapper.Configuration.Projection; using Dictionaries; #if FEATURE_DYNAMIC @@ -37,6 +39,8 @@ public MappingConfigurator(MappingConfigInfo configInfo) protected MapperContext MapperContext => ConfigInfo.MapperContext; + private UserConfigurationSet UserConfigurations => MapperContext.UserConfigurations; + #region IFullMappingInlineConfigurator Members public MappingConfigStartingPoint WhenMapping @@ -96,7 +100,7 @@ public IFullProjectionInlineConfigurator<TSource, TTarget> RecurseToDepth(int re { var depthSettings = new RecursionDepthSettings(ConfigInfo, recursionDepth); - ConfigInfo.MapperContext.UserConfigurations.Add(depthSettings); + UserConfigurations.Add(depthSettings); return this; } @@ -213,7 +217,7 @@ private FactorySpecifier<TSource, TTarget, TObject> CreateFactorySpecifier<TObje public IFullMappingSettings<TSource, TTarget> PassExceptionsTo(Action<IMappingExceptionData<TSource, TTarget>> callback) { - MapperContext.UserConfigurations.Add(new ExceptionCallback(ConfigInfo, callback.ToConstantExpression())); + UserConfigurations.Add(new ExceptionCallback(ConfigInfo, callback.ToConstantExpression())); return this; } @@ -223,13 +227,13 @@ public IFullMappingSettings<TSource, TTarget> PassExceptionsTo(Action<IMappingEx private IFullMappingSettings<TSource, TTarget> SetMappedObjectCaching(bool cache) { - MapperContext.UserConfigurations.Add(new MappedObjectCachingSetting(ConfigInfo, cache)); + UserConfigurations.Add(new MappedObjectCachingSetting(ConfigInfo, cache)); return this; } public IFullMappingSettings<TSource, TTarget> MapNullCollectionsToNull() { - MapperContext.UserConfigurations.Add(new NullCollectionsSetting(ConfigInfo)); + UserConfigurations.Add(new NullCollectionsSetting(ConfigInfo)); return this; } @@ -239,7 +243,7 @@ public IFullMappingSettings<TSource, TTarget> MapNullCollectionsToNull() private IFullMappingSettings<TSource, TTarget> SetEntityKeyMapping(bool mapKeys) { - MapperContext.UserConfigurations.Add(new EntityKeyMappingSetting(ConfigInfo, mapKeys)); + UserConfigurations.Add(new EntityKeyMappingSetting(ConfigInfo, mapKeys)); return this; } @@ -251,7 +255,7 @@ public IFullMappingSettings<TSource, TTarget> DoNotAutoReverseConfiguredDataSour private IFullMappingSettings<TSource, TTarget> SetDataSourceReversal(bool reverse) { - MapperContext.UserConfigurations.Add(new DataSourceReversalSetting(ConfigInfo, reverse)); + UserConfigurations.Add(new DataSourceReversalSetting(ConfigInfo, reverse)); return this; } @@ -273,63 +277,110 @@ IProjectionEnumPairSpecifier<TSource, TTarget> IFullProjectionSettings<TSource, #region Ignoring Members - public IMappingConfigContinuation<TSource, TTarget> IgnoreTargetMembersOfType<TMember>() - => IgnoreMembersByFilter(member => member.HasType<TMember>()); - - IProjectionConfigContinuation<TSource, TTarget> IRootProjectionConfigurator<TSource, TTarget>.IgnoreTargetMembersOfType<TMember>() - => IgnoreMembersByFilter(member => member.HasType<TMember>()); - - public IMappingConfigContinuation<TSource, TTarget> IgnoreTargetMembersWhere( - Expression<Func<TargetMemberSelector, bool>> memberFilter) + public IMappingConfigContinuation<TSource, TTarget> IgnoreSources( + Expression<Func<SourceValueFilterSpecifier, bool>> valuesFilter) { - return IgnoreMembersByFilter(memberFilter); + return IgnoreMembersByFilter( + ConfiguredSourceValueFilter.Create(ConfigInfo, valuesFilter), + UserConfigurations.Add); } - IProjectionConfigContinuation<TSource, TTarget> IRootProjectionConfigurator<TSource, TTarget>.IgnoreTargetMembersWhere( - Expression<Func<TargetMemberSelector, bool>> memberFilter) + public IMappingConfigContinuation<TSource, TTarget> IgnoreSource(params Expression<Func<TSource, object>>[] sourceMembers) { - return IgnoreMembersByFilter(memberFilter); + return IgnoreMembers( + sourceMembers, + (ci, tm) => new ConfiguredSourceMemberIgnore(ci, tm), + UserConfigurations.Add); } - private MappingConfigContinuation<TSource, TTarget> IgnoreMembersByFilter( - Expression<Func<TargetMemberSelector, bool>> memberFilter) + public IMappingConfigContinuation<TSource, TTarget> IgnoreSourceMembersOfType<TMember>() + => IgnoreSourceMembersWhere(member => member.HasType<TMember>()); + + public IMappingConfigContinuation<TSource, TTarget> IgnoreSourceMembersWhere( + Expression<Func<SourceMemberSelector, bool>> memberFilter) { -#if NET35 - var configuredIgnoredMember = new ConfiguredIgnoredMember(ConfigInfo, memberFilter.ToDlrExpression()); -#else - var configuredIgnoredMember = new ConfiguredIgnoredMember(ConfigInfo, memberFilter); -#endif - MapperContext.UserConfigurations.Add(configuredIgnoredMember); + return IgnoreSourceMembersByFilter(memberFilter); + } - return new MappingConfigContinuation<TSource, TTarget>(ConfigInfo); + private MappingConfigContinuation<TSource, TTarget> IgnoreSourceMembersByFilter( + Expression<Func<SourceMemberSelector, bool>> memberFilter) + { + return IgnoreMembersByFilter( + new ConfiguredSourceMemberFilter(ConfigInfo, memberFilter), + UserConfigurations.Add); } public IMappingConfigContinuation<TSource, TTarget> Ignore(params Expression<Func<TTarget, object>>[] targetMembers) - => IgnoreMembers(targetMembers); + => IgnoreTargetMembers(targetMembers); IProjectionConfigContinuation<TSource, TTarget> IRootProjectionConfigurator<TSource, TTarget>.Ignore( params Expression<Func<TTarget, object>>[] resultMembers) { - return IgnoreMembers(resultMembers); + return IgnoreTargetMembers(resultMembers); } - private MappingConfigContinuation<TSource, TTarget> IgnoreMembers( + private MappingConfigContinuation<TSource, TTarget> IgnoreTargetMembers( IEnumerable<Expression<Func<TTarget, object>>> targetMembers) { - foreach (var targetMember in targetMembers) + return IgnoreMembers( + targetMembers, + (ci, tm) => new ConfiguredMemberIgnore(ci, tm), + UserConfigurations.Add); + } + + private MappingConfigContinuation<TSource, TTarget> IgnoreMembers<TMember, TConfig>( + IEnumerable<Expression<Func<TMember, object>>> members, + Func<MappingConfigInfo, LambdaExpression, TConfig> configuredIgnoreFactory, + Action<TConfig> configurationsAddMethod) + where TConfig : UserConfiguredItemBase + { + foreach (var member in members) { -#if NET35 - var configuredIgnoredMember = new ConfiguredIgnoredMember(ConfigInfo, targetMember.ToDlrExpression()); -#else - var configuredIgnoredMember = new ConfiguredIgnoredMember(ConfigInfo, targetMember); -#endif - MapperContext.UserConfigurations.Add(configuredIgnoredMember); + var configuredIgnoredMember = configuredIgnoreFactory.Invoke(ConfigInfo, member); + + configurationsAddMethod.Invoke(configuredIgnoredMember); ConfigInfo.NegateCondition(); } return new MappingConfigContinuation<TSource, TTarget>(ConfigInfo); } + public IMappingConfigContinuation<TSource, TTarget> IgnoreTargetMembersOfType<TMember>() + => IgnoreTargetMembersWhere(member => member.HasType<TMember>()); + + IProjectionConfigContinuation<TSource, TTarget> IRootProjectionConfigurator<TSource, TTarget>.IgnoreTargetMembersOfType<TMember>() + => IgnoreTargetMembersByFilter(member => member.HasType<TMember>()); + + public IMappingConfigContinuation<TSource, TTarget> IgnoreTargetMembersWhere( + Expression<Func<TargetMemberSelector, bool>> memberFilter) + { + return IgnoreTargetMembersByFilter(memberFilter); + } + + IProjectionConfigContinuation<TSource, TTarget> IRootProjectionConfigurator<TSource, TTarget>.IgnoreTargetMembersWhere( + Expression<Func<TargetMemberSelector, bool>> memberFilter) + { + return IgnoreTargetMembersByFilter(memberFilter); + } + + private MappingConfigContinuation<TSource, TTarget> IgnoreTargetMembersByFilter( + Expression<Func<TargetMemberSelector, bool>> memberFilter) + { + return IgnoreMembersByFilter( + new ConfiguredMemberFilter(ConfigInfo, memberFilter), + UserConfigurations.Add); + } + + private MappingConfigContinuation<TSource, TTarget> IgnoreMembersByFilter<TIgnore>( + TIgnore memberIgnore, + Action<TIgnore> configurationsAddMethod) + where TIgnore : UserConfiguredItemBase + { + configurationsAddMethod.Invoke(memberIgnore); + + return new MappingConfigContinuation<TSource, TTarget>(ConfigInfo); + } + #endregion public PreEventMappingConfigStartingPoint<TSource, TTarget> Before @@ -347,7 +398,7 @@ public ICustomDataSourceMappingConfigContinuation<TSource, TTarget> Map<TSourceV return GetValueFactoryTargetMemberSpecifier<TSourceValue>(valueFactoryExpression).To(targetMember); } - public ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( + public ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( Expression<Func<IMappingData<TSource, TTarget>, TSourceValue>> valueFactoryExpression) { return GetValueFactoryTargetMemberSpecifier<TSourceValue>(valueFactoryExpression); @@ -359,23 +410,23 @@ public ICustomProjectionDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TS return GetValueFactoryTargetMemberSpecifier<TSourceValue>(valueFactoryExpression); } - public ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( + public ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( Expression<Func<TSource, TTarget, TSourceValue>> valueFactoryExpression) { return GetValueFactoryTargetMemberSpecifier<TSourceValue>(valueFactoryExpression); } - public ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( + public ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>( Expression<Func<TSource, TTarget, int?, TSourceValue>> valueFactoryExpression) { return GetValueFactoryTargetMemberSpecifier<TSourceValue>(valueFactoryExpression); } - public ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> MapFunc<TSourceValue>( + public ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> MapFunc<TSourceValue>( Func<TSource, TSourceValue> valueFunc) => GetConstantValueTargetMemberSpecifier(valueFunc); - public ICustomMappingDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>(TSourceValue value) + public ICustomDataSourceTargetMemberSpecifier<TSource, TTarget> Map<TSourceValue>(TSourceValue value) => GetConstantValueTargetMemberSpecifier(value); public IMappingConfigContinuation<TSource, TTarget> Map<TSourceValue, TTargetValue>( @@ -461,7 +512,7 @@ private MappingConfigContinuation<TSource, TTarget> RegisterMapToNullCondition() { var condition = new MapToNullCondition(ConfigInfo); - MapperContext.UserConfigurations.Add(condition); + UserConfigurations.Add(condition); return new MappingConfigContinuation<TSource, TTarget>(ConfigInfo); } diff --git a/AgileMapper/Api/Configuration/PostEventMappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/PostEventMappingConfigStartingPoint.cs index 6b6d8cd18..351b9043e 100644 --- a/AgileMapper/Api/Configuration/PostEventMappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/PostEventMappingConfigStartingPoint.cs @@ -11,7 +11,7 @@ /// </summary> /// <typeparam name="TSource">The source type to which the configuration should apply.</typeparam> /// <typeparam name="TTarget">The target type to which the configuration should apply.</typeparam> - public class PostEventMappingConfigStartingPoint<TSource, TTarget> : MappingConfigStartingPointBase<TSource, TTarget> + public class PostEventMappingConfigStartingPoint<TSource, TTarget> : EventConfigStartingPointBase<TSource, TTarget> { internal PostEventMappingConfigStartingPoint(MappingConfigInfo configInfo) : base(configInfo, CallbackPosition.After) diff --git a/AgileMapper/Api/Configuration/PreEventMappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/PreEventMappingConfigStartingPoint.cs index d8c4fd5b1..3e76e8a8b 100644 --- a/AgileMapper/Api/Configuration/PreEventMappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/PreEventMappingConfigStartingPoint.cs @@ -11,7 +11,7 @@ /// </summary> /// <typeparam name="TSource">The source type to which the configuration should apply.</typeparam> /// <typeparam name="TTarget">The target type to which the configuration should apply.</typeparam> - public class PreEventMappingConfigStartingPoint<TSource, TTarget> : MappingConfigStartingPointBase<TSource, TTarget> + public class PreEventMappingConfigStartingPoint<TSource, TTarget> : EventConfigStartingPointBase<TSource, TTarget> { internal PreEventMappingConfigStartingPoint(MappingConfigInfo configInfo) : base(configInfo, CallbackPosition.Before) diff --git a/AgileMapper/Api/Configuration/SourceValueFilterSpecifier.cs b/AgileMapper/Api/Configuration/SourceValueFilterSpecifier.cs new file mode 100644 index 000000000..e0cbffd64 --- /dev/null +++ b/AgileMapper/Api/Configuration/SourceValueFilterSpecifier.cs @@ -0,0 +1,38 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using System; + using System.Linq.Expressions; + + /// <summary> + /// Provides options to configure conditions under which a source value should not be used to + /// populate a target member. + /// </summary> + public class SourceValueFilterSpecifier + { + /// <summary> + /// Ignore any source values which match the given <paramref name="valueFilter"/>. + /// </summary> + /// <param name="valueFilter"> + /// The matching function with which to test a source value to determine if it should be ignored. + /// </param> + /// <returns> + /// A boolean value, in order to enable composition of an ignore predicate with multiple clauses. + /// </returns> + public bool If(Expression<Func<object, bool>> valueFilter) => If<object>(valueFilter); + + /// <summary> + /// Ignore any source values of type <typeparamref name="TMember"/> which match the given + /// <paramref name="valueFilter"/>. + /// </summary> + /// <typeparam name="TMember"> + /// The type of source member to which the <paramref name="valueFilter"/> should be applied. + /// </typeparam> + /// <param name="valueFilter"> + /// The matching function with which to test a source value to determine if it should be ignored. + /// </param> + /// <returns> + /// A boolean value, in order to enable composition of an ignore predicate with multiple clauses. + /// </returns> + public bool If<TMember>(Expression<Func<TMember, bool>> valueFilter) => true; + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/TargetSpecifier.cs b/AgileMapper/Api/Configuration/TargetSpecifier.cs index 8db3c33a8..5c2678d4c 100644 --- a/AgileMapper/Api/Configuration/TargetSpecifier.cs +++ b/AgileMapper/Api/Configuration/TargetSpecifier.cs @@ -1,5 +1,7 @@ namespace AgileObjects.AgileMapper.Api.Configuration { + using System; + using System.Linq.Expressions; using AgileMapper.Configuration; using Dictionaries; #if FEATURE_DYNAMIC @@ -22,7 +24,7 @@ internal TargetSpecifier(MappingConfigInfo configInfo) } /// <summary> - /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets + /// Configure how this mapper performs mappings from the source type being configured in all mapping rule sets /// (create new, overwrite, etc), to the target type specified by the given <typeparamref name="TTarget"/> /// argument. /// </summary> @@ -73,14 +75,15 @@ private MappingConfigurator<TSource, TTarget> UsingRuleSet<TTarget>(string name) => new MappingConfigurator<TSource, TTarget>(_configInfo.ForRuleSet(name)); /// <summary> - /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets - /// (create new, overwrite, etc), to target Dictionaries. + /// Configure how this mapper performs mappings from the source type being configured for all + /// mapping rule sets (create new, overwrite, etc), to target Dictionaries. /// </summary> public ITargetDictionaryMappingConfigurator<TSource, object> ToDictionaries => ToDictionariesWithValueType<object>(); /// <summary> - /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets - /// (create new, overwrite, etc), to target Dictionary{string, <typeparamref name="TValue"/>} instances. + /// Configure how this mapper performs mappings from the source type being configured for all + /// mapping rule sets (create new, overwrite, etc), to target Dictionary{string, <typeparamref name="TValue"/>} + /// instances. /// </summary> /// <typeparam name="TValue"> /// The type of values contained in the Dictionary to which the configuration will apply. @@ -91,11 +94,82 @@ public ITargetDictionaryMappingConfigurator<TSource, TValue> ToDictionariesWithV #if FEATURE_DYNAMIC /// <summary> - /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets - /// (create new, overwrite, etc), to target ExpandoObjects. + /// Configure how this mapper performs mappings from the source type being configured for all + /// mapping rule sets (create new, overwrite, etc), to target ExpandoObjects. /// </summary> public ITargetDynamicMappingConfigurator<TSource> ToDynamics => new TargetDynamicMappingConfigurator<TSource>(_configInfo); #endif + + #region SourceIgnores + + /// <summary> + /// Ignore all source members with a value matching the <paramref name="valuesFilter"/>, when + /// mapping from the source type being configured to all target types. Matching member values + /// will not be used to populate target members in mappings for all mapping rule sets (create + /// new, overwrite, etc). + /// </summary> + /// <param name="valuesFilter"> + /// The matching function with which to test source values to determine if they should be + /// ignored. + /// </param> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from the source + /// type being configured. + /// </returns> + public IMappingConfigContinuation<TSource, object> IgnoreSources( + Expression<Func<SourceValueFilterSpecifier, bool>> valuesFilter) + { + return To<object>().IgnoreSources(valuesFilter); + } + + /// <summary> + /// Ignore the given <paramref name="sourceMembers"/> when mapping from the source type being + /// configured to all target types. The given member(s) will not be used to populate target + /// members in mappings for all mapping rule sets (create new, overwrite, etc). + /// </summary> + /// <param name="sourceMembers">The source member(s) which should be ignored.</param> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from the source + /// type being configured. + /// </returns> + public IMappingConfigContinuation<TSource, object> IgnoreSource( + params Expression<Func<TSource, object>>[] sourceMembers) + { + return To<object>().IgnoreSource(sourceMembers); + } + + /// <summary> + /// Ignore all source members of the given <typeparamref name="TMember">Type</typeparamref> + /// when mapping from the source type being configured to all target types. Source members of + /// this type will not be used to populate target members in mappings for all mapping rule + /// sets (create new, overwrite, etc). + /// </summary> + /// <typeparam name="TMember">The Type of source member to ignore.</typeparam> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from the source + /// type being configured. + /// </returns> + public IMappingConfigContinuation<TSource, object> IgnoreSourceMembersOfType<TMember>() + => To<object>().IgnoreSourceMembersOfType<TMember>(); + + /// <summary> + /// Ignore all source members matching the given <paramref name="memberFilter"/> when mapping + /// from the source type being configured to all target types. Source members matching the + /// filter will not be used to populate target members in mappings for all mapping rule sets + /// (create new, overwrite, etc). + /// </summary> + /// <param name="memberFilter">The matching function with which to select source members to ignore.</param> + /// <returns> + /// An IMappingConfigContinuation to enable further configuration of mappings from the source + /// type being configured. + /// </returns> + public IMappingConfigContinuation<TSource, object> IgnoreSourceMembersWhere( + Expression<Func<SourceMemberSelector, bool>> memberFilter) + { + return To<object>().IgnoreSourceMembersWhere(memberFilter); + } + + #endregion } } \ No newline at end of file diff --git a/AgileMapper/Api/PlanTargetSelector.cs b/AgileMapper/Api/PlanTargetSelector.cs index 4d493ee99..b42cb7995 100644 --- a/AgileMapper/Api/PlanTargetSelector.cs +++ b/AgileMapper/Api/PlanTargetSelector.cs @@ -40,8 +40,8 @@ public MappingPlanSet To<TTarget>( _mapperContext .RuleSets .All - .Filter(ruleSet => ruleSet != _mapperContext.RuleSets.Project) - .Project(rs => GetMappingPlan(rs, configurations)) + .Filter(_mapperContext, (mc, ruleSet) => ruleSet != mc.RuleSets.Project) + .Project(configurations, (cs, rs) => GetMappingPlan(rs, cs)) .ToArray()); } diff --git a/AgileMapper/Configuration/ConfiguredIgnoredMember.cs b/AgileMapper/Configuration/ConfiguredIgnoredMember.cs deleted file mode 100644 index 7bf4c4ae1..000000000 --- a/AgileMapper/Configuration/ConfiguredIgnoredMember.cs +++ /dev/null @@ -1,170 +0,0 @@ -namespace AgileObjects.AgileMapper.Configuration -{ - using System; - using DataSources; - using Members; - using ReadableExpressions; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - - internal class ConfiguredIgnoredMember : - UserConfiguredItemBase, - IPotentialAutoCreatedItem, - IReverseConflictable -#if NET35 - , IComparable<ConfiguredIgnoredMember> -#endif - { - private readonly Expression _memberFilterLambda; - private readonly Func<TargetMemberSelector, bool> _memberFilter; - - public ConfiguredIgnoredMember(MappingConfigInfo configInfo, LambdaExpression targetMemberLambda) - : base(configInfo, targetMemberLambda) - { - } - - public ConfiguredIgnoredMember( - MappingConfigInfo configInfo, - Expression<Func<TargetMemberSelector, bool>> memberFilterLambda) - : base(configInfo, QualifiedMember.All) - { - _memberFilterLambda = memberFilterLambda.Body; - _memberFilter = memberFilterLambda.Compile(); - } - - private ConfiguredIgnoredMember( - MappingConfigInfo configInfo, - QualifiedMember targetMember, - Expression memberFilterLambda, - Func<TargetMemberSelector, bool> memberFilter) - : base(configInfo, targetMember) - { - _memberFilterLambda = memberFilterLambda; - _memberFilter = memberFilter; - } - - public string GetConflictMessage(UserConfiguredItemBase conflictingConfiguredItem) - { - if (conflictingConfiguredItem is ConfiguredDataSourceFactory conflictingDataSource) - { - return GetConflictMessage(conflictingDataSource); - } - - return $"Member {TargetMember.GetPath()} has been ignored"; - } - - public string GetConflictMessage(ConfiguredIgnoredMember conflictingIgnoredMember) - { - string thisFilter = TargetMemberFilter, thatFilter = null; - var matcher = thisFilter ?? (thatFilter = conflictingIgnoredMember.TargetMemberFilter); - - if (matcher == null) - { - return $"Member {TargetMember.GetPath()} has already been ignored"; - } - - if (thisFilter == (thatFilter ?? conflictingIgnoredMember.TargetMemberFilter)) - { - return $"Ignore pattern '{matcher}' has already been configured"; - } - - return $"Member {TargetMember.GetPath()} is already ignored by ignore pattern '{matcher}'"; - } - - public string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) - { - if (HasMemberFilter) - { - return $"Member ignore pattern '{TargetMemberFilter}' conflicts with a configured data source"; - } - - return $"Ignored member {TargetMember.GetPath()} has a configured data source"; - } - - public string GetIgnoreMessage(IQualifiedMember targetMember) - { - if (HasMemberFilter) - { - return $"{targetMember.Name} is ignored by filter:{Environment.NewLine}{TargetMemberFilter}"; - } - - return targetMember.Name + " is ignored"; - } - - private bool HasMemberFilter => _memberFilter != null; - - private bool HasNoMemberFilter => !HasMemberFilter; - - private string TargetMemberFilter => _memberFilterLambda?.ToReadableString(); - - public override bool AppliesTo(IBasicMapperData mapperData) - { - if (!base.AppliesTo(mapperData)) - { - return false; - } - - return HasNoMemberFilter || - _memberFilter.Invoke(new TargetMemberSelector(mapperData.TargetMember)); - } - - protected override bool HasReverseConflict(UserConfiguredItemBase otherItem) => false; - - protected override bool MembersConflict(UserConfiguredItemBase otherConfiguredItem) - { - if (HasNoMemberFilter) - { - return base.MembersConflict(otherConfiguredItem); - } - - if ((otherConfiguredItem is ConfiguredIgnoredMember otherIgnoredMember) && - otherIgnoredMember.HasMemberFilter) - { - return otherIgnoredMember.TargetMemberFilter == TargetMemberFilter; - } - - return _memberFilter.Invoke(new TargetMemberSelector(otherConfiguredItem.TargetMember)); - } - - #region IPotentialAutoCreatedItem Members - - public bool WasAutoCreated { get; private set; } - - public IPotentialAutoCreatedItem Clone() - { - return new ConfiguredIgnoredMember( - ConfigInfo, - TargetMember, - _memberFilterLambda, - _memberFilter) - { - WasAutoCreated = true - }; - } - - public bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem) - { - if (HasMemberFilter) - { - return false; - } - - var clonedIgnoredMember = (ConfiguredIgnoredMember)autoCreatedItem; - - return clonedIgnoredMember.HasNoMemberFilter && - ConfigInfo.HasSameSourceTypeAs(clonedIgnoredMember.ConfigInfo) && - ConfigInfo.HasSameTargetTypeAs(clonedIgnoredMember.ConfigInfo) && - MembersConflict(clonedIgnoredMember); - } - - #endregion - -#if NET35 - int IComparable<ConfiguredIgnoredMember>.CompareTo(ConfiguredIgnoredMember other) - => DoComparisonTo(other); -#endif - } -} \ No newline at end of file diff --git a/AgileMapper/Configuration/ConfiguredItemExtensions.cs b/AgileMapper/Configuration/ConfiguredItemExtensions.cs index 2bbbb3758..d290504f1 100644 --- a/AgileMapper/Configuration/ConfiguredItemExtensions.cs +++ b/AgileMapper/Configuration/ConfiguredItemExtensions.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.Configuration { using System.Collections.Generic; + using System.Linq; using Extensions; using Extensions.Internal; using Members; @@ -10,13 +11,19 @@ internal static class ConfiguredItemExtensions public static TItem FindMatch<TItem>(this IList<TItem> items, IBasicMapperData mapperData) where TItem : UserConfiguredItemBase { - return items?.FirstOrDefault(item => item.AppliesTo(mapperData)); + return items?.FirstOrDefault(mapperData, (md, item) => item.AppliesTo(md)); + } + + public static IList<TItem> FindRelevantMatches<TItem>(this IEnumerable<TItem> items, IBasicMapperData mapperData) + where TItem : UserConfiguredItemBase + { + return items?.Filter(mapperData, (md, item) => item.CouldApplyTo(md)).ToArray() ?? Enumerable<TItem>.EmptyArray; } public static IEnumerable<TItem> FindMatches<TItem>(this IEnumerable<TItem> items, IBasicMapperData mapperData) where TItem : UserConfiguredItemBase { - return items?.Filter(item => item.AppliesTo(mapperData)) ?? Enumerable<TItem>.Empty; + return items?.Filter(mapperData, (md, item) => item.AppliesTo(md)) ?? Enumerable<TItem>.Empty; } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/ConfiguredServiceProvider.cs b/AgileMapper/Configuration/ConfiguredServiceProvider.cs index 5ee7d3b6a..5c17fdd46 100644 --- a/AgileMapper/Configuration/ConfiguredServiceProvider.cs +++ b/AgileMapper/Configuration/ConfiguredServiceProvider.cs @@ -79,6 +79,7 @@ public static IEnumerable<ConfiguredServiceProvider> CreateFromOrThrow<TServiceP var providers = providerObject .Type .GetPublicInstanceMethods() + .Filter(_serviceProviderMethodNames, (spmns, method) => Array.IndexOf(spmns, method.Name) != -1) .Project(method => GetServiceProviderOrNull( method, providerObject, @@ -108,11 +109,6 @@ private static ConfiguredServiceProvider GetServiceProviderOrNull( ref bool unnamedServiceProviderFound, ref bool namedServiceProviderFound) { - if (Array.IndexOf(_serviceProviderMethodNames, method.Name) == -1) - { - return null; - } - var parameters = method.GetParameters(); if (parameters.None() || (parameters[0].ParameterType != typeof(Type))) diff --git a/AgileMapper/Configuration/DataSourceReversalSetting.cs b/AgileMapper/Configuration/DataSourceReversalSetting.cs index 2a8848c01..36176ce70 100644 --- a/AgileMapper/Configuration/DataSourceReversalSetting.cs +++ b/AgileMapper/Configuration/DataSourceReversalSetting.cs @@ -25,11 +25,6 @@ public DataSourceReversalSetting(MappingConfigInfo configInfo, bool reverse) public override bool ConflictsWith(UserConfiguredItemBase otherItem) { - if (otherItem == this) - { - return true; - } - if (base.ConflictsWith(otherItem)) { var otherSettings = (DataSourceReversalSetting)otherItem; diff --git a/AgileMapper/Configuration/DerivedTypePairSet.cs b/AgileMapper/Configuration/DerivedTypePairSet.cs index 2a04730dd..339e9fa20 100644 --- a/AgileMapper/Configuration/DerivedTypePairSet.cs +++ b/AgileMapper/Configuration/DerivedTypePairSet.cs @@ -46,7 +46,7 @@ private void AddTypePairFor(Type targetType, DerivedTypePair typePair) if (_typePairsByTargetType.TryGetValue(targetType, out var typePairs)) { RemoveConflictingPairIfAppropriate(typePair, typePairs); - typePairs.AddSorted(typePair); + typePairs.AddThenSort(typePair); return; } @@ -62,8 +62,8 @@ private static void RemoveConflictingPairIfAppropriate( return; } - var existingTypePair = typePairs.FirstOrDefault(tp => - !tp.HasConfiguredCondition && (tp.DerivedSourceType == typePair.DerivedSourceType)); + var existingTypePair = typePairs.FirstOrDefault(typePair.DerivedSourceType, (dst, tp) => + !tp.HasConfiguredCondition && (tp.DerivedSourceType == dst)); if (existingTypePair != null) { @@ -78,7 +78,7 @@ public IList<DerivedTypePair> GetImplementationTypePairsFor( if (_typePairsByTargetType.TryGetValue(mapperData.TargetType, out var typePairs)) { return typePairs - .Filter(tp => tp.IsImplementationPairing && tp.AppliesTo(mapperData)) + .Filter(mapperData, (md, tp) => tp.IsImplementationPairing && tp.AppliesTo(md)) .ToArray(); } @@ -98,7 +98,7 @@ public IList<DerivedTypePair> GetDerivedTypePairsFor( if (_typePairsByTargetType.TryGetValue(mapperData.TargetType, out var typePairs)) { - return typePairs.Filter(tp => tp.AppliesTo(mapperData)).ToArray(); + return typePairs.Filter(mapperData, (md, tp) => tp.AppliesTo(md)).ToArray(); } return Enumerable<DerivedTypePair>.EmptyArray; @@ -157,16 +157,16 @@ private void LookForDerivedTypePairs(ITypePair mapperData, MapperContext mapperC return; } - var candidatePairsData = derivedSourceTypes.ProjectToArray(t => new + var candidatePairsData = derivedSourceTypes.ProjectToArray(derivedTargetTypeNameFactory, (dttnf, t) => new { DerivedSourceType = t, - DerivedTargetTypeName = derivedTargetTypeNameFactory.Invoke(t) + DerivedTargetTypeName = dttnf.Invoke(t) }); foreach (var candidatePairData in candidatePairsData) { var derivedTargetType = derivedTargetTypes - .FirstOrDefault(t => t.Name == candidatePairData.DerivedTargetTypeName); + .FirstOrDefault(candidatePairData, (cpd, t) => t.Name == cpd.DerivedTargetTypeName); if (derivedTargetType == null) { diff --git a/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs b/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs index 9be9db8d8..cc11334a5 100644 --- a/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs +++ b/AgileMapper/Configuration/Dictionaries/CustomDictionaryKey.cs @@ -2,7 +2,7 @@ { using System; using System.Dynamic; - using DataSources; + using DataSources.Factories; using Members; using ReadableExpressions.Extensions; #if NET35 diff --git a/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs b/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs index 97ef75ad2..20390bf7d 100644 --- a/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs +++ b/AgileMapper/Configuration/Dictionaries/DictionarySettings.cs @@ -135,7 +135,7 @@ private static void ThrowIfConflictingKeyPartFactoryExists<TKeyPartFactory>( } var conflictingFactory = existingFactories - .FirstOrDefault(kpf => kpf.ConflictsWith(factory)); + .FirstOrDefault(factory, (f, kpf) => kpf.ConflictsWith(f)); if (conflictingFactory == null) { diff --git a/AgileMapper/Configuration/EntityKeyMappingSetting.cs b/AgileMapper/Configuration/EntityKeyMappingSetting.cs index 992a36248..ae0764e34 100644 --- a/AgileMapper/Configuration/EntityKeyMappingSetting.cs +++ b/AgileMapper/Configuration/EntityKeyMappingSetting.cs @@ -25,25 +25,21 @@ public EntityKeyMappingSetting(MappingConfigInfo configInfo, bool mapKeys) public override bool ConflictsWith(UserConfiguredItemBase otherItem) { - if (otherItem == this) + if (!base.ConflictsWith(otherItem)) { - return true; + return false; } - if (base.ConflictsWith(otherItem)) - { - var otherSettings = (EntityKeyMappingSetting)otherItem; - - if ((this == MapAllKeys) || (otherSettings == MapAllKeys)) - { - return (otherSettings.MapKeys == MapKeys); - } + var otherSettings = (EntityKeyMappingSetting)otherItem; - // Settings have overlapping, non-global source and target types - return true; + if ((this == MapAllKeys) || (otherSettings == MapAllKeys)) + { + return (otherSettings.MapKeys == MapKeys); } - return false; + // Settings have overlapping, non-global source and target types + return true; + } public string GetConflictMessage(EntityKeyMappingSetting conflicting) diff --git a/AgileMapper/Configuration/EnumComparisonFixer.cs b/AgileMapper/Configuration/EnumComparisonFixer.cs index 74dfccdc5..176188859 100644 --- a/AgileMapper/Configuration/EnumComparisonFixer.cs +++ b/AgileMapper/Configuration/EnumComparisonFixer.cs @@ -72,7 +72,7 @@ private static bool TryGetConvertedEnumMember(Expression value, out Expression e // The enum member being compared to an enum constant is nullable; // the NestedAccessFinder will weave in a HasValue check, so we can // use its .Value property in the fixed comparison: - convertedValue = Expression.Property(convertedValue, "Value"); + convertedValue = convertedValue.GetNullableValueAccess(); } enumMember = convertedValue; diff --git a/AgileMapper/Configuration/ExceptionCallback.cs b/AgileMapper/Configuration/ExceptionCallback.cs index d426ad798..d49703ede 100644 --- a/AgileMapper/Configuration/ExceptionCallback.cs +++ b/AgileMapper/Configuration/ExceptionCallback.cs @@ -1,19 +1,61 @@ namespace AgileObjects.AgileMapper.Configuration { + using System; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; + using NetStandardPolyfills; + using ObjectPopulation; internal class ExceptionCallback : UserConfiguredItemBase { + private readonly Expression _callback; + public ExceptionCallback(MappingConfigInfo configInfo, Expression callback) : base(configInfo) { - Callback = callback; + _callback = callback; } - public Expression Callback { get; } + public Expression ToCatchBody( + Expression exceptionVariable, + Type returnType, + IMemberMapperData mapperData) + { + var callbackActionType = _callback.Type.GetGenericTypeArguments()[0]; + + Type[] contextTypes; + Expression contextAccess; + + if (callbackActionType.IsGenericType()) + { + contextTypes = callbackActionType.GetGenericTypeArguments(); + contextAccess = mapperData.GetAppropriateTypedMappingContextAccess(contextTypes); + } + else + { + contextTypes = new[] { mapperData.SourceType, mapperData.TargetType }; + contextAccess = mapperData.MappingDataObject; + } + + var exceptionContextCreateMethod = ObjectMappingExceptionData + .CreateMethod + .MakeGenericMethod(contextTypes); + + var createExceptionContextCall = Expression.Call( + exceptionContextCreateMethod, + contextAccess, + exceptionVariable); + + var callbackInvocation = Expression.Invoke(_callback, createExceptionContextCall); + var returnDefault = returnType.ToDefaultExpression(); + var configuredCatchBody = Expression.Block(callbackInvocation, returnDefault); + + return configuredCatchBody; + } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs b/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs index b87004884..2b880d6a2 100644 --- a/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs +++ b/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs @@ -3,14 +3,10 @@ using System; using System.Collections; using System.Collections.Generic; -#if NET35 - using System.Linq; -#endif using System.Linq.Expressions; using Api.Configuration; using Api.Configuration.Projection; using Caching; - using Extensions; #if NET35 using Extensions.Internal; #endif diff --git a/AgileMapper/Configuration/MapToNullCondition.cs b/AgileMapper/Configuration/MapToNullCondition.cs index bafa283c1..20aa0af16 100644 --- a/AgileMapper/Configuration/MapToNullCondition.cs +++ b/AgileMapper/Configuration/MapToNullCondition.cs @@ -34,14 +34,7 @@ public string GetConflictMessage() => $"Type {TargetTypeName} already has a configured map-to-null condition"; public override bool AppliesTo(IBasicMapperData mapperData) - { - if (mapperData.TargetMemberIsEnumerableElement()) - { - return false; - } - - return base.AppliesTo(mapperData); - } + => !mapperData.TargetMemberIsEnumerableElement() && base.AppliesTo(mapperData); protected override Expression GetConditionOrNull(IMemberMapperData mapperData, CallbackPosition position) { diff --git a/AgileMapper/Configuration/MapperConfiguration.cs b/AgileMapper/Configuration/MapperConfiguration.cs index be06ee64c..2d46b2469 100644 --- a/AgileMapper/Configuration/MapperConfiguration.cs +++ b/AgileMapper/Configuration/MapperConfiguration.cs @@ -66,6 +66,7 @@ protected TServiceProvider GetServiceProvider<TServiceProvider>() /// <returns>A cloned copy of the mapper being configured.</returns> protected IMapper CreateNewMapper() => _mapper.CloneSelf(); + // TODO: Test coverage /// <summary> /// Create and compile a mapping function for a particular type of mapping of the source type specified by /// the given <paramref name="exampleInstance"/>. Use this overload for anonymous types. diff --git a/AgileMapper/Configuration/MappingConfigInfo.cs b/AgileMapper/Configuration/MappingConfigInfo.cs index b9c853b8e..48c87739c 100644 --- a/AgileMapper/Configuration/MappingConfigInfo.cs +++ b/AgileMapper/Configuration/MappingConfigInfo.cs @@ -11,12 +11,13 @@ #endif using Extensions.Internal; using Members; + using NetStandardPolyfills; using ObjectPopulation; using ReadableExpressions; internal class MappingConfigInfo : ITypePair { - private static readonly MappingRuleSet _allRuleSets = new MappingRuleSet("*", null, null, null, null, null, null); + private static readonly MappingRuleSet _allRuleSets = new MappingRuleSet("*"); public static readonly MappingConfigInfo AllRuleSetsSourceTypesAndTargetTypes = AllRuleSetsAndSourceTypes(null).ForAllTargetTypes(); @@ -54,7 +55,8 @@ public MappingConfigInfo ForSourceType(Type sourceType) return this; } - public bool HasSameSourceTypeAs(MappingConfigInfo otherConfigInfo) => otherConfigInfo.SourceType == SourceType; + public bool HasSameSourceTypeAs(MappingConfigInfo otherConfigInfo) + => otherConfigInfo.SourceType == SourceType; public Type TargetType { get; private set; } @@ -123,9 +125,10 @@ public void AddConditionOrThrow(LambdaExpression conditionLambda) _conditionLambda = ConfiguredLambdaInfo.For(conditionLambda); } - private static void ErrorIfConditionHasTypeTest(LambdaExpression conditionLambda) + private void ErrorIfConditionHasTypeTest(LambdaExpression conditionLambda) { - if (TypeTestFinder.HasNoTypeTest(conditionLambda)) + if ((SourceType?.IsInterface() == true) || + TypeTestFinder.HasNoTypeTest(conditionLambda)) { return; } @@ -168,7 +171,7 @@ public Expression GetConditionOrNull( if (_negateCondition) { - condition = Expression.Not(condition); + condition = condition.Negate(); } var targetCanBeNull = position.IsPriorToObjectCreation(targetMember); diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs new file mode 100644 index 000000000..8e0b45aac --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberFilter.cs @@ -0,0 +1,95 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ + using System; +#if NET35 + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; +#else + using System.Linq.Expressions; +#endif + using DataSources.Factories; +#if NET35 + using Extensions.Internal; +#endif + using Members; + using ReadableExpressions; + + internal class ConfiguredMemberFilter : ConfiguredMemberIgnoreBase, IMemberFilterIgnore + { + private readonly Expression _memberFilterExpression; + private readonly Func<TargetMemberSelector, bool> _memberFilter; +#if NET35 + public ConfiguredMemberFilter( + MappingConfigInfo configInfo, + LinqExp.Expression<Func<TargetMemberSelector, bool>> memberFilterLambda) + : this(configInfo, memberFilterLambda.ToDlrExpression()) + { + } +#endif + public ConfiguredMemberFilter( + MappingConfigInfo configInfo, + Expression<Func<TargetMemberSelector, bool>> memberFilterLambda) + : this(configInfo, memberFilterLambda.Body, memberFilterLambda.Compile()) + { + } + + private ConfiguredMemberFilter( + MappingConfigInfo configInfo, + Expression memberFilterExpression, + Func<TargetMemberSelector, bool> memberFilter) + : base(configInfo) + { + _memberFilterExpression = memberFilterExpression; + _memberFilter = memberFilter; + } + + private string TargetMemberFilter => _memberFilterExpression?.ToReadableString(); + + string IMemberFilterIgnore.MemberFilter => TargetMemberFilter; + + public override string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore) + => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); + + public string GetConflictMessage(ConfiguredMemberIgnore conflictingMemberIgnore) + => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); + + public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + => $"Member ignore pattern '{TargetMemberFilter}' conflicts with a configured data source"; + + public override string GetIgnoreMessage(IQualifiedMember targetMember) + => $"{targetMember.Name} is ignored by filter:{Environment.NewLine}{TargetMemberFilter}"; + + public override bool AppliesTo(IBasicMapperData mapperData) + => base.AppliesTo(mapperData) && IsFiltered(mapperData.TargetMember); + + protected override bool MembersConflict(UserConfiguredItemBase otherItem) + { + if (otherItem is ConfiguredMemberFilter otherIgnoredMemberFilter) + { + return otherIgnoredMemberFilter.TargetMemberFilter == TargetMemberFilter; + } + + return IsFiltered(otherItem.TargetMember); + } + + public bool IsFiltered(QualifiedMember member) + => _memberFilter.Invoke(new TargetMemberSelector(member)); + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new ConfiguredMemberFilter( + ConfigInfo, + _memberFilterExpression, + _memberFilter) + { + WasAutoCreated = true + }; + } + + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem) => false; + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs new file mode 100644 index 000000000..168f820da --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnore.cs @@ -0,0 +1,68 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ +#if NET35 + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; +#else + using System.Linq.Expressions; +#endif + using DataSources.Factories; +#if NET35 + using Extensions.Internal; +#endif + using Members; + + internal class ConfiguredMemberIgnore : ConfiguredMemberIgnoreBase, IMemberIgnore + { +#if NET35 + public ConfiguredMemberIgnore(MappingConfigInfo configInfo, LinqExp.LambdaExpression targetMemberLambda) + : this(configInfo, targetMemberLambda.ToDlrExpression()) + { + } +#endif + public ConfiguredMemberIgnore(MappingConfigInfo configInfo, LambdaExpression targetMemberLambda) + : base(configInfo, targetMemberLambda) + { + } + + private ConfiguredMemberIgnore(MappingConfigInfo configInfo, QualifiedMember targetMember) + : base(configInfo, targetMember) + { + } + + public override string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore) + => ((IMemberIgnore)this).GetConflictMessage(conflictingMemberIgnore); + + public override string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource) + => $"Ignored member {TargetMember.GetPath()} has a configured data source"; + + public override string GetIgnoreMessage(IQualifiedMember targetMember) + => targetMember.Name + " is ignored"; + + QualifiedMember IMemberIgnore.Member => TargetMember; + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new ConfiguredMemberIgnore(ConfigInfo, TargetMember) + { + WasAutoCreated = true + }; + } + + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem) + { + if (!(autoCreatedItem is ConfiguredMemberIgnore clonedIgnoredMember)) + { + return false; + } + + return ConfigInfo.HasSameSourceTypeAs(clonedIgnoredMember.ConfigInfo) && + ConfigInfo.HasSameTargetTypeAs(clonedIgnoredMember.ConfigInfo) && + MembersConflict(clonedIgnoredMember); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs new file mode 100644 index 000000000..e60e37225 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredMemberIgnoreBase.cs @@ -0,0 +1,75 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ +#if NET35 + using System; + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; +#else + using System.Linq.Expressions; +#endif + using DataSources.Factories; + using Members; + + internal abstract class ConfiguredMemberIgnoreBase : + UserConfiguredItemBase, + IMemberIgnoreBase, + IPotentialAutoCreatedItem, + IReverseConflictable +#if NET35 + , IComparable<ConfiguredMemberIgnoreBase> +#endif + { + protected ConfiguredMemberIgnoreBase(MappingConfigInfo configInfo) + : base(configInfo) + { + } + + protected ConfiguredMemberIgnoreBase(MappingConfigInfo configInfo, LambdaExpression targetMemberLambda) + : base(configInfo, targetMemberLambda) + { + } + + protected ConfiguredMemberIgnoreBase(MappingConfigInfo configInfo, QualifiedMember targetMember) + : base(configInfo, targetMember) + { + } + + public string GetConflictMessage(UserConfiguredItemBase conflictingConfiguredItem) + { + if (conflictingConfiguredItem is ConfiguredDataSourceFactory conflictingDataSource) + { + return GetConflictMessage(conflictingDataSource); + } + + if (conflictingConfiguredItem is ConfiguredMemberIgnoreBase conflictingMemberIgnore) + { + return GetConflictMessage(conflictingMemberIgnore); + } + + return $"Member {TargetMember.GetPath()} has been ignored"; + } + + public abstract string GetConflictMessage(ConfiguredMemberIgnoreBase conflictingMemberIgnore); + + public abstract string GetConflictMessage(ConfiguredDataSourceFactory conflictingDataSource); + + public abstract string GetIgnoreMessage(IQualifiedMember targetMember); + + protected override bool HasReverseConflict(UserConfiguredItemBase otherItem) => false; + + #region IPotentialAutoCreatedItem Members + + public bool WasAutoCreated { get; protected set; } + + public abstract IPotentialAutoCreatedItem Clone(); + + public abstract bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem); + + #endregion + +#if NET35 + int IComparable<ConfiguredMemberIgnoreBase>.CompareTo(ConfiguredMemberIgnoreBase other) + => DoComparisonTo(other); +#endif + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs new file mode 100644 index 000000000..6d6e557b1 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberFilter.cs @@ -0,0 +1,98 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ + using System; +#if NET35 + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; + using Extensions.Internal; +#else + using System.Linq.Expressions; +#endif + using Members; + using ReadableExpressions; + + + internal class ConfiguredSourceMemberFilter : ConfiguredSourceMemberIgnoreBase, IMemberFilterIgnore + { + private readonly Expression _memberFilterExpression; + private readonly Func<SourceMemberSelector, bool> _memberFilter; +#if NET35 + public ConfiguredSourceMemberFilter( + MappingConfigInfo configInfo, + LinqExp.Expression<Func<SourceMemberSelector, bool>> memberFilterLambda) + : this(configInfo, memberFilterLambda.ToDlrExpression()) + { + } +#endif + public ConfiguredSourceMemberFilter( + MappingConfigInfo configInfo, + Expression<Func<SourceMemberSelector, bool>> memberFilterLambda) + : base(configInfo) + { + _memberFilterExpression = memberFilterLambda.Body; + _memberFilter = memberFilterLambda.Compile(); + } + + private ConfiguredSourceMemberFilter( + MappingConfigInfo configInfo, + Expression memberFilterExpression, + Func<SourceMemberSelector, bool> memberFilter) + : base(configInfo) + { + _memberFilterExpression = memberFilterExpression; + _memberFilter = memberFilter; + } + + private string SourceMemberFilter => _memberFilterExpression?.ToReadableString(); + + string IMemberFilterIgnore.MemberFilter => SourceMemberFilter; + + public override string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore) + => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingSourceMemberIgnore); + + public string GetConflictMessage(ConfiguredSourceMemberIgnore conflictingMemberIgnore) + => ((IMemberFilterIgnore)this).GetConflictMessage(conflictingMemberIgnore); + + public override bool AppliesTo(IBasicMapperData mapperData) + { + return base.AppliesTo(mapperData) && + (mapperData.SourceMember is QualifiedMember sourceMember) && + IsFiltered(sourceMember); + } + + protected override bool MembersConflict(UserConfiguredItemBase otherItem) + { + if (otherItem is ConfiguredSourceMemberFilter otherIgnoredMemberFilter) + { + return SourceMemberFilter == otherIgnoredMemberFilter.SourceMemberFilter; + } + + if (otherItem is ConfiguredSourceMemberIgnore otherIgnoredMember) + { + return IsFiltered(otherIgnoredMember.SourceMember); + } + + return false; + } + + public bool IsFiltered(QualifiedMember member) + => _memberFilter.Invoke(new SourceMemberSelector(member)); + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new ConfiguredSourceMemberFilter( + ConfigInfo, + _memberFilterExpression, + _memberFilter) + { + WasAutoCreated = true + }; + } + + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem) => false; + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs new file mode 100644 index 000000000..6c4d447fb --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnore.cs @@ -0,0 +1,87 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ +#if NET35 + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; + using Extensions.Internal; +#else + using System.Linq.Expressions; +#endif + using Members; + + internal class ConfiguredSourceMemberIgnore : ConfiguredSourceMemberIgnoreBase, IMemberIgnore + { +#if NET35 + public ConfiguredSourceMemberIgnore(MappingConfigInfo configInfo, LinqExp.LambdaExpression sourceMemberLambda) + : this(configInfo, sourceMemberLambda.ToDlrExpression()) + { + } +#endif + public ConfiguredSourceMemberIgnore(MappingConfigInfo configInfo, LambdaExpression sourceMemberLambda) + : base(configInfo) + { + SourceMember = sourceMemberLambda.ToSourceMemberOrNull(configInfo.MapperContext, out var failureReason) ?? + throw new MappingConfigurationException(failureReason); + } + + private ConfiguredSourceMemberIgnore(MappingConfigInfo configInfo, QualifiedMember sourceMember) + : base(configInfo) + { + SourceMember = sourceMember; + } + + public QualifiedMember SourceMember { get; } + + QualifiedMember IMemberIgnore.Member => SourceMember; + + public override string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore) + => ((IMemberIgnore)this).GetConflictMessage(conflictingSourceMemberIgnore); + + public override bool AppliesTo(IBasicMapperData mapperData) + => base.AppliesTo(mapperData) && SourceMembersMatch(mapperData.SourceMember as QualifiedMember); + + protected override bool MembersConflict(UserConfiguredItemBase otherItem) + { + if (!(otherItem is ConfiguredSourceMemberIgnore otherIgnoredMember)) + { + return false; + } + + return SourceMembersMatch(otherIgnoredMember.SourceMember); + } + + private bool SourceMembersMatch(QualifiedMember otherSourceMember) + { + if ((SourceMember == null) || (otherSourceMember == null)) + { + return false; + } + + return SourceMember.LeafMember.Equals(otherSourceMember.LeafMember); + } + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new ConfiguredSourceMemberIgnore(ConfigInfo, SourceMember) + { + WasAutoCreated = true + }; + } + + public override bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem) + { + if (!(autoCreatedItem is ConfiguredSourceMemberIgnore clonedIgnoredSourceMember)) + { + return false; + } + + return ConfigInfo.HasSameSourceTypeAs(clonedIgnoredSourceMember.ConfigInfo) && + ConfigInfo.HasSameTargetTypeAs(clonedIgnoredSourceMember.ConfigInfo) && + MembersConflict(clonedIgnoredSourceMember); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs new file mode 100644 index 000000000..6880e3d86 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/ConfiguredSourceMemberIgnoreBase.cs @@ -0,0 +1,37 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ +#if NET35 + using System; +#endif + + internal abstract class ConfiguredSourceMemberIgnoreBase : + UserConfiguredItemBase, + IMemberIgnoreBase, + IPotentialAutoCreatedItem +#if NET35 + , IComparable<ConfiguredSourceMemberIgnoreBase> +#endif + { + protected ConfiguredSourceMemberIgnoreBase(MappingConfigInfo configInfo) + : base(configInfo) + { + } + + public abstract string GetConflictMessage(ConfiguredSourceMemberIgnoreBase conflictingSourceMemberIgnore); + + #region IPotentialAutoCreatedItem Members + + public bool WasAutoCreated { get; protected set; } + + public abstract IPotentialAutoCreatedItem Clone(); + + public abstract bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem); + + #endregion + +#if NET35 + int IComparable<ConfiguredSourceMemberIgnoreBase>.CompareTo(ConfiguredSourceMemberIgnoreBase other) + => DoComparisonTo(other); +#endif + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs b/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs new file mode 100644 index 000000000..5f9fec293 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/IMemberFilterIgnore.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ + using Members; + + internal interface IMemberFilterIgnore : IMemberIgnoreBase + { + string MemberFilter { get; } + + bool IsFiltered(QualifiedMember member); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/IMemberIgnore.cs b/AgileMapper/Configuration/MemberIgnores/IMemberIgnore.cs new file mode 100644 index 000000000..5a6690521 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/IMemberIgnore.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ + using Members; + + internal interface IMemberIgnore : IMemberIgnoreBase + { + QualifiedMember Member { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/IMemberIgnoreBase.cs b/AgileMapper/Configuration/MemberIgnores/IMemberIgnoreBase.cs new file mode 100644 index 000000000..f7a4dcecd --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/IMemberIgnoreBase.cs @@ -0,0 +1,6 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ + internal interface IMemberIgnoreBase + { + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/MemberIgnoreExtensions.cs b/AgileMapper/Configuration/MemberIgnores/MemberIgnoreExtensions.cs new file mode 100644 index 000000000..a71a66ef5 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/MemberIgnoreExtensions.cs @@ -0,0 +1,39 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores +{ + internal static class MemberIgnoreExtensions + { + public static string GetConflictMessage( + this IMemberFilterIgnore memberFilterIgnore, + IMemberIgnoreBase conflictingMemberIgnore) + { + if (conflictingMemberIgnore is IMemberIgnore otherMemberIgnore) + { + return memberFilterIgnore.GetConflictMessage(otherMemberIgnore); + } + + var otherIgnoredMemberFilter = (IMemberFilterIgnore)conflictingMemberIgnore; + + return $"Ignore pattern '{otherIgnoredMemberFilter.MemberFilter}' has already been configured"; + } + + public static string GetConflictMessage( + this IMemberFilterIgnore memberFilterIgnore, + IMemberIgnore conflictingMemberIgnore) + { + return $"Member {conflictingMemberIgnore.Member.GetPath()} is " + + $"already ignored by ignore pattern '{memberFilterIgnore.MemberFilter}'"; + } + + public static string GetConflictMessage( + this IMemberIgnore memberIgnore, + IMemberIgnoreBase conflictingMemberIgnore) + { + if (conflictingMemberIgnore is IMemberFilterIgnore memberFilterIgnore) + { + return memberFilterIgnore.GetConflictMessage(memberIgnore); + } + + return $"Member {memberIgnore.Member.GetPath()} has already been ignored"; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/ConfiguredSourceValueFilter.cs b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/ConfiguredSourceValueFilter.cs new file mode 100644 index 000000000..7612a81cc --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/ConfiguredSourceValueFilter.cs @@ -0,0 +1,212 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores.SourceValueFilters +{ + using System; + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; +#else + using System.Linq.Expressions; +#endif + using Api.Configuration; + using Extensions.Internal; + using Members; + using ReadableExpressions; + using static Extensions.Internal.ExpressionEvaluation; + using static FilterConstants; + + internal abstract class ConfiguredSourceValueFilter : + UserConfiguredItemBase, + IPotentialAutoCreatedItem +#if NET35 + , IComparable<ConfiguredSourceValueFilter> +#endif + { + protected ConfiguredSourceValueFilter(MappingConfigInfo configInfo, Expression valuesFilter) + : base(configInfo) + { + ValuesFilter = valuesFilter; + } + + #region Factory Methods +#if NET35 + public static ConfiguredSourceValueFilter Create( + MappingConfigInfo configInfo, + LinqExp.Expression<Func<SourceValueFilterSpecifier, bool>> valuesFilter) + { + return Create(configInfo, valuesFilter.ToDlrExpression()); + } +#endif + public static ConfiguredSourceValueFilter Create( + MappingConfigInfo configInfo, + Expression<Func<SourceValueFilterSpecifier, bool>> valuesFilter) + { + var filterConditions = FilterCondition.GetConditions(valuesFilter); + + if (filterConditions.None()) + { + throw new MappingConfigurationException("At least one source filter must be specified."); + } + + if (filterConditions.HasOne()) + { + return new SingleConditionConfiguredSourceValueFilter( + configInfo, + valuesFilter.Body, + filterConditions.First()); + } + + return new MultipleConditionConfiguredSourceValueFilter( + configInfo, + valuesFilter.Body, + filterConditions); + } + + #endregion + + protected Expression ValuesFilter { get; } + + public override bool ConflictsWith(UserConfiguredItemBase otherConfiguredItem) + => base.ConflictsWith(otherConfiguredItem) && FiltersAreTheSame((ConfiguredSourceValueFilter)otherConfiguredItem); + + private bool FiltersAreTheSame(ConfiguredSourceValueFilter otherSourceValueFilter) + => AreEqual(ValuesFilter, otherSourceValueFilter.ValuesFilter); + + public string GetConflictMessage() + { + var filterDescription = ValuesFilter.ToReadableString(o => o.UseExplicitGenericParameters); + + return $"Source filter '{filterDescription}' has already been configured"; + } + + public bool AppliesTo(Type sourceValueType, IBasicMapperData mapperData) + => AppliesTo(mapperData) && Filters(sourceValueType); + + protected abstract bool Filters(Type valueType); + + public Expression GetConditionOrNull(Expression sourceValue, IMemberMapperData mapperData) + { + var hasFixedValueOperands = false; + var filterExpression = GetFilterExpression(sourceValue, ref hasFixedValueOperands); + + if (hasFixedValueOperands) + { + filterExpression = FilterOptimiser.Optimise(filterExpression); + } + + if (filterExpression == False) + { + return null; + } + + var condition = GetConditionOrNull(mapperData); + + if (condition != null) + { + filterExpression = Expression.AndAlso(condition, filterExpression); + } + + return filterExpression.Negate(); + } + + protected abstract Expression GetFilterExpression(Expression sourceValue, ref bool hasFixedValueOperands); + + #region IPotentialAutoCreatedItem Members + + public bool WasAutoCreated { get; protected set; } + + public abstract IPotentialAutoCreatedItem Clone(); + + public bool IsReplacementFor(IPotentialAutoCreatedItem autoCreatedItem) + { + var otherSourceValueFilter = (ConfiguredSourceValueFilter)autoCreatedItem; + + return otherSourceValueFilter.HasOverlappingTypes(this) && FiltersAreTheSame(otherSourceValueFilter); + } + + #endregion + +#if NET35 + int IComparable<ConfiguredSourceValueFilter>.CompareTo(ConfiguredSourceValueFilter other) + => DoComparisonTo(other); +#endif + + private class SingleConditionConfiguredSourceValueFilter : ConfiguredSourceValueFilter + { + private readonly FilterCondition _filterCondition; + + public SingleConditionConfiguredSourceValueFilter( + MappingConfigInfo configInfo, + Expression valuesFilter, + FilterCondition filterCondition) + : base(configInfo, valuesFilter) + { + _filterCondition = filterCondition; + } + + protected override bool Filters(Type valueType) => _filterCondition.Filters(valueType); + + protected override Expression GetFilterExpression(Expression sourceValue, ref bool hasFixedValueOperands) + { + return ValuesFilter.Replace( + _filterCondition.Filter, + _filterCondition.GetConditionReplacement(sourceValue, ref hasFixedValueOperands)); + } + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new SingleConditionConfiguredSourceValueFilter(ConfigInfo, ValuesFilter, _filterCondition) + { + WasAutoCreated = true + }; + } + + #endregion + } + + private class MultipleConditionConfiguredSourceValueFilter : ConfiguredSourceValueFilter + { + private readonly IList<FilterCondition> _filterConditions; + + public MultipleConditionConfiguredSourceValueFilter( + MappingConfigInfo configInfo, + Expression valuesFilter, + IList<FilterCondition> filterConditions) + : base(configInfo, valuesFilter) + { + _filterConditions = filterConditions; + } + + protected override bool Filters(Type valueType) + => _filterConditions.Any(valueType, (vt, fc) => fc.Filters(vt)); + + protected override Expression GetFilterExpression(Expression sourceValue, ref bool hasFixedValueOperands) + { + var conditionReplacements = new Dictionary<Expression, Expression>(_filterConditions.Count); + + foreach (var filterCondition in _filterConditions) + { + conditionReplacements.Add( + filterCondition.Filter, + filterCondition.GetConditionReplacement(sourceValue, ref hasFixedValueOperands)); + } + + return ValuesFilter.Replace(conditionReplacements); + } + + #region IPotentialAutoCreatedItem Members + + public override IPotentialAutoCreatedItem Clone() + { + return new MultipleConditionConfiguredSourceValueFilter(ConfigInfo, ValuesFilter, _filterConditions) + { + WasAutoCreated = true + }; + } + + #endregion + } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterCondition.cs b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterCondition.cs new file mode 100644 index 000000000..66dfb4c4c --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterCondition.cs @@ -0,0 +1,161 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores.SourceValueFilters +{ + using System; + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Api.Configuration; + using Extensions.Internal; + using Members; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; + using static FilterConstants; + + internal class FilterCondition + { + private readonly Type _filteredValueType; + private readonly bool _appliesToAllSources; + private readonly bool _filteredValueTypeIsNullable; + private readonly Expression _filterParameter; + private readonly Expression _filterExpression; + private readonly Expression _filterNestedAccessChecks; + + private FilterCondition( + MethodCallExpression filterCreationCall, + Type filteredValueType) + { + Filter = filterCreationCall; + _filteredValueType = filteredValueType; + _appliesToAllSources = filteredValueType == typeof(object); + + if (!_appliesToAllSources) + { + _filteredValueTypeIsNullable = filteredValueType.IsNullableType(); + } + + var filterArgument = filterCreationCall.Arguments.First(); + + if (filterArgument.NodeType == ExpressionType.Quote) + { + filterArgument = ((UnaryExpression)filterArgument).Operand; + } + + var filterLambda = (LambdaExpression)filterArgument; + + _filterParameter = filterLambda.Parameters.First(); + _filterExpression = filterLambda.Body; + + _filterNestedAccessChecks = ExpressionInfoFinder + .Default + .FindIn( + _filterExpression, + checkMultiInvocations: false, + invertNestedAccessChecks: true) + .NestedAccessChecks; + } + + #region Factory Method + + public static IList<FilterCondition> GetConditions(Expression filter) + { + var finder = new FilterConditionsFinder(); + + finder.Visit(filter); + + return finder.Conditions; + } + + #endregion + + public Expression Filter { get; } + + public bool Filters(Type valueType) + { + if (_appliesToAllSources) + { + return true; + } + + if (!_filteredValueTypeIsNullable) + { + valueType = valueType.GetNonNullableType(); + } + + return valueType.IsAssignableTo(_filteredValueType); + } + + public Expression GetConditionReplacement(Expression sourceValue, ref bool hasFixedValueOperands) + { + if (_appliesToAllSources) + { + return GetFilterCondition(sourceValue.GetConversionToObject()); + } + + var sourceType = sourceValue.Type; + + if (!_filteredValueTypeIsNullable) + { + sourceType = sourceType.GetNonNullableType(); + } + + if (!sourceType.IsAssignableTo(_filteredValueType)) + { + hasFixedValueOperands = true; + return False; + } + + if (sourceType != sourceValue.Type) + { + sourceValue = sourceValue.GetNullableValueAccess(); + } + + return GetFilterCondition(sourceValue); + } + + private Expression GetFilterCondition(Expression sourceValue) + { + var condition = ReplaceFilterParameter(_filterExpression, sourceValue); + + if (_filterNestedAccessChecks == null) + { + return condition; + } + + return Expression.OrElse( + ReplaceFilterParameter(_filterNestedAccessChecks, sourceValue), + condition); + } + + private Expression ReplaceFilterParameter(Expression expression, Expression sourceValue) + => expression.Replace(_filterParameter, sourceValue); + + private class FilterConditionsFinder : ExpressionVisitor + { + public FilterConditionsFinder() + { + Conditions = new List<FilterCondition>(); + } + + public List<FilterCondition> Conditions { get; } + + protected override Expression VisitMethodCall(MethodCallExpression methodCall) + { + if (methodCall.Method.DeclaringType != typeof(SourceValueFilterSpecifier)) + { + return base.VisitMethodCall(methodCall); + } + + var filterValueType = methodCall.Method.IsGenericMethod + ? methodCall.Method.GetGenericArguments().First() + : typeof(object); + + Conditions.Add(new FilterCondition(methodCall, filterValueType)); + + return base.VisitMethodCall(methodCall); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterConstants.cs b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterConstants.cs new file mode 100644 index 000000000..e8e96dd45 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterConstants.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores.SourceValueFilters +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + + internal static class FilterConstants + { + public static readonly Expression True = Expression.Constant(true, typeof(bool)); + public static readonly Expression False = Expression.Constant(false, typeof(bool)); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterOptimiser.cs b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterOptimiser.cs new file mode 100644 index 000000000..36032a8c6 --- /dev/null +++ b/AgileMapper/Configuration/MemberIgnores/SourceValueFilters/FilterOptimiser.cs @@ -0,0 +1,79 @@ +namespace AgileObjects.AgileMapper.Configuration.MemberIgnores.SourceValueFilters +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using static FilterConstants; + + internal class FilterOptimiser : ExpressionVisitor + { + private bool _incomplete; + + public static Expression Optimise(Expression expression) + { + if (expression == False) + { + return expression; + } + + var optimiser = new FilterOptimiser(); + + do + { + optimiser._incomplete = false; + expression = optimiser.VisitAndConvert(expression, nameof(FilterOptimiser)); + } + while (optimiser._incomplete); + + return expression; + } + + protected override Expression VisitUnary(UnaryExpression unary) + { + switch (unary.NodeType) + { + case ExpressionType.Not when unary.Operand == False: + _incomplete = true; + return True; + + case ExpressionType.Not when unary.Operand == True: + _incomplete = true; + return False; + + default: + return base.VisitUnary(unary); + } + } + + protected override Expression VisitBinary(BinaryExpression binary) + { + switch (binary.NodeType) + { + case ExpressionType.AndAlso when binary.Left == False || binary.Right == False: + _incomplete = true; + return False; + + case ExpressionType.OrElse when binary.Left == True || binary.Right == True: + _incomplete = true; + return True; + + case ExpressionType.AndAlso when binary.Left == True: + case ExpressionType.OrElse when binary.Left == False: + _incomplete = true; + // ReSharper disable once AssignNullToNotNullAttribute + return base.Visit(binary.Right); + + case ExpressionType.AndAlso when binary.Right == True: + case ExpressionType.OrElse when binary.Right == False: + _incomplete = true; + // ReSharper disable once AssignNullToNotNullAttribute + return base.Visit(binary.Left); + + default: + return base.VisitBinary(binary); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberSelector.cs b/AgileMapper/Configuration/MemberSelector.cs deleted file mode 100644 index 13802401f..000000000 --- a/AgileMapper/Configuration/MemberSelector.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace AgileObjects.AgileMapper.Configuration -{ - using System; - using System.Diagnostics; - using System.Reflection; - using Members; - using NetStandardPolyfills; - - /// <summary> - /// Provides a fluent interface to select members by their characteristics. - /// </summary> - public class TargetMemberSelector - { - private readonly QualifiedMember _targetMember; - private string _path; - - [DebuggerStepThrough] - internal TargetMemberSelector(QualifiedMember targetMember) - { - _targetMember = targetMember; - } - - /// <summary> - /// Select target members by name. Constructor parameters will not be selected. - /// </summary> - public string Name => _targetMember.Name; - - /// <summary> - /// Select target members by their nested member path. Constructor parameters will not be selected. - /// </summary> - public string Path => _path ?? (_path = GetPath()); - - private string GetPath() - { - var path = _targetMember.GetPath(); - - return path.StartsWith("Target.", StringComparison.Ordinal) - ? path.Substring("Target.".Length) - : path; - } - - /// <summary> - /// Select all target properties. - /// </summary> - public bool IsProperty => TargetMemberIs(MemberType.Property); - - /// <summary> - /// Select any target properties which match the given <paramref name="propertyMatcher"/>. - /// </summary> - /// <param name="propertyMatcher">The predicate with which to select a matching property.</param> - /// <returns> - /// True if the target member is a property matching the given <paramref name="propertyMatcher"/>, - /// otherwise false. - /// </returns> - public bool IsPropertyMatching(Func<PropertyInfo, bool> propertyMatcher) - => IsProperty && TargetMemberInfoMatches(propertyMatcher); - - /// <summary> - /// Select all target fields. - /// </summary> - public bool IsField => TargetMemberIs(MemberType.Field); - - /// <summary> - /// Select any target fields which match the given <paramref name="fieldMatcher"/>. - /// </summary> - /// <param name="fieldMatcher">The predicate with which to select a matching field.</param> - /// <returns> - /// True if the target member is a field matching the given <paramref name="fieldMatcher"/>, otherwise - /// false. - /// </returns> - public bool IsFieldMatching(Func<FieldInfo, bool> fieldMatcher) - => IsField && TargetMemberInfoMatches(fieldMatcher); - - /// <summary> - /// Select all target set methods. - /// </summary> - public bool IsSetMethod => TargetMemberIs(MemberType.SetMethod); - - /// <summary> - /// Select any target set methods which match the given <paramref name="setMethodMatcher"/>. - /// </summary> - /// <param name="setMethodMatcher">The predicate with which to select a matching set method.</param> - /// <returns> - /// True if the target member is a set method matching the given <paramref name="setMethodMatcher"/>, - /// otherwise false. - /// </returns> - public bool IsSetMethodMatching(Func<MethodInfo, bool> setMethodMatcher) - => IsSetMethod && TargetMemberInfoMatches(setMethodMatcher); - - private bool TargetMemberIs(MemberType type) - => _targetMember.LeafMember.MemberType == type; - - private bool TargetMemberInfoMatches<TMemberInfo>(Func<TMemberInfo, bool> matcher) - where TMemberInfo : MemberInfo - { - return matcher.Invoke((TMemberInfo)_targetMember.LeafMember.MemberInfo); - } - - /// <summary> - /// Select target members with the given <typeparamref name="TMember">Type</typeparamref>. Constructor - /// parameters will not be selected. - /// </summary> - /// <typeparam name="TMember">The Type of the target members to select.</typeparam> - /// <returns> - /// True if the target member has the given <typeparamref name="TMember">Type</typeparamref>, otherwise - /// false. - /// </returns> - public bool HasType<TMember>() - { - if (typeof(TMember) == typeof(object)) - { - return _targetMember.Type == typeof(object); - } - - return _targetMember.Type.IsAssignableTo(typeof(TMember)); - } - - /// <summary> - /// Select target members with attributes of the given <typeparamref name="TAttribute">Type</typeparamref>. - /// </summary> - /// <typeparam name="TAttribute">The Type of attribute of the target members to select.</typeparam> - /// <returns> - /// True if the target member has an attribute of the given <typeparamref name="TAttribute">Type</typeparamref>, - /// otherwise false. - /// </returns> - public bool HasAttribute<TAttribute>() - where TAttribute : Attribute - => _targetMember.LeafMember.HasAttribute<TAttribute>(); - } -} \ No newline at end of file diff --git a/AgileMapper/Configuration/MemberSelectorBase.cs b/AgileMapper/Configuration/MemberSelectorBase.cs new file mode 100644 index 000000000..0f84b8d6c --- /dev/null +++ b/AgileMapper/Configuration/MemberSelectorBase.cs @@ -0,0 +1,114 @@ +namespace AgileObjects.AgileMapper.Configuration +{ + using System; + using System.Reflection; + using Members; + using NetStandardPolyfills; + + /// <summary> + /// Provides a fluent interface to select members by their characteristics. + /// </summary> + public abstract class MemberSelectorBase + { + private readonly QualifiedMember _member; + private string _path; + + internal MemberSelectorBase(QualifiedMember member) + { + _member = member; + } + + /// <summary> + /// Select members by name. Constructor parameters will not be selected. + /// </summary> + public string Name => _member.Name; + + /// <summary> + /// Select members by their nested path. Constructor parameters will not be selected. + /// </summary> + public string Path => _path ?? (_path = GetPath()); + + private string GetPath() + { + var path = _member.GetPath(); + + return path.StartsWith(PathPrefix, StringComparison.Ordinal) + ? path.Substring(PathPrefix.Length) + : path; + } + + internal abstract string PathPrefix { get; } + + /// <summary> + /// Select all properties. + /// </summary> + public bool IsProperty => MemberIs(MemberType.Property); + + /// <summary> + /// Select any properties which match the given <paramref name="propertyMatcher"/>. + /// </summary> + /// <param name="propertyMatcher">The predicate with which to select a matching property.</param> + /// <returns> + /// True if the member is a property matching the given <paramref name="propertyMatcher"/>, + /// otherwise false. + /// </returns> + public bool IsPropertyMatching(Func<PropertyInfo, bool> propertyMatcher) + => IsProperty && MemberInfoMatches(propertyMatcher); + + /// <summary> + /// Select all fields. + /// </summary> + public bool IsField => MemberIs(MemberType.Field); + + /// <summary> + /// Select any fields which match the given <paramref name="fieldMatcher"/>. + /// </summary> + /// <param name="fieldMatcher">The predicate with which to select a matching field.</param> + /// <returns> + /// True if the member is a field matching the given <paramref name="fieldMatcher"/>, otherwise + /// false. + /// </returns> + public bool IsFieldMatching(Func<FieldInfo, bool> fieldMatcher) + => IsField && MemberInfoMatches(fieldMatcher); + + internal bool MemberIs(MemberType type) + => _member.LeafMember.MemberType == type; + + internal bool MemberInfoMatches<TMemberInfo>(Func<TMemberInfo, bool> matcher) + where TMemberInfo : MemberInfo + { + return matcher.Invoke((TMemberInfo)_member.LeafMember.MemberInfo); + } + + /// <summary> + /// Select members with the given <typeparamref name="TMember">Type</typeparamref>. Constructor + /// parameters will not be selected. + /// </summary> + /// <typeparam name="TMember">The Type of the members to select.</typeparam> + /// <returns> + /// True if the member has the given <typeparamref name="TMember">Type</typeparamref>, otherwise + /// false. + /// </returns> + public bool HasType<TMember>() + { + if (typeof(TMember) == typeof(object)) + { + return _member.Type == typeof(object); + } + + return _member.Type.IsAssignableTo(typeof(TMember)); + } + + /// <summary> + /// Select members with attributes of the given <typeparamref name="TAttribute">Type</typeparamref>. + /// </summary> + /// <typeparam name="TAttribute">The Type of attribute of the members to select.</typeparam> + /// <returns> + /// True if the member has an attribute of the given <typeparamref name="TAttribute">Type</typeparamref>, + /// otherwise false. + /// </returns> + public bool HasAttribute<TAttribute>() + where TAttribute : Attribute + => _member.LeafMember.HasAttribute<TAttribute>(); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/ParametersSwapper.cs b/AgileMapper/Configuration/ParametersSwapper.cs index 88e1c8507..92cc89c39 100644 --- a/AgileMapper/Configuration/ParametersSwapper.cs +++ b/AgileMapper/Configuration/ParametersSwapper.cs @@ -3,16 +3,16 @@ namespace AgileObjects.AgileMapper.Configuration using System; using System.Collections.Generic; using System.Linq; - using Extensions; - using Extensions.Internal; - using Members; - using NetStandardPolyfills; - using ObjectPopulation; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions; + using Extensions.Internal; + using Members; + using NetStandardPolyfills; + using ObjectPopulation; using static Members.Member; internal class ParametersSwapper @@ -165,12 +165,12 @@ private static Expression ReplaceParameters( { var contextInfo = GetAppropriateMappingContext(swapArgs); - return swapArgs.Lambda.ReplaceParametersWith(parameterFactories.ProjectToArray(f => f.Invoke(contextInfo))); + return swapArgs.Lambda.ReplaceParametersWith(parameterFactories.ProjectToArray(contextInfo, (ci, f) => f.Invoke(ci))); } private static MappingContextInfo GetAppropriateMappingContext(SwapArgs swapArgs) { - if (swapArgs.ContextTypes.All(t => t.IsSimple())) + if (swapArgs.SourceType.IsSimple()) { return GetSimpleTypesMappingContextInfo(swapArgs); } @@ -298,8 +298,8 @@ public MappingContextInfo(SwapArgs swapArgs, Expression contextAccess) : this( swapArgs, contextAccess, - GetValueAccess(swapArgs.GetSourceAccess(contextAccess), swapArgs.ContextTypes[0]), - GetValueAccess(swapArgs.GetTargetAccess(contextAccess), swapArgs.ContextTypes[1])) + GetValueAccess(swapArgs.GetSourceAccess(contextAccess), swapArgs.SourceType), + GetValueAccess(swapArgs.GetTargetAccess(contextAccess), swapArgs.TargetType)) { } @@ -370,6 +370,10 @@ public SwapArgs( public Type[] ContextTypes { get; } + public Type SourceType => ContextTypes[0]; + + public Type TargetType => ContextTypes[1]; + public IMemberMapperData MapperData { get; } public Func<IMemberMapperData, Expression, Type, Expression> TargetValueFactory { get; } @@ -386,10 +390,10 @@ public Expression GetTypedContextAccess(Expression contextAccess) => MapperData.GetTypedContextAccess(contextAccess, ContextTypes); public Expression GetSourceAccess(Expression contextAccess) - => MapperData.GetSourceAccess(contextAccess, ContextTypes[0]); + => MapperData.GetSourceAccess(contextAccess, SourceType); public Expression GetTargetAccess(Expression contextAccess) - => TargetValueFactory.Invoke(MapperData, contextAccess, ContextTypes[1]); + => TargetValueFactory.Invoke(MapperData, contextAccess, TargetType); } #endregion diff --git a/AgileMapper/Configuration/PotentialCloneExtensions.cs b/AgileMapper/Configuration/PotentialCloneExtensions.cs index d723afeb2..de5b5e9ab 100644 --- a/AgileMapper/Configuration/PotentialCloneExtensions.cs +++ b/AgileMapper/Configuration/PotentialCloneExtensions.cs @@ -21,7 +21,7 @@ public static IList<T> CloneItems<T>(this IList<T> cloneableItems) return clonedItems; } - public static void AddSorted<T>(this IList<T> items, T newItem) + public static void AddThenSort<T>(this IList<T> items, T newItem) where T : IComparable<T> { if (items.None()) @@ -42,7 +42,7 @@ public static void AddSorted<T>(this IList<T> items, T newItem) items.Add(newItem); } - public static void AddSortFilter<T>(this List<T> cloneableItems, T newItem) + public static void AddOrReplaceThenSort<T>(this List<T> cloneableItems, T newItem) where T : IPotentialAutoCreatedItem, IComparable<T> { if (cloneableItems.None()) @@ -63,7 +63,7 @@ public static void AddSortFilter<T>(this List<T> cloneableItems, T newItem) return; } - cloneableItems.AddSorted(newItem); + cloneableItems.AddThenSort(newItem); } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/SourceMemberSelector.cs b/AgileMapper/Configuration/SourceMemberSelector.cs new file mode 100644 index 000000000..8755afc1a --- /dev/null +++ b/AgileMapper/Configuration/SourceMemberSelector.cs @@ -0,0 +1,37 @@ +namespace AgileObjects.AgileMapper.Configuration +{ + using System; + using System.Diagnostics; + using System.Reflection; + using Members; + + /// <summary> + /// Provides a fluent interface to select source members by their characteristics. + /// </summary> + public class SourceMemberSelector : MemberSelectorBase + { + [DebuggerStepThrough] + internal SourceMemberSelector(QualifiedMember sourceMember) + : base(sourceMember) + { + } + + internal override string PathPrefix => "Source."; + + /// <summary> + /// Select all set methods. + /// </summary> + public bool IsGetMethod => MemberIs(MemberType.GetMethod); + + /// <summary> + /// Select any source get methods which match the given <paramref name="getMethodMatcher"/>. + /// </summary> + /// <param name="getMethodMatcher">The predicate with which to select a matching get method.</param> + /// <returns> + /// True if the source member is a get method matching the given <paramref name="getMethodMatcher"/>, + /// otherwise false. + /// </returns> + public bool IsGetMethodMatching(Func<MethodInfo, bool> getMethodMatcher) + => IsGetMethod && MemberInfoMatches(getMethodMatcher); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/TargetMemberSelector.cs b/AgileMapper/Configuration/TargetMemberSelector.cs new file mode 100644 index 000000000..b6f71cce4 --- /dev/null +++ b/AgileMapper/Configuration/TargetMemberSelector.cs @@ -0,0 +1,37 @@ +namespace AgileObjects.AgileMapper.Configuration +{ + using System; + using System.Diagnostics; + using System.Reflection; + using Members; + + /// <summary> + /// Provides a fluent interface to select target members by their characteristics. + /// </summary> + public class TargetMemberSelector : MemberSelectorBase + { + [DebuggerStepThrough] + internal TargetMemberSelector(QualifiedMember targetMember) + : base(targetMember) + { + } + + internal override string PathPrefix => "Target."; + + /// <summary> + /// Select all set methods. + /// </summary> + public bool IsSetMethod => MemberIs(MemberType.SetMethod); + + /// <summary> + /// Select any target set methods which match the given <paramref name="setMethodMatcher"/>. + /// </summary> + /// <param name="setMethodMatcher">The predicate with which to select a matching set method.</param> + /// <returns> + /// True if the target member is a set method matching the given <paramref name="setMethodMatcher"/>, + /// otherwise false. + /// </returns> + public bool IsSetMethodMatching(Func<MethodInfo, bool> setMethodMatcher) + => IsSetMethod && MemberInfoMatches(setMethodMatcher); + } +} \ No newline at end of file diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index 3ca074145..526166251 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -3,19 +3,22 @@ using System; using System.Collections.Generic; using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using DataSources; + using DataSources.Factories; using Dictionaries; using Extensions; using Extensions.Internal; + using MemberIgnores; + using MemberIgnores.SourceValueFilters; using Members; using ObjectPopulation; using Projection; using ReadableExpressions.Extensions; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif internal class UserConfigurationSet { @@ -30,7 +33,9 @@ internal class UserConfigurationSet private ConfiguredServiceProvider _namedServiceProvider; private List<ConfiguredObjectFactory> _objectFactories; private MemberIdentifierSet _identifiers; - private List<ConfiguredIgnoredMember> _ignoredMembers; + private List<ConfiguredSourceValueFilter> _sourceValueFilters; + private List<ConfiguredSourceMemberIgnoreBase> _ignoredSourceMembers; + private List<ConfiguredMemberIgnoreBase> _ignoredMembers; private List<EnumMemberPair> _enumPairings; private DictionarySettings _dictionaries; private List<ConfiguredDataSourceFactory> _dataSourceFactories; @@ -62,7 +67,7 @@ public void Add(MappedObjectCachingSetting setting) _mappedObjectCachingSettings, (s, conflicting) => conflicting.GetConflictMessage(s)); - MappedObjectCachingSettings.AddSorted(setting); + MappedObjectCachingSettings.AddThenSort(setting); } public MappedObjectCachingMode CacheMappedObjects(IBasicMapperData basicData) @@ -73,7 +78,7 @@ public MappedObjectCachingMode CacheMappedObjects(IBasicMapperData basicData) } var applicableSettings = _mappedObjectCachingSettings - .FirstOrDefault(tm => tm.AppliesTo(basicData)); + .FirstOrDefault(basicData, (bd, tm) => tm.AppliesTo(bd)); if (applicableSettings == null) { @@ -98,7 +103,7 @@ public void Add(MapToNullCondition condition) ThrowIfConflictingItemExists(condition, conditions, (c, cC) => c.GetConflictMessage()); - conditions.AddSorted(condition); + conditions.AddThenSort(condition); } public Expression GetMapToNullConditionOrNull(IMemberMapperData mapperData) @@ -114,7 +119,7 @@ private List<NullCollectionsSetting> NullCollectionsSettings public void Add(NullCollectionsSetting setting) => NullCollectionsSettings.Add(setting); public bool MapToNullCollections(IBasicMapperData basicData) - => _nullCollectionsSettings?.Any(s => s.AppliesTo(basicData)) == true; + => _nullCollectionsSettings?.Any(basicData, (bd, s) => s.AppliesTo(bd)) == true; #endregion @@ -127,11 +132,18 @@ public void Add(EntityKeyMappingSetting setting) { ThrowIfConflictingKeyMappingSettingExists(setting); - EntityKeyMappingSettings.AddSorted(setting); + EntityKeyMappingSettings.AddThenSort(setting); } public bool MapEntityKeys(IBasicMapperData basicData) - => _entityKeyMappingSettings?.FirstOrDefault(s => s.AppliesTo(basicData))?.MapKeys == true; + { + var applicableSetting = _entityKeyMappingSettings? + .FirstOrDefault(basicData, (bd, s) => s.AppliesTo(bd))? + .MapKeys; + + return (applicableSetting == true) || + (basicData.RuleSet.Settings.AllowEntityKeyMapping && (applicableSetting != false)); + } #endregion @@ -144,7 +156,7 @@ public void Add(DataSourceReversalSetting setting) { ThrowIfConflictingDataSourceReversalSettingExists(setting); - DataSourceReversalSettings.AddSorted(setting); + DataSourceReversalSettings.AddThenSort(setting); } public void AddReverseDataSourceFor(ConfiguredDataSourceFactory dataSourceFactory) @@ -156,7 +168,7 @@ private void AddReverse(ConfiguredDataSourceFactory dataSourceFactory, bool isAu if (reverseDataSourceFactory != null) { - DataSourceFactories.AddSortFilter(reverseDataSourceFactory); + DataSourceFactories.AddOrReplaceThenSort(reverseDataSourceFactory); } } @@ -190,7 +202,8 @@ private bool AutoDataSourceReversalEnabled<T>(T dataItem, Func<T, IBasicMapperDa var basicData = mapperDataFactory.Invoke(dataItem); - return _dataSourceReversalSettings.FirstOrDefault(s => s.AppliesTo(basicData))?.Reverse == true; + return _dataSourceReversalSettings + .FirstOrDefault(basicData, (bd, s) => s.AppliesTo(bd))?.Reverse == true; } #endregion @@ -275,7 +288,7 @@ public void Add(ConfiguredObjectFactory objectFactory) _objectFactories, (of1, of2) => $"An object factory for type {of1.ObjectTypeName} has already been configured"); - ObjectFactories.AddSortFilter(objectFactory); + ObjectFactories.AddOrReplaceThenSort(objectFactory); } public IEnumerable<ConfiguredObjectFactory> GetObjectFactories(IBasicMapperData mapperData) @@ -285,22 +298,63 @@ public IEnumerable<ConfiguredObjectFactory> GetObjectFactories(IBasicMapperData public MemberIdentifierSet Identifiers => _identifiers ?? (_identifiers = new MemberIdentifierSet(_mapperContext)); - #region IgnoredMembers + #region SourceValueFilters + + public bool HasSourceValueFilters => _sourceValueFilters?.Any() == true; + + private List<ConfiguredSourceValueFilter> SourceValueFilters + => _sourceValueFilters ?? (_sourceValueFilters = new List<ConfiguredSourceValueFilter>()); + + public void Add(ConfiguredSourceValueFilter sourceValueFilter) + { + ThrowIfConflictingItemExists(sourceValueFilter, _sourceValueFilters, (svf, cSvf) => svf.GetConflictMessage()); + + SourceValueFilters.AddOrReplaceThenSort(sourceValueFilter); + } + + public IList<ConfiguredSourceValueFilter> GetSourceValueFilters(IBasicMapperData mapperData, Type sourceValueType) + { + return HasSourceValueFilters + ? _sourceValueFilters.Filter(svf => svf.AppliesTo(sourceValueType, mapperData)).ToArray() + : Enumerable<ConfiguredSourceValueFilter>.EmptyArray; + } + + #endregion + + #region MemberIgnores + + public bool HasSourceMemberIgnores => _ignoredSourceMembers?.Any() == true; + + private List<ConfiguredSourceMemberIgnoreBase> IgnoredSourceMembers + => _ignoredSourceMembers ?? (_ignoredSourceMembers = new List<ConfiguredSourceMemberIgnoreBase>()); + + public void Add(ConfiguredSourceMemberIgnoreBase sourceMemberIgnore) + { + ThrowIfConflictingIgnoredSourceMemberExists(sourceMemberIgnore, (ism, cIsm) => ism.GetConflictMessage(cIsm)); + + IgnoredSourceMembers.AddOrReplaceThenSort(sourceMemberIgnore); + } + + public IList<ConfiguredSourceMemberIgnoreBase> GetRelevantSourceMemberIgnores(IBasicMapperData mapperData) + => _ignoredSourceMembers.FindRelevantMatches(mapperData); - private List<ConfiguredIgnoredMember> IgnoredMembers - => _ignoredMembers ?? (_ignoredMembers = new List<ConfiguredIgnoredMember>()); + public ConfiguredSourceMemberIgnoreBase GetSourceMemberIgnoreOrNull(IBasicMapperData mapperData) + => _ignoredSourceMembers.FindMatch(mapperData); - public void Add(ConfiguredIgnoredMember ignoredMember) + private List<ConfiguredMemberIgnoreBase> IgnoredMembers + => _ignoredMembers ?? (_ignoredMembers = new List<ConfiguredMemberIgnoreBase>()); + + public void Add(ConfiguredMemberIgnoreBase memberIgnore) { - ThrowIfMemberIsUnmappable(ignoredMember); - ThrowIfConflictingIgnoredMemberExists(ignoredMember, (im, cIm) => im.GetConflictMessage(cIm)); - ThrowIfConflictingDataSourceExists(ignoredMember, (im, cDsf) => im.GetConflictMessage(cDsf)); + ThrowIfMemberIsUnmappable(memberIgnore); + ThrowIfConflictingIgnoredMemberExists(memberIgnore, (im, cIm) => im.GetConflictMessage(cIm)); + ThrowIfConflictingDataSourceExists(memberIgnore, (im, cDsf) => im.GetConflictMessage(cDsf)); - IgnoredMembers.AddSortFilter(ignoredMember); + IgnoredMembers.AddOrReplaceThenSort(memberIgnore); } - public ConfiguredIgnoredMember GetMemberIgnoreOrNull(IBasicMapperData mapperData) - => _ignoredMembers.FindMatch(mapperData); + public IList<ConfiguredMemberIgnoreBase> GetRelevantMemberIgnores(IBasicMapperData mapperData) + => _ignoredMembers.FindRelevantMatches(mapperData); #endregion @@ -338,7 +392,7 @@ public void Add(ConfiguredDataSourceFactory dataSourceFactory) ThrowIfConflictingDataSourceExists(dataSourceFactory, (dsf, cDsf) => dsf.GetConflictMessage(cDsf)); } - DataSourceFactories.AddSortFilter(dataSourceFactory); + DataSourceFactories.AddOrReplaceThenSort(dataSourceFactory); if (dataSourceFactory.TargetMember.IsRoot) { @@ -353,12 +407,12 @@ public void Add(ConfiguredDataSourceFactory dataSourceFactory) } public ConfiguredDataSourceFactory GetDataSourceFactoryFor(MappingConfigInfo configInfo) - => _dataSourceFactories.First(dsf => dsf.ConfigInfo == configInfo); + => _dataSourceFactories.First(configInfo, (ci, dsf) => dsf.ConfigInfo == ci); public bool HasConfiguredToTargetDataSources { get; private set; } - public IList<IConfiguredDataSource> GetDataSources(IMemberMapperData mapperData) - => GetDataSources(QueryDataSourceFactories(mapperData), mapperData); + public IList<ConfiguredDataSourceFactory> GetRelevantDataSourceFactories(IMemberMapperData mapperData) + => _dataSourceFactories.FindRelevantMatches(mapperData); public IList<IConfiguredDataSource> GetDataSourcesForToTarget(IMemberMapperData mapperData) { @@ -367,18 +421,12 @@ public IList<IConfiguredDataSource> GetDataSourcesForToTarget(IMemberMapperData return Enumerable<IConfiguredDataSource>.EmptyArray; } - var toTargetDataSourceFactories = - QueryDataSourceFactories(mapperData) - .Filter(dsf => dsf.TargetMember.IsRoot); - - return GetDataSources(toTargetDataSourceFactories, mapperData); - } + var toTargetDataSources = QueryDataSourceFactories(mapperData) + .Filter(dsf => dsf.TargetMember.IsRoot) + .Project(mapperData, (md, dsf) => dsf.Create(md)) + .ToArray(); - private static IList<IConfiguredDataSource> GetDataSources( - IEnumerable<ConfiguredDataSourceFactory> factories, - IMemberMapperData mapperData) - { - return factories.Project(dsf => dsf.Create(mapperData)).ToArray(); + return toTargetDataSources; } public IEnumerable<ConfiguredDataSourceFactory> QueryDataSourceFactories(IBasicMapperData mapperData) @@ -418,8 +466,8 @@ private List<ExceptionCallback> ExceptionCallbackFactories public void Add(ExceptionCallback callback) => ExceptionCallbackFactories.Add(callback); - public Expression GetExceptionCallbackOrNull(IBasicMapperData mapperData) - => _exceptionCallbackFactories?.FindMatch(mapperData)?.Callback; + public ExceptionCallback GetExceptionCallbackOrNull(IBasicMapperData mapperData) + => _exceptionCallbackFactories.FindMatch(mapperData); #endregion @@ -449,19 +497,6 @@ public bool ShortCircuitRecursion(IBasicMapperData mapperData) #region Validation - private void ThrowIfMemberIsUnmappable(ConfiguredIgnoredMember ignoredMember) - { - if (ignoredMember.ConfigInfo.ToMapperData().TargetMemberIsUnmappable( - ignoredMember.TargetMember, - QueryDataSourceFactories, - this, - out var reason)) - { - throw new MappingConfigurationException( - $"{ignoredMember.TargetMember.GetPath()} will not be mapped and does not need to be ignored ({reason})"); - } - } - private void ThrowIfConflictingKeyMappingSettingExists(EntityKeyMappingSetting setting) { if ((_entityKeyMappingSettings == null) && !setting.MapKeys) @@ -488,6 +523,27 @@ private void ThrowIfConflictingDataSourceReversalSettingExists(DataSourceReversa (s, conflicting) => conflicting.GetConflictMessage(s)); } + private void ThrowIfMemberIsUnmappable(ConfiguredMemberIgnoreBase memberIgnore) + { + if (memberIgnore.ConfigInfo.ToMapperData().TargetMemberIsUnmappable( + memberIgnore.TargetMember, + QueryDataSourceFactories, + this, + out var reason)) + { + throw new MappingConfigurationException( + $"{memberIgnore.TargetMember.GetPath()} will not be mapped and does not need to be ignored ({reason})"); + } + } + + private void ThrowIfConflictingIgnoredSourceMemberExists<TConfiguredItem>( + TConfiguredItem configuredItem, + Func<TConfiguredItem, ConfiguredSourceMemberIgnoreBase, string> messageFactory) + where TConfiguredItem : UserConfiguredItemBase + { + ThrowIfConflictingItemExists(configuredItem, _ignoredSourceMembers, messageFactory); + } + internal void ThrowIfConflictingIgnoredMemberExists<TConfiguredItem>(TConfiguredItem configuredItem) where TConfiguredItem : UserConfiguredItemBase { @@ -496,7 +552,7 @@ internal void ThrowIfConflictingIgnoredMemberExists<TConfiguredItem>(TConfigured private void ThrowIfConflictingIgnoredMemberExists<TConfiguredItem>( TConfiguredItem configuredItem, - Func<TConfiguredItem, ConfiguredIgnoredMember, string> messageFactory) + Func<TConfiguredItem, ConfiguredMemberIgnoreBase, string> messageFactory) where TConfiguredItem : UserConfiguredItemBase { ThrowIfConflictingItemExists(configuredItem, _ignoredMembers, messageFactory); @@ -518,7 +574,7 @@ private static void ThrowIfConflictingItemExists<TConfiguredItem, TExistingItem> where TExistingItem : UserConfiguredItemBase { var conflictingItem = existingItems? - .FirstOrDefault(ci => ci.ConflictsWith(configuredItem)); + .FirstOrDefault(configuredItem, (sci, ci) => ci.ConflictsWith(sci)); if (conflictingItem == null) { @@ -542,6 +598,8 @@ public void CloneTo(UserConfigurationSet configurations) _dataSourceReversalSettings?.CopyTo(configurations.DataSourceReversalSettings); _objectFactories?.CloneItems().CopyTo(configurations.ObjectFactories); _identifiers?.CloneTo(configurations.Identifiers); + _sourceValueFilters?.CloneItems().CopyTo(configurations.SourceValueFilters); + _ignoredSourceMembers?.CloneItems().CopyTo(configurations.IgnoredSourceMembers); _ignoredMembers?.CloneItems().CopyTo(configurations.IgnoredMembers); _enumPairings?.CopyTo(configurations.EnumPairings); _dictionaries?.CloneTo(configurations.Dictionaries); @@ -564,6 +622,8 @@ public void Reset() _serviceProvider = _namedServiceProvider = null; _objectFactories?.Clear(); _identifiers?.Reset(); + _sourceValueFilters?.Clear(); + _ignoredSourceMembers?.Clear(); _ignoredMembers?.Clear(); _enumPairings?.Clear(); _dictionaries?.Reset(); diff --git a/AgileMapper/Configuration/UserConfiguredItemBase.cs b/AgileMapper/Configuration/UserConfiguredItemBase.cs index 9a84a1d13..f60e1ab45 100644 --- a/AgileMapper/Configuration/UserConfiguredItemBase.cs +++ b/AgileMapper/Configuration/UserConfiguredItemBase.cs @@ -1,15 +1,15 @@ namespace AgileObjects.AgileMapper.Configuration { using System; - using Members; - using NetStandardPolyfills; - using ObjectPopulation; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Members; + using NetStandardPolyfills; + using ObjectPopulation; + using ReadableExpressions.Extensions; internal abstract class UserConfiguredItemBase : IComparable<UserConfiguredItemBase> { @@ -93,14 +93,19 @@ public Expression GetConditionOrNull(IMemberMapperData mapperData) protected virtual Expression GetConditionOrNull(IMemberMapperData mapperData, CallbackPosition position) => ConfigInfo.GetConditionOrNull(mapperData, position, TargetMember); + public bool CouldApplyTo(IBasicMapperData mapperData) + => RuleSetMatches(mapperData) && TypesMatch(mapperData); + public virtual bool AppliesTo(IBasicMapperData mapperData) { - return ConfigInfo.IsFor(mapperData.RuleSet) && + return RuleSetMatches(mapperData) && TargetMembersMatch(mapperData) && HasCompatibleCondition(mapperData) && - MemberPathMatches(mapperData); + TypesMatch(mapperData); } + private bool RuleSetMatches(IBasicMapperData mapperData) => ConfigInfo.IsFor(mapperData.RuleSet); + private bool TargetMembersMatch(IBasicMapperData mapperData) { // The order of these checks is significant! @@ -130,12 +135,12 @@ protected virtual bool TargetMembersAreCompatible(IBasicMapperData mapperData) private bool HasCompatibleCondition(IBasicMapperData mapperData) => !HasConfiguredCondition || ConfigInfo.ConditionSupports(mapperData.RuleSet); - protected virtual bool MemberPathMatches(IBasicMapperData mapperData) - => MemberPathHasMatchingSourceAndTargetTypes(mapperData); + protected virtual bool TypesMatch(IBasicMapperData mapperData) + => SourceAndTargetTypesMatch(mapperData); - protected bool MemberPathHasMatchingSourceAndTargetTypes(IBasicMapperData mapperData) + protected bool SourceAndTargetTypesMatch(IBasicMapperData mapperData) { - if (mapperData.HasCompatibleTypes(ConfigInfo)) + if (TypesAreCompatible(mapperData)) { return true; } @@ -163,6 +168,8 @@ protected bool MemberPathHasMatchingSourceAndTargetTypes(IBasicMapperData mapper } } + protected bool TypesAreCompatible(IBasicMapperData mapperData) => mapperData.HasCompatibleTypes(ConfigInfo); + int IComparable<UserConfiguredItemBase>.CompareTo(UserConfiguredItemBase other) => DoComparisonTo(other); diff --git a/AgileMapper/Constants.cs b/AgileMapper/Constants.cs index 64462339d..402217af6 100644 --- a/AgileMapper/Constants.cs +++ b/AgileMapper/Constants.cs @@ -3,33 +3,33 @@ using System; using System.Collections.Generic; using System.Linq; - using Extensions.Internal; - using NetStandardPolyfills; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using NetStandardPolyfills; internal static class Constants { public static readonly bool ReflectionNotPermitted = ReflectionExtensions.ReflectionNotPermitted; - public static readonly string RootMemberName = "Root"; - public static readonly string EnumerableElementName = "[i]"; + public const string RootMemberName = "Root"; + public const string EnumerableElementName = "[i]"; public static readonly Type[] EmptyTypeArray = Enumerable<Type>.EmptyArray; public static readonly Type AllTypes = typeof(Constants); public static readonly Expression EmptyExpression = Expression.Empty(); - public const string CreateNew = "CreateNew"; + public const string CreateNew = nameof(CreateNew); - public const string Merge = "Merge"; + public const string Merge = nameof(Merge); - public const string Overwrite = "Overwrite"; + public const string Overwrite = nameof(Overwrite); - public const string Project = "Project"; + public const string Project = nameof(Project); public const int BeforeLoopExitCheck = 0; @@ -57,7 +57,7 @@ internal static class Constants typeof(ulong) }; - public static readonly Type[] NumericTypes = WholeNumberNumericTypes + public static readonly IList<Type> NumericTypes = WholeNumberNumericTypes .Append(new[] { typeof(float), typeof(decimal), typeof(double) }); public static readonly IDictionary<Type, double> NumericTypeMaxValuesByType = GetValuesByType("MaxValue"); diff --git a/AgileMapper/DataSources/AdHocDataSource.cs b/AgileMapper/DataSources/AdHocDataSource.cs index ecdcd95ef..a84b3ebab 100644 --- a/AgileMapper/DataSources/AdHocDataSource.cs +++ b/AgileMapper/DataSources/AdHocDataSource.cs @@ -1,12 +1,11 @@ namespace AgileObjects.AgileMapper.DataSources { - using System.Collections.Generic; - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Members; internal class AdHocDataSource : DataSourceBase { @@ -27,8 +26,12 @@ public AdHocDataSource( IQualifiedMember sourceMember, Expression value, Expression condition, - ICollection<ParameterExpression> variables = null) - : base(sourceMember, variables ?? Enumerable<ParameterExpression>.EmptyArray, value, condition) + params ParameterExpression[] variables) + : base( + sourceMember, + variables, + value, + condition) { } } diff --git a/AgileMapper/DataSources/ComplexTypeDataSource.cs b/AgileMapper/DataSources/ComplexTypeDataSource.cs new file mode 100644 index 000000000..051e08440 --- /dev/null +++ b/AgileMapper/DataSources/ComplexTypeDataSource.cs @@ -0,0 +1,65 @@ +namespace AgileObjects.AgileMapper.DataSources +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Extensions.Internal; + using Members; + using ObjectPopulation; + using ObjectPopulation.ComplexTypes; + + internal class ComplexTypeDataSource : DataSourceBase + { + private ComplexTypeDataSource(IDataSource wrappedDataSource, Expression mapping) + : base(wrappedDataSource, mapping) + { + } + + private ComplexTypeDataSource(IQualifiedMember sourceMember, Expression mapping) + : base(sourceMember, mapping) + { + } + + #region Factory Methods + + public static IDataSource Create(IObjectMappingData mappingData) + { + var mapping = ComplexTypeMappingExpressionFactory.Instance.Create(mappingData); + + return new ComplexTypeDataSource(mappingData.MapperData.SourceMember, mapping); + } + + public static IDataSource Create( + IDataSource wrappedDataSource, + int dataSourceIndex, + IChildMemberMappingData complexTypeMappingData) + { + var mapping = MappingFactory.GetChildMapping( + wrappedDataSource.SourceMember, + wrappedDataSource.Value, + dataSourceIndex, + complexTypeMappingData); + + return new ComplexTypeDataSource(wrappedDataSource, mapping); + } + + public static IDataSource Create(int dataSourceIndex, IChildMemberMappingData complexTypeMappingData) + { + var complexTypeMapperData = complexTypeMappingData.MapperData; + var relativeMember = complexTypeMapperData.SourceMember.RelativeTo(complexTypeMapperData.SourceMember); + var sourceMemberAccess = relativeMember.GetQualifiedAccess(complexTypeMapperData); + + var mapping = MappingFactory.GetChildMapping( + relativeMember, + sourceMemberAccess, + dataSourceIndex, + complexTypeMappingData); + + return new ComplexTypeDataSource(complexTypeMapperData.SourceMember, mapping); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/ComplexTypeMappingDataSource.cs b/AgileMapper/DataSources/ComplexTypeMappingDataSource.cs deleted file mode 100644 index 3b9e1f717..000000000 --- a/AgileMapper/DataSources/ComplexTypeMappingDataSource.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources -{ - using Members; - using ObjectPopulation; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - - internal class ComplexTypeMappingDataSource : DataSourceBase - { - public ComplexTypeMappingDataSource( - IDataSource complexTypeDataSource, - int dataSourceIndex, - IChildMemberMappingData complexTypeMappingData) - : base( - complexTypeDataSource, - GetMapping(complexTypeDataSource, dataSourceIndex, complexTypeMappingData)) - { - } - - private static Expression GetMapping( - IDataSource complexTypeDataSource, - int dataSourceIndex, - IChildMemberMappingData complexTypeMappingData) - { - var mapping = MappingFactory.GetChildMapping( - complexTypeDataSource.SourceMember, - complexTypeDataSource.Value, - dataSourceIndex, - complexTypeMappingData); - - return mapping; - } - - public ComplexTypeMappingDataSource(int dataSourceIndex, IChildMemberMappingData complexTypeMappingData) - : base(complexTypeMappingData.MapperData.SourceMember, GetMapping(dataSourceIndex, complexTypeMappingData)) - { - } - - private static Expression GetMapping(int dataSourceIndex, IChildMemberMappingData complexTypeMappingData) - { - var complexTypeMapperData = complexTypeMappingData.MapperData; - var relativeMember = complexTypeMapperData.SourceMember.RelativeTo(complexTypeMapperData.SourceMember); - var sourceMemberAccess = relativeMember.GetQualifiedAccess(complexTypeMapperData); - - return MappingFactory.GetChildMapping( - relativeMember, - sourceMemberAccess, - dataSourceIndex, - complexTypeMappingData); - } - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/ConfiguredDataSource.cs b/AgileMapper/DataSources/ConfiguredDataSource.cs index 319bfd5c7..37edb4f0e 100644 --- a/AgileMapper/DataSources/ConfiguredDataSource.cs +++ b/AgileMapper/DataSources/ConfiguredDataSource.cs @@ -1,12 +1,12 @@ namespace AgileObjects.AgileMapper.DataSources { - using Extensions.Internal; - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; internal class ConfiguredDataSource : DataSourceBase, IConfiguredDataSource { diff --git a/AgileMapper/DataSources/DataSourceBase.cs b/AgileMapper/DataSources/DataSourceBase.cs index 35a0174a6..8fa87e070 100644 --- a/AgileMapper/DataSources/DataSourceBase.cs +++ b/AgileMapper/DataSources/DataSourceBase.cs @@ -1,14 +1,14 @@ namespace AgileObjects.AgileMapper.DataSources { using System.Collections.Generic; - using Extensions.Internal; - using Members; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; + using ReadableExpressions.Extensions; internal abstract class DataSourceBase : IDataSource { @@ -29,7 +29,7 @@ protected DataSourceBase(IDataSource wrappedDataSource, Expression value) protected DataSourceBase( IQualifiedMember sourceMember, - ICollection<ParameterExpression> variables, + IList<ParameterExpression> variables, Expression value, Expression condition = null) { @@ -117,7 +117,7 @@ private Expression GetCondition(Expression nestedAccessChecks, IMemberMapperData if (sourceMemberValue.Type.IsNullableType()) { - sourceMemberValue = Expression.Property(sourceMemberValue, "Value"); + sourceMemberValue = sourceMemberValue.GetNullableValueAccess(); } var zero = 0.ToConstantExpression(sourceValueType); @@ -168,7 +168,7 @@ private static bool IsNotOptionalEntityMemberId(IMemberMapperData mapperData) .Instance .MemberCache .GetTargetMembers(mapperData.TargetType) - .FirstOrDefault(m => m.Name == entityMemberName); + .FirstOrDefault(entityMemberName, (emn, m) => m.Name == emn); return !mapperData.IsEntity(entityMember?.Type, out _); } @@ -181,23 +181,29 @@ private static bool IsNotOptionalEntityMemberId(IMemberMapperData mapperData) public virtual bool IsValid => Value != Constants.EmptyExpression; - public virtual Expression PreCondition => null; - public bool IsConditional => Condition != null; + public virtual bool IsFallback => false; + public virtual Expression Condition { get; } - public ICollection<ParameterExpression> Variables { get; } + public IList<ParameterExpression> Variables { get; } public Expression Value { get; } - public virtual Expression AddPreCondition(Expression population) => population; + public virtual Expression AddSourceCondition(Expression value) => value; - public Expression AddCondition(Expression value, Expression alternateBranch = null) + public virtual Expression FinalisePopulation(Expression population, Expression alternatePopulation) { - return alternateBranch != null - ? Expression.IfThenElse(Condition, value, alternateBranch) - : Expression.IfThen(Condition, value); + if (!IsConditional) + { + return population; + } + + return (alternatePopulation != null) + ? Expression.IfThenElse(Condition, population, alternatePopulation) + : Expression.IfThen(Condition, population); + } } } \ No newline at end of file diff --git a/AgileMapper/DataSources/DataSourceFilteringExtensions.cs b/AgileMapper/DataSources/DataSourceFilteringExtensions.cs new file mode 100644 index 000000000..cd231d344 --- /dev/null +++ b/AgileMapper/DataSources/DataSourceFilteringExtensions.cs @@ -0,0 +1,124 @@ +namespace AgileObjects.AgileMapper.DataSources +{ + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Configuration.MemberIgnores.SourceValueFilters; + using Extensions.Internal; + using Members; + + internal static class DataSourceFilteringExtensions + { + public static IList<IDataSource> WithFilters( + this IList<IDataSource> dataSources, + IMemberMapperData mapperData) + { + var dataSourceCount = dataSources.Count; + var filteredDataSources = new IDataSource[dataSourceCount]; + + for (var i = 0; i < dataSourceCount; ++i) + { + var dataSource = dataSources[i]; + + var filteredDataSource = filteredDataSources[i] = ApplyFilter( + dataSource.IsFallback ? dataSources[i - 1].SourceMember : dataSource.SourceMember, + dataSource, + mapperData); + + if (!dataSource.IsFallback) + { + continue; + } + + if (filteredDataSource != null) + { + break; + } + + var filteredDataSourcesWithoutFallback = new IDataSource[dataSourceCount - 1]; + + filteredDataSourcesWithoutFallback.CopyFrom(filteredDataSources); + + return filteredDataSourcesWithoutFallback; + } + + return filteredDataSources; + } + + public static IDataSource WithFilter(this IDataSource dataSource, IMemberMapperData mapperData) + => ApplyFilter(dataSource.SourceMember, dataSource, mapperData); + + private static IDataSource ApplyFilter( + IQualifiedMember sourceMember, + IDataSource dataSource, + IMemberMapperData mapperData) + { + if (DoNotApplyFilter(sourceMember, dataSource, mapperData)) + { + return dataSource; + } + + var filters = mapperData.GetSourceValueFilters(sourceMember.Type); + + if (filters.None()) + { + return dataSource; + } + + var contextMapperData = mapperData.IsEntryPoint || (sourceMember != mapperData.SourceMember) + ? mapperData + : mapperData.Parent; + + var rawSourceValue = sourceMember + .RelativeTo(contextMapperData.SourceMember) + .GetQualifiedAccess(contextMapperData.SourceObject); + + var filterConditions = filters.GetFilterConditionsOrNull(rawSourceValue, contextMapperData); + + if (filterConditions == null) + { + return dataSource; + } + + if (dataSource.IsConditional) + { + filterConditions = Expression.AndAlso(dataSource.Condition, filterConditions); + } + + return new AdHocDataSource(sourceMember, dataSource.Value, filterConditions); + } + + private static bool DoNotApplyFilter( + IQualifiedMember sourceMember, + IDataSource dataSource, + IMemberMapperData mapperData) + { + if (!dataSource.IsValid) + { + return true; + } + + // Non-simple enumerable elements will be filtered out elsewhere, + // unless they're being runtime-typed: + return !sourceMember.IsSimple && !mapperData.IsEntryPoint && + mapperData.TargetMemberIsEnumerableElement(); + } + + public static Expression GetFilterConditionsOrNull( + this IList<ConfiguredSourceValueFilter> filters, + Expression sourceValue, + IMemberMapperData mapperData) + { + return filters.HasOne() + ? filters.First().GetConditionOrNull(sourceValue, mapperData) + : filters + .ProjectToArray( + new { sourceValue, mapperData }, + (d, filter) => filter.GetConditionOrNull(d.sourceValue, d.mapperData)) + .AndTogether(); + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index bb226972a..eaf743498 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -1,171 +1,232 @@ namespace AgileObjects.AgileMapper.DataSources { + using System; using System.Collections; using System.Collections.Generic; - using Extensions.Internal; - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; - internal class DataSourceSet : IEnumerable<IDataSource> + internal static class DataSourceSet { - private readonly IList<IDataSource> _dataSources; - private Expression _value; + #region Factory Methods - public DataSourceSet(IMemberMapperData mapperData, params IDataSource[] dataSources) + public static IDataSourceSet For( + IDataSource dataSource, + IMemberMapperData mapperData, + Func<IList<IDataSource>, IMemberMapperData, Expression> valueBuilder = null) { - MapperData = mapperData; - _dataSources = dataSources; - None = dataSources.Length == 0; - - if (None) + if (mapperData.MapperContext.UserConfigurations.HasSourceValueFilters) { - Variables = Enumerable<ParameterExpression>.EmptyArray; - return; + dataSource = dataSource.WithFilter(mapperData); } - var variables = new List<ParameterExpression>(); + return new SingleValueDataSourceSet(dataSource, mapperData, valueBuilder); + } - for (var i = 0; i < dataSources.Length;) + public static IDataSourceSet For( + IList<IDataSource> dataSources, + IMemberMapperData mapperData, + Func<IList<IDataSource>, IMemberMapperData, Expression> valueBuilder = null) + { + if (dataSources.HasOne()) { - var dataSource = dataSources[i++]; + return For(dataSources.First(), mapperData, valueBuilder); + } - if (dataSource.IsValid) - { - HasValue = true; - } + if (mapperData.MapperContext.UserConfigurations.HasSourceValueFilters) + { + dataSources = dataSources.WithFilters(mapperData); + } - if (dataSource.IsConditional) - { - IsConditional = true; - } + return new MultipleValueDataSourceSet(dataSources, mapperData, valueBuilder); + } + + #endregion - if (dataSource.Variables.Any()) + private class SingleValueDataSourceSet : IDataSourceSet + { + private readonly IDataSource _dataSource; + private readonly Func<IDataSource, IMemberMapperData, Expression> _valueBuilder; + private Expression _value; + + public SingleValueDataSourceSet( + IDataSource dataSource, + IMemberMapperData mapperData, + Func<IList<IDataSource>, IMemberMapperData, Expression> valueBuilder) + { + _dataSource = dataSource; + MapperData = mapperData; + + if (valueBuilder == null) { - variables.AddRange(dataSource.Variables); + _valueBuilder = ValueExpressionBuilders.SingleDataSource; } - - if (dataSource.SourceMemberTypeTest != null) + else { - SourceMemberTypeTest = dataSource.SourceMemberTypeTest; + _valueBuilder = (ds, md) => valueBuilder.Invoke(new[] { ds }, md); } } - Variables = variables; - } + public IMemberMapperData MapperData { get; } - public IMemberMapperData MapperData { get; } + public bool None => false; - public bool None { get; } + public bool HasValue => _dataSource.IsValid; - public bool HasValue { get; } + public bool IsConditional => _dataSource.IsConditional; - public bool IsConditional { get; } + public Expression SourceMemberTypeTest => _dataSource.SourceMemberTypeTest; - public Expression SourceMemberTypeTest { get; } + public IList<ParameterExpression> Variables => _dataSource.Variables; - public IList<ParameterExpression> Variables { get; } + public IDataSource this[int index] => _dataSource; - public IDataSource this[int index] => _dataSources[index]; + public int Count => 1; - public Expression ValueExpression => _value ?? (_value = BuildValueExpression()); + public Expression BuildValue() + => _value ?? (_value = _valueBuilder.Invoke(_dataSource, MapperData)); - private Expression BuildValueExpression() - { - var value = default(Expression); + public Expression GetFinalValueOrNull() => _dataSource.Value; + + #region IEnumerable<IDataSource> Members - for (var i = _dataSources.Count - 1; i >= 0;) + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator<IDataSource> GetEnumerator() { - var dataSource = _dataSources[i--]; - - value = dataSource.AddPreConditionIfNecessary(value == default(Expression) - ? dataSource.Value - : Expression.Condition( - dataSource.Condition, - dataSource.Value.GetConversionTo(value.Type), - value)); + yield return _dataSource; } - return value; + #endregion } - public Expression GetPopulationExpression() + private class MultipleValueDataSourceSet : IDataSourceSet { - var fallbackValue = GetFallbackValueOrNull(); - var excludeFallback = fallbackValue == null; + private readonly IList<IDataSource> _dataSources; + private readonly Func<IList<IDataSource>, IMemberMapperData, Expression> _valueBuilder; + private Expression _value; + + public MultipleValueDataSourceSet( + IList<IDataSource> dataSources, + IMemberMapperData mapperData, + Func<IList<IDataSource>, IMemberMapperData, Expression> valueBuilder) + { + _dataSources = dataSources; + MapperData = mapperData; + None = dataSources.Count == 0; + + if (None) + { + Variables = Enumerable<ParameterExpression>.EmptyArray; + return; + } - Expression population = null; + _valueBuilder = valueBuilder ?? ValueExpressionBuilders.ConditionTree; - for (var i = _dataSources.Count - 1; i >= 0; --i) - { - var dataSource = _dataSources[i]; + var variables = default(List<ParameterExpression>); - if (i == _dataSources.Count - 1) + for (var i = 0; i < dataSources.Count;) { - if (excludeFallback) + var dataSource = dataSources[i++]; + + if (dataSource.IsValid) { - continue; + HasValue = true; } - population = MapperData.GetTargetMemberPopulation(fallbackValue); - if (dataSource.IsConditional) { - population = dataSource.AddCondition(population); + IsConditional = true; } - population = dataSource.AddPreCondition(population); - continue; - } + if (dataSource.Variables.Any()) + { + if (variables == null) + { + variables = new List<ParameterExpression>(); + } - var memberPopulation = MapperData.GetTargetMemberPopulation(dataSource.Value); + variables.AddRange(dataSource.Variables); + } - population = dataSource.AddCondition(memberPopulation, population); - population = dataSource.AddPreCondition(population); + if (dataSource.SourceMemberTypeTest != null) + { + SourceMemberTypeTest = dataSource.SourceMemberTypeTest; + } + } + + Variables = (variables != null) + ? (IList<ParameterExpression>)variables + : Enumerable<ParameterExpression>.EmptyArray; } - return population; - } + public IMemberMapperData MapperData { get; } - private Expression GetFallbackValueOrNull() - { - var finalDataSource = _dataSources.Last(); - var fallbackValue = finalDataSource.Value; + public bool None { get; } - if (finalDataSource.IsConditional || _dataSources.HasOne()) - { - return fallbackValue; - } + public bool HasValue { get; } - if (fallbackValue.NodeType == ExpressionType.Coalesce) - { - return ((BinaryExpression)fallbackValue).Right; - } + public bool IsConditional { get; } + + public Expression SourceMemberTypeTest { get; } + + public IList<ParameterExpression> Variables { get; } + + public IDataSource this[int index] => _dataSources[index]; + + public int Count => _dataSources.Count; - var targetMemberAccess = MapperData.GetTargetMemberAccess(); + public Expression BuildValue() + => _value ?? (_value = _valueBuilder.Invoke(_dataSources, MapperData)); - if (ExpressionEvaluation.AreEqual(fallbackValue, targetMemberAccess)) + public Expression GetFinalValueOrNull() { - return null; - } + var finalDataSource = _dataSources.Last(); + var finalValue = finalDataSource.Value; - return fallbackValue; - } + if (!finalDataSource.IsFallback) + { + return finalValue; + } + + if (finalValue.NodeType == ExpressionType.Coalesce) + { + // Coalesce between the existing target member value and the fallback: + return ((BinaryExpression)finalValue).Right; + } + + var targetMemberAccess = MapperData.GetTargetMemberAccess(); - #region IEnumerable<IDataSource> Members + if (ExpressionEvaluation.AreEqual(finalValue, targetMemberAccess)) + { + return null; + } + + return finalValue; + } - public IEnumerator<IDataSource> GetEnumerator() => _dataSources.GetEnumerator(); + #region IEnumerable<IDataSource> Members - #region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if DEBUG - [ExcludeFromCodeCoverage] + [ExcludeFromCodeCoverage] #endif - #endregion - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - #endregion + public IEnumerator<IDataSource> GetEnumerator() => _dataSources.GetEnumerator(); + + #endregion + } } } \ No newline at end of file diff --git a/AgileMapper/DataSources/DefaultValueDataSource.cs b/AgileMapper/DataSources/DefaultValueDataSource.cs deleted file mode 100644 index 38d1f8191..000000000 --- a/AgileMapper/DataSources/DefaultValueDataSource.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources -{ - using Members; - - internal class DefaultValueDataSource : DataSourceBase - { - public DefaultValueDataSource(IMemberMapperData mapperData) - : base(mapperData.SourceMember, mapperData.GetTargetMemberDefault()) - { - } - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/DefaultValueDataSourceFactory.cs b/AgileMapper/DataSources/DefaultValueDataSourceFactory.cs deleted file mode 100644 index de2890765..000000000 --- a/AgileMapper/DataSources/DefaultValueDataSourceFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources -{ - using Members; - - internal struct DefaultValueDataSourceFactory : IDataSourceFactory - { - public IDataSource Create(IMemberMapperData mapperData) - => new DefaultValueDataSource(mapperData); - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/DictionaryEntryDataSource.cs b/AgileMapper/DataSources/DictionaryEntryDataSource.cs index d6b6d76b0..6f44b8220 100644 --- a/AgileMapper/DataSources/DictionaryEntryDataSource.cs +++ b/AgileMapper/DataSources/DictionaryEntryDataSource.cs @@ -1,11 +1,11 @@ namespace AgileObjects.AgileMapper.DataSources { - using Extensions.Internal; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; internal class DictionaryEntryDataSource : DataSourceBase { @@ -50,7 +50,12 @@ private static Expression GetValidEntryExistsTest(DictionaryEntryVariablePair di return valueNonNull; } - public override Expression PreCondition => _preCondition ?? (_preCondition = CreatePreCondition()); + public override Expression AddSourceCondition(Expression value) + { + var preCondition = _preCondition ?? (_preCondition = CreatePreCondition()); + + return value.ToIfFalseDefaultCondition(preCondition); + } private Expression CreatePreCondition() { @@ -66,8 +71,10 @@ private Expression CreatePreCondition() return Expression.Block(keyAssignment, matchingKeyExists); } - public override Expression AddPreCondition(Expression population) + public override Expression FinalisePopulation(Expression population, Expression alternatePopulation) { + population = base.FinalisePopulation(population, alternatePopulation); + var matchingKeyExists = GetMatchingKeyExistsTest(); var ifKeyExistsPopulate = Expression.IfThen(matchingKeyExists, population); diff --git a/AgileMapper/DataSources/DictionaryEntryVariablePair.cs b/AgileMapper/DataSources/DictionaryEntryVariablePair.cs index 41ebe079b..a822a42f2 100644 --- a/AgileMapper/DataSources/DictionaryEntryVariablePair.cs +++ b/AgileMapper/DataSources/DictionaryEntryVariablePair.cs @@ -59,7 +59,7 @@ private static string GetTargetMemberName(IBasicMapperData mapperData) public IMemberMapperData MapperData { get; } - public ICollection<ParameterExpression> Variables { get; } + public IList<ParameterExpression> Variables { get; } public ParameterExpression Key => _key ?? (_key = Expression.Variable(SourceMember.KeyType, _targetMemberName + "Key")); diff --git a/AgileMapper/DataSources/EnumerableMappingDataSource.cs b/AgileMapper/DataSources/EnumerableDataSource.cs similarity index 95% rename from AgileMapper/DataSources/EnumerableMappingDataSource.cs rename to AgileMapper/DataSources/EnumerableDataSource.cs index 059f1d13a..b68f50644 100644 --- a/AgileMapper/DataSources/EnumerableMappingDataSource.cs +++ b/AgileMapper/DataSources/EnumerableDataSource.cs @@ -2,20 +2,20 @@ { using System; using System.Linq; - using Extensions; - using Extensions.Internal; - using Members; - using ObjectPopulation; - using ObjectPopulation.Enumerables; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions; + using Extensions.Internal; + using Members; + using ObjectPopulation; + using ObjectPopulation.Enumerables; - internal class EnumerableMappingDataSource : DataSourceBase + internal class EnumerableDataSource : DataSourceBase { - public EnumerableMappingDataSource( + public EnumerableDataSource( IDataSource sourceEnumerableDataSource, int dataSourceIndex, IChildMemberMappingData enumerableMappingData) @@ -91,7 +91,7 @@ private static bool IsNotMappingFromLinkingType( .GetSourceMembers(sourceElementType); var backLinkMember = sourceElementMembers - .FirstOrDefault(m => m.IsComplex && m.Type == mapperData.SourceType); + .FirstOrDefault(mapperData.SourceType, (st, m) => m.IsComplex && m.Type == st); if (backLinkMember == null) { @@ -100,7 +100,7 @@ private static bool IsNotMappingFromLinkingType( } var otherComplexTypeMembers = sourceElementMembers - .Filter(m => m.IsComplex && (m.Type != mapperData.SourceType)) + .Filter(mapperData, (md, m) => m.IsComplex && (m.Type != md.SourceType)) .ToArray(); if ((otherComplexTypeMembers.Length != 1) || diff --git a/AgileMapper/DataSources/ExistingMemberValueOrDefaultDataSource.cs b/AgileMapper/DataSources/ExistingMemberValueOrDefaultDataSource.cs deleted file mode 100644 index 4525eb024..000000000 --- a/AgileMapper/DataSources/ExistingMemberValueOrDefaultDataSource.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources -{ - using Members; - using Members.Dictionaries; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - - internal class ExistingMemberValueOrDefaultDataSource : DataSourceBase - { - public ExistingMemberValueOrDefaultDataSource(IMemberMapperData mapperData) - : base(mapperData.SourceMember, GetValue(mapperData), mapperData) - { - } - - private static Expression GetValue(IMemberMapperData mapperData) - { - if (mapperData.TargetMember.IsEnumerable) - { - return FallbackToCollection(mapperData) - ? mapperData.GetFallbackCollectionValue() - : mapperData.GetTargetMemberDefault(); - } - - if (mapperData.TargetMember.IsReadable && !mapperData.UseMemberInitialisations()) - { - return mapperData.GetTargetMemberAccess(); - } - - return mapperData.GetTargetMemberDefault(); - } - - private static bool FallbackToCollection(IBasicMapperData mapperData) - { - if (mapperData.TargetMember.IsDictionary) - { - return true; - } - - if (!(mapperData.TargetMember is DictionaryTargetMember dictionaryTargetMember)) - { - return true; - } - - return dictionaryTargetMember.HasEnumerableEntries; - } - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/ExistingOrDefaultValueDataSourceFactory.cs b/AgileMapper/DataSources/ExistingOrDefaultValueDataSourceFactory.cs deleted file mode 100644 index c1335c979..000000000 --- a/AgileMapper/DataSources/ExistingOrDefaultValueDataSourceFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources -{ - using Members; - - internal struct ExistingOrDefaultValueDataSourceFactory : IDataSourceFactory - { - public IDataSource Create(IMemberMapperData mapperData) - => new ExistingMemberValueOrDefaultDataSource(mapperData); - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/ConfiguredDataSourceFactory.cs b/AgileMapper/DataSources/Factories/ConfiguredDataSourceFactory.cs similarity index 99% rename from AgileMapper/DataSources/ConfiguredDataSourceFactory.cs rename to AgileMapper/DataSources/Factories/ConfiguredDataSourceFactory.cs index 284cdea9c..e2485dd10 100644 --- a/AgileMapper/DataSources/ConfiguredDataSourceFactory.cs +++ b/AgileMapper/DataSources/Factories/ConfiguredDataSourceFactory.cs @@ -1,15 +1,13 @@ -namespace AgileObjects.AgileMapper.DataSources +namespace AgileObjects.AgileMapper.DataSources.Factories { #if NET35 using System; -#endif - using Configuration; - using Members; -#if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Configuration; + using Members; internal class ConfiguredDataSourceFactory : UserConfiguredItemBase, diff --git a/AgileMapper/DataSources/Finders/ConfiguredDataSourceFinder.cs b/AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs similarity index 68% rename from AgileMapper/DataSources/Finders/ConfiguredDataSourceFinder.cs rename to AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs index 7a4f916f6..1ddc4815d 100644 --- a/AgileMapper/DataSources/Finders/ConfiguredDataSourceFinder.cs +++ b/AgileMapper/DataSources/Factories/ConfiguredDataSourcesFactory.cs @@ -1,11 +1,11 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders +namespace AgileObjects.AgileMapper.DataSources.Factories { using System.Collections.Generic; using Extensions.Internal; - internal struct ConfiguredDataSourceFinder : IDataSourceFinder + internal static class ConfiguredDataSourcesFactory { - public IEnumerable<IDataSource> FindFor(DataSourceFindContext context) + public static IEnumerable<IDataSource> Create(DataSourceFindContext context) { if (context.ConfiguredDataSources.None()) { @@ -20,8 +20,6 @@ public IEnumerable<IDataSource> FindFor(DataSourceFindContext context) { yield break; } - - ++context.DataSourceIndex; } } } diff --git a/AgileMapper/DataSources/ConfiguredDictionaryEntryDataSourceFactory.cs b/AgileMapper/DataSources/Factories/ConfiguredDictionaryEntryDataSourceFactory.cs similarity index 95% rename from AgileMapper/DataSources/ConfiguredDictionaryEntryDataSourceFactory.cs rename to AgileMapper/DataSources/Factories/ConfiguredDictionaryEntryDataSourceFactory.cs index 04f72efec..e79ec6f81 100644 --- a/AgileMapper/DataSources/ConfiguredDictionaryEntryDataSourceFactory.cs +++ b/AgileMapper/DataSources/Factories/ConfiguredDictionaryEntryDataSourceFactory.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.DataSources +namespace AgileObjects.AgileMapper.DataSources.Factories { using Configuration; using Members; diff --git a/AgileMapper/DataSources/Factories/DataSourceFindContext.cs b/AgileMapper/DataSources/Factories/DataSourceFindContext.cs new file mode 100644 index 000000000..2864ca9a2 --- /dev/null +++ b/AgileMapper/DataSources/Factories/DataSourceFindContext.cs @@ -0,0 +1,160 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using System.Collections.Generic; + using System.Linq; + using Configuration; + using Extensions; + using Extensions.Internal; + using Members; + + internal class DataSourceFindContext + { + private IList<ConfiguredDataSourceFactory> _relevantConfiguredDataSourceFactories; + private IList<IConfiguredDataSource> _configuredDataSources; + private SourceMemberMatchContext _sourceMemberMatchContext; + private SourceMemberMatch _bestSourceMemberMatch; + private IDataSource _matchingSourceMemberDataSource; + + public DataSourceFindContext(IChildMemberMappingData memberMappingData) + { + MemberMappingData = memberMappingData; + } + + public MapperContext MapperContext => MemberMapperData.MapperContext; + + public IChildMemberMappingData MemberMappingData { get; private set; } + + public IMemberMapperData MemberMapperData => MemberMappingData.MapperData; + + public QualifiedMember TargetMember => MemberMapperData.TargetMember; + + public int DataSourceIndex { get; set; } + + public bool StopFind { get; set; } + + private IEnumerable<ConfiguredDataSourceFactory> RelevantConfiguredDataSourceFactories + => _relevantConfiguredDataSourceFactories ?? + (_relevantConfiguredDataSourceFactories = GetRelevantConfiguredDataSourceFactories()); + + private IList<ConfiguredDataSourceFactory> GetRelevantConfiguredDataSourceFactories() + { + var relevantDataSourceFactories = GetRelevantConfiguredDataSourceFactories(MemberMapperData); + + if (!MemberMapperData.Parent.Context.IsForToTargetMapping) + { + return relevantDataSourceFactories; + } + + var originalChildMapperData = new ChildMemberMapperData( + TargetMember, + MemberMapperData.Parent.OriginalMapperData); + + relevantDataSourceFactories = relevantDataSourceFactories.Append( + GetRelevantConfiguredDataSourceFactories(originalChildMapperData)); + + return relevantDataSourceFactories; + } + + private IList<ConfiguredDataSourceFactory> GetRelevantConfiguredDataSourceFactories(IMemberMapperData mapperData) + => MapperContext.UserConfigurations.GetRelevantDataSourceFactories(mapperData); + + public IList<IConfiguredDataSource> ConfiguredDataSources + { + get + { + return _configuredDataSources ?? (_configuredDataSources = + RelevantConfiguredDataSourceFactories + .FindMatches(MemberMapperData) + .Project(MemberMapperData, (md, dsf) => dsf.Create(md)) + .ToArray()); + } + } + + public IDataSource MatchingSourceMemberDataSource + => _matchingSourceMemberDataSource ?? (_matchingSourceMemberDataSource = GetSourceMemberDataSource()); + + private IDataSource GetSourceMemberDataSource() + { + if (BestSourceMemberMatch.IsUseable) + { + return GetFinalDataSource( + _bestSourceMemberMatch.CreateDataSource(), + _bestSourceMemberMatch.ContextMappingData); + } + + return new AdHocDataSource( + _bestSourceMemberMatch.SourceMember, + Constants.EmptyExpression); + } + + public SourceMemberMatch BestSourceMemberMatch => + _bestSourceMemberMatch ?? + (_bestSourceMemberMatch = SourceMemberMatcher.GetMatchFor(SourceMemberMatchContext)); + + private SourceMemberMatchContext SourceMemberMatchContext => + (_sourceMemberMatchContext != null) + ? _sourceMemberMatchContext.With(MemberMappingData) + : _sourceMemberMatchContext = new SourceMemberMatchContext(MemberMappingData); + + public IDataSource GetFallbackDataSource() + => MemberMappingData.RuleSet.FallbackDataSourceFactory.Invoke(MemberMapperData); + + public IDataSource GetFinalDataSource(IDataSource foundDataSource) + => GetFinalDataSource(foundDataSource, MemberMappingData); + + public IDataSource GetFinalDataSource(IDataSource foundDataSource, IChildMemberMappingData mappingData) + { + var childTargetMember = mappingData.MapperData.TargetMember; + + if (UseComplexTypeDataSource(foundDataSource, childTargetMember)) + { + return ComplexTypeDataSource.Create(foundDataSource, DataSourceIndex, mappingData); + } + + if (childTargetMember.IsEnumerable && foundDataSource.SourceMember.IsEnumerable) + { + return new EnumerableDataSource(foundDataSource, DataSourceIndex, mappingData); + } + + return foundDataSource; + } + + private static bool UseComplexTypeDataSource(IDataSource dataSource, QualifiedMember targetMember) + { + if (!targetMember.IsComplex) + { + return false; + } + + if (targetMember.IsDictionary) + { + return true; + } + + if (targetMember.Type == typeof(object)) + { + return !dataSource.SourceMember.Type.IsSimple(); + } + + if ((dataSource.Value.Type == targetMember.Type) && + (dataSource.SourceMember.Type != targetMember.Type)) + { + return false; + } + + return !targetMember.Type.IsFromBcl(); + } + + public DataSourceFindContext With(IChildMemberMappingData memberMappingData) + { + MemberMappingData = memberMappingData; + _configuredDataSources = null; + _sourceMemberMatchContext = null; + _bestSourceMemberMatch = null; + _matchingSourceMemberDataSource = null; + DataSourceIndex = 0; + StopFind = false; + return this; + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/DataSourceSetFactory.cs b/AgileMapper/DataSources/Factories/DataSourceSetFactory.cs new file mode 100644 index 000000000..1d022063c --- /dev/null +++ b/AgileMapper/DataSources/Factories/DataSourceSetFactory.cs @@ -0,0 +1,73 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using System.Collections.Generic; + using System.Linq; + using Extensions.Internal; + using MappingRoot; + using ObjectPopulation; + + internal static class DataSourceSetFactory + { + private static readonly IMappingRootDataSourceFactory[] _mappingRootDataSourceFactories = + { + new QueryProjectionRootDataSourceFactory(), + new EnumMappingRootDataSourceFactory(), + new DictionaryMappingRootDataSourceFactory(), + new EnumerableMappingRootDataSourceFactory(), + new ComplexTypeMappingRootDataSourceFactory() + }; + + private static readonly DataSourcesFactory[] _childDataSourceFactories = + { + ConfiguredDataSourcesFactory.Create, + MaptimeDataSourcesFactory.Create, + SourceMemberDataSourcesFactory.Create, + MetaMemberDataSourcesFactory.Create + }; + + public static IDataSourceSet CreateFor(IObjectMappingData rootMappingData) + { + var rootDataSourceFactory = _mappingRootDataSourceFactories + .First(rootMappingData, (rmd, mef) => mef.IsFor(rmd)); + + var rootDataSource = rootDataSourceFactory.CreateFor(rootMappingData); + + return DataSourceSet.For(rootDataSource, rootMappingData.MapperData); + } + + public static IDataSourceSet CreateFor(DataSourceFindContext findContext) + { + var validDataSources = EnumerateDataSources(findContext).ToArray(); + + return DataSourceSet.For(validDataSources, findContext.MemberMapperData); + } + + private static IEnumerable<IDataSource> EnumerateDataSources(DataSourceFindContext context) + { + foreach (var factory in _childDataSourceFactories) + { + foreach (var dataSource in factory.Invoke(context)) + { + if (!dataSource.IsValid) + { + continue; + } + + yield return dataSource; + + if (!dataSource.IsConditional) + { + yield break; + } + + ++context.DataSourceIndex; + } + + if (context.StopFind) + { + yield break; + } + } + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/DataSourcesFactory.cs b/AgileMapper/DataSources/Factories/DataSourcesFactory.cs new file mode 100644 index 000000000..feeb31788 --- /dev/null +++ b/AgileMapper/DataSources/Factories/DataSourcesFactory.cs @@ -0,0 +1,6 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using System.Collections.Generic; + + internal delegate IEnumerable<IDataSource> DataSourcesFactory(DataSourceFindContext context); +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/DefaultValueFallbackDataSourceFactory.cs b/AgileMapper/DataSources/Factories/DefaultValueFallbackDataSourceFactory.cs new file mode 100644 index 000000000..24628c86e --- /dev/null +++ b/AgileMapper/DataSources/Factories/DefaultValueFallbackDataSourceFactory.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using Members; + + internal static class DefaultValueFallbackDataSourceFactory + { + public static IDataSource Create(IMemberMapperData mapperData) + => new DefaultValueFallbackDataSource(mapperData); + + private class DefaultValueFallbackDataSource : DataSourceBase + { + public DefaultValueFallbackDataSource(IMemberMapperData mapperData) + : base(mapperData.SourceMember, mapperData.GetTargetMemberDefault()) + { + } + + public override bool IsFallback => true; + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/DerivedComplexTypeDataSourcesFactory.cs b/AgileMapper/DataSources/Factories/DerivedComplexTypeDataSourcesFactory.cs new file mode 100644 index 000000000..971aa7943 --- /dev/null +++ b/AgileMapper/DataSources/Factories/DerivedComplexTypeDataSourcesFactory.cs @@ -0,0 +1,560 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using System; + using System.Collections.Generic; + using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; + using static Microsoft.Scripting.Ast.Expression; +#else + using System.Linq.Expressions; + using static System.Linq.Expressions.Expression; +#endif + using Configuration; + using Extensions; + using Extensions.Internal; + using Members; + using NetStandardPolyfills; + using ObjectPopulation; + using static Constants; + using static TypeComparer; + + internal static class DerivedComplexTypeDataSourcesFactory + { + public static IList<IDataSource> CreateFor(IObjectMappingData declaredTypeMappingData) + { + var declaredTypeMapperData = declaredTypeMappingData.MapperData; + + if (DoNotMapDerivedTypes(declaredTypeMapperData)) + { + return Enumerable<IDataSource>.EmptyArray; + } + + var derivedSourceTypes = GetDerivedSourceTypesIfNecessary(declaredTypeMapperData); + var hasDerivedSourceTypes = derivedSourceTypes.Any(); + var hasNoDerivedSourceTypes = !hasDerivedSourceTypes; + + var derivedTargetTypes = GetDerivedTargetTypesIfNecessary(declaredTypeMapperData); + var hasDerivedTargetTypes = derivedTargetTypes.Any(); + + var derivedTypePairs = GetTypePairsFor(declaredTypeMapperData, declaredTypeMapperData); + var hasDerivedTypePairs = derivedTypePairs.Any(); + + if (hasNoDerivedSourceTypes && !hasDerivedTargetTypes && !hasDerivedTypePairs) + { + return Enumerable<IDataSource>.EmptyArray; + } + + var derivedTypeDataSources = new List<IDataSource>(); + + if (hasDerivedTypePairs) + { + AddDeclaredSourceTypeDataSources( + derivedTypePairs, + declaredTypeMappingData, + derivedTypeDataSources); + + if (hasNoDerivedSourceTypes && !derivedTypeDataSources.Last().IsConditional) + { + return derivedTypeDataSources; + } + } + + if (hasDerivedSourceTypes) + { + AddDerivedSourceTypeDataSources( + derivedSourceTypes, + declaredTypeMappingData, + derivedTypeDataSources); + } + + if (hasDerivedTargetTypes) + { + AddDerivedTargetTypeDataSources( + declaredTypeMappingData, + derivedTargetTypes, + derivedTypeDataSources); + } + + return derivedTypeDataSources; + } + + private static bool DoNotMapDerivedTypes(IMemberMapperData mapperData) + { + if (mapperData.Context.IsForDerivedType) + { + return !mapperData.TargetType.IsInterface(); + } + + return mapperData.HasSameSourceAsParent(); + } + + private static ICollection<Type> GetDerivedSourceTypesIfNecessary(IMemberMapperData mapperData) + { + return mapperData.RuleSet.Settings.CheckDerivedSourceTypes + ? mapperData.GetDerivedSourceTypes() + : EmptyTypeArray; + } + + private static ICollection<Type> GetDerivedTargetTypesIfNecessary(IMemberMapperData mapperData) + { + return mapperData.TargetCouldBePopulated() + ? mapperData.GetDerivedTargetTypes() + : EmptyTypeArray; + } + + private static ICollection<DerivedTypePair> GetTypePairsFor(IBasicMapperData pairTestMapperData, IMemberMapperData mapperData) + { + var derivedTypePairs = mapperData.MapperContext.UserConfigurations + .DerivedTypes + .GetDerivedTypePairsFor(pairTestMapperData, mapperData.MapperContext); + + return derivedTypePairs; + } + + private static void AddDeclaredSourceTypeDataSources( + IEnumerable<DerivedTypePair> derivedTypePairs, + IObjectMappingData declaredTypeMappingData, + ICollection<IDataSource> derivedTypeDataSources) + { + var declaredTypeMapperData = declaredTypeMappingData.MapperData; + + derivedTypePairs = derivedTypePairs + .OrderBy(tp => tp.DerivedSourceType, MostToLeastDerived); + + foreach (var derivedTypePair in derivedTypePairs) + { + var condition = GetTypePairCondition(derivedTypePair, declaredTypeMapperData); + + var sourceValue = GetDerivedTypeSourceValue( + derivedTypePair, + declaredTypeMappingData, + out var sourceValueCondition); + + var derivedTypeMapping = DerivedMappingFactory.GetDerivedTypeMapping( + declaredTypeMappingData, + sourceValue, + derivedTypePair.DerivedTargetType, + out var derivedTypeMappingData); + + if (sourceValueCondition != null) + { + derivedTypeMapping = derivedTypeMapping.ToIfFalseDefaultCondition(sourceValueCondition); + } + + var returnMappingResult = GetReturnMappingResultExpression(declaredTypeMapperData, derivedTypeMapping); + + var derivedTypeMappingDataSource = new DerivedComplexTypeDataSource( + derivedTypeMappingData.MapperData.SourceMember, + condition, + returnMappingResult); + + derivedTypeDataSources.Add(derivedTypeMappingDataSource); + + if (!derivedTypeMappingDataSource.IsConditional) + { + return; + } + } + } + + private static Expression GetTypePairCondition(DerivedTypePair derivedTypePair, IMemberMapperData declaredTypeMapperData) + { + var condition = declaredTypeMapperData.GetTargetValidCheckOrNull(derivedTypePair.DerivedTargetType); + + if (!derivedTypePair.HasConfiguredCondition) + { + return condition; + } + + var pairCondition = derivedTypePair.GetConditionOrNull(declaredTypeMapperData); + + return (condition != null) ? AndAlso(pairCondition, condition) : pairCondition; + } + + private static Expression GetDerivedTypeSourceValue( + DerivedTypePair derivedTypePair, + IObjectMappingData declaredTypeMappingData, + out Expression sourceValueCondition) + { + if (!derivedTypePair.IsImplementationPairing) + { + sourceValueCondition = null; + return declaredTypeMappingData.MapperData.SourceObject; + } + + var implementationMappingData = declaredTypeMappingData + .WithTypes(derivedTypePair.DerivedSourceType, derivedTypePair.DerivedTargetType); + + if (implementationMappingData.IsTargetConstructable()) + { + sourceValueCondition = null; + return declaredTypeMappingData.MapperData.SourceObject; + } + + // Derived Type is an implementation Type for an unconstructable target Type, + // and is itself unconstructable; only way we get here is if a ToTarget data + // source has been configured: + var toTargetDataSource = implementationMappingData + .GetToTargetDataSourceOrNullForTargetType(); + + sourceValueCondition = toTargetDataSource.IsConditional + ? toTargetDataSource.Condition.Replace( + implementationMappingData.MapperData.SourceObject, + declaredTypeMappingData.MapperData.SourceObject, + ExpressionEvaluation.Equivalator) + : null; + + return toTargetDataSource.Value.Replace( + implementationMappingData.MapperData.SourceObject, + declaredTypeMappingData.MapperData.SourceObject, + ExpressionEvaluation.Equivalator); + } + + private static void AddDerivedSourceTypeDataSources( + IEnumerable<Type> derivedSourceTypes, + IObjectMappingData declaredTypeMappingData, + IList<IDataSource> derivedTypeDataSources) + { + var declaredTypeMapperData = declaredTypeMappingData.MapperData; + var insertionOffset = derivedTypeDataSources.Count; + + var orderedDerivedSourceTypes = derivedSourceTypes + .OrderBy(t => t, MostToLeastDerived); + + foreach (var derivedSourceType in orderedDerivedSourceTypes) + { + var derivedSourceCheck = new DerivedSourceTypeCheck(derivedSourceType); + var targetType = declaredTypeMapperData.TargetType.GetRuntimeTargetType(derivedSourceType); + + var outerCondition = derivedSourceCheck.TypeCheck; + outerCondition = AppendTargetValidCheckIfAppropriate(outerCondition, targetType, declaredTypeMapperData); + + var derivedTypePairs = GetTypePairsFor(derivedSourceType, targetType, declaredTypeMapperData); + + IDataSource sourceVariableIsDerivedTypeDataSource; + + if (derivedTypePairs.None()) + { + sourceVariableIsDerivedTypeDataSource = GetReturnMappingResultDataSource( + declaredTypeMappingData, + outerCondition, + derivedSourceCheck, + targetType); + + derivedTypeDataSources.Insert(sourceVariableIsDerivedTypeDataSource, insertionOffset); + continue; + } + + var hasUnconditionalDerivedTargetTypeMapping = HasUnconditionalDerivedTargetTypeMapping( + derivedTypePairs, + declaredTypeMapperData, + out var unconditionalDerivedTargetType, + out var groupedTypePairs); + + if (hasUnconditionalDerivedTargetTypeMapping) + { + sourceVariableIsDerivedTypeDataSource = GetReturnMappingResultDataSource( + declaredTypeMappingData, + outerCondition, + derivedSourceCheck, + unconditionalDerivedTargetType); + + derivedTypeDataSources.Insert(sourceVariableIsDerivedTypeDataSource, insertionOffset); + continue; + } + + sourceVariableIsDerivedTypeDataSource = GetMapFromConditionOrDefaultDataSource( + declaredTypeMappingData, + outerCondition, + derivedSourceCheck, + groupedTypePairs, + targetType); + + derivedTypeDataSources.Insert(sourceVariableIsDerivedTypeDataSource, insertionOffset); + } + } + + private static IDataSource GetMapFromConditionOrDefaultDataSource( + IObjectMappingData declaredTypeMappingData, + Expression condition, + DerivedSourceTypeCheck derivedSourceCheck, + IEnumerable<TypePairGroup> typePairGroups, + Type targetType) + { + var declaredTypeMapperData = declaredTypeMappingData.MapperData; + var typePairDataSources = new List<IDataSource>(); + + Expression derivedTypeMapping; + IObjectMappingData derivedTypeMappingData; + + foreach (var typePairGroup in typePairGroups) + { + var typePairsCondition = + declaredTypeMapperData.GetTypePairsCondition(typePairGroup.TypePairs) ?? + declaredTypeMapperData.GetTargetValidCheckOrNull(typePairGroup.DerivedTargetType); + + derivedTypeMapping = GetReturnMappingResultExpression( + declaredTypeMappingData, + derivedSourceCheck.TypedVariable, + typePairGroup.DerivedTargetType, + out derivedTypeMappingData); + + var typePairDataSource = new DerivedComplexTypeDataSource( + derivedTypeMappingData.MapperData.SourceMember, + typePairsCondition, + derivedTypeMapping); + + typePairDataSources.Add(typePairDataSource); + } + + var derivedTargetTypeDataSources = DataSourceSet.For( + typePairDataSources, + declaredTypeMapperData, + ValueExpressionBuilders.ValueSequence); + + derivedTypeMapping = GetReturnMappingResultExpression( + declaredTypeMappingData, + derivedSourceCheck.TypedVariable, + targetType, + out derivedTypeMappingData); + + var derivedTypeMappings = Block( + derivedTargetTypeDataSources.BuildValue(), + derivedTypeMapping); + + return new DerivedComplexTypeDataSource( + derivedTypeMappingData.MapperData.SourceMember, + derivedSourceCheck, + condition, + derivedTypeMappings, + declaredTypeMapperData); + } + + private static Expression GetTypePairsCondition( + this IMemberMapperData mapperData, + IEnumerable<DerivedTypePair> derivedTypePairs) + { + var conditionalPairs = derivedTypePairs + .Filter(pair => pair.HasConfiguredCondition) + .ToArray(); + + var pairConditions = conditionalPairs.Chain( + firstPair => firstPair.GetConditionOrNull(mapperData), + (conditionSoFar, pair) => OrElse( + conditionSoFar, + pair.GetConditionOrNull(mapperData))); + + return pairConditions; + } + + private static Expression AppendTargetValidCheckIfAppropriate( + Expression condition, + Type targetType, + IMemberMapperData mapperData) + { + if (targetType == mapperData.TargetType) + { + return condition; + } + + var targetIsValid = mapperData.GetTargetValidCheckOrNull(targetType); + + if (targetIsValid == null) + { + return condition; + } + + condition = AndAlso(condition, targetIsValid); + + return condition; + } + + private static bool HasUnconditionalDerivedTargetTypeMapping( + IEnumerable<DerivedTypePair> derivedTypePairs, + IMemberMapperData declaredTypeMapperData, + out Type unconditionalDerivedTargetType, + out TypePairGroup[] groupedTypePairs) + { + groupedTypePairs = derivedTypePairs + .GroupBy(tp => tp.DerivedTargetType) + .Project(group => new TypePairGroup(group)) + .OrderBy(tp => tp.DerivedTargetType, MostToLeastDerived) + .ToArray(); + + var unconditionalTypePairs = groupedTypePairs + .Filter(tpg => tpg.TypePairs.None(tp => tp.HasConfiguredCondition)); + + foreach (var unconditionalTypePair in unconditionalTypePairs) + { + var typePairsCondition = declaredTypeMapperData + .GetTargetValidCheckOrNull(unconditionalTypePair.DerivedTargetType); + + if (typePairsCondition == null) + { + unconditionalDerivedTargetType = unconditionalTypePair.DerivedTargetType; + return true; + } + } + + unconditionalDerivedTargetType = null; + return false; + } + + private static ICollection<DerivedTypePair> GetTypePairsFor( + Type derivedSourceType, + Type targetType, + IMemberMapperData mapperData) + { + var pairTestMapperData = new BasicMapperData( + mapperData.RuleSet, + derivedSourceType, + targetType, + mapperData.TargetMember.WithType(targetType), + mapperData.Parent); + + return GetTypePairsFor(pairTestMapperData, mapperData); + } + + private static void AddDerivedTargetTypeDataSources( + IObjectMappingData declaredTypeMappingData, + IEnumerable<Type> derivedTargetTypes, + ICollection<IDataSource> derivedTypeDataSources) + { + var declaredTypeMapperData = declaredTypeMappingData.MapperData; + + derivedTargetTypes = derivedTargetTypes.OrderBy(t => t, MostToLeastDerived); + + foreach (var derivedTargetType in derivedTargetTypes) + { + var targetTypeCondition = declaredTypeMapperData.GetTargetIsDerivedTypeCheck(derivedTargetType); + + var derivedTypeMapping = GetReturnMappingResultExpression( + declaredTypeMappingData, + declaredTypeMapperData.SourceObject, + derivedTargetType, + out var derivedTypeMappingData); + + if (derivedTypeMapping == EmptyExpression) + { + continue; + } + + var derivedTargetTypeDataSource = new DerivedComplexTypeDataSource( + derivedTypeMappingData.MapperData.SourceMember, + targetTypeCondition, + derivedTypeMapping); + + derivedTypeDataSources.Add(derivedTargetTypeDataSource); + } + } + + private static IDataSource GetReturnMappingResultDataSource( + IObjectMappingData declaredTypeMappingData, + Expression condition, + DerivedSourceTypeCheck derivedSourceCheck, + Type targetType) + { + var derivedTypeMapping = GetReturnMappingResultExpression( + declaredTypeMappingData, + derivedSourceCheck.TypedVariable, + targetType, + out var derivedTypeMappingData); + + return new DerivedComplexTypeDataSource( + derivedTypeMappingData.MapperData.SourceMember, + derivedSourceCheck, + condition, + derivedTypeMapping, + declaredTypeMappingData.MapperData); + } + + private static Expression GetReturnMappingResultExpression( + IObjectMappingData declaredTypeMappingData, + Expression sourceValue, + Type targetType, + out IObjectMappingData derivedTypeMappingData) + { + var mapping = DerivedMappingFactory.GetDerivedTypeMapping( + declaredTypeMappingData, + sourceValue, + targetType, + out derivedTypeMappingData); + + return (mapping != EmptyExpression) + ? GetReturnMappingResultExpression(declaredTypeMappingData.MapperData, mapping) + : mapping; + } + + private static Expression GetReturnMappingResultExpression(ObjectMapperData mapperData, Expression mapping) + => Return(mapperData.ReturnLabelTarget, mapping, mapperData.TargetType); + + private static Expression GetTargetValidCheckOrNull(this IMemberMapperData mapperData, Type targetType) + { + if (!mapperData.TargetMember.IsReadable || mapperData.TargetIsDefinitelyUnpopulated()) + { + return null; + } + + var targetIsOfDerivedType = mapperData.GetTargetIsDerivedTypeCheck(targetType); + + if (mapperData.TargetIsDefinitelyPopulated()) + { + return targetIsOfDerivedType; + } + + var targetIsNull = mapperData.TargetObject.GetIsDefaultComparison(); + var targetIsValid = OrElse(targetIsNull, targetIsOfDerivedType); + + return targetIsValid; + } + + private static Expression GetTargetIsDerivedTypeCheck(this IMemberMapperData mapperData, Type targetType) + => TypeIs(mapperData.TargetObject, targetType); + + private class DerivedComplexTypeDataSource : DataSourceBase + { + private readonly Expression _typedVariableAssignment; + + public DerivedComplexTypeDataSource( + IQualifiedMember sourceMember, + Expression condition, + Expression value) + : base(sourceMember, Enumerable<ParameterExpression>.EmptyArray, value, condition) + { + } + + public DerivedComplexTypeDataSource( + IQualifiedMember sourceMember, + DerivedSourceTypeCheck derivedSourceCheck, + Expression condition, + Expression value, + IMemberMapperData declaredTypeMapperData) + : base(sourceMember, new[] { derivedSourceCheck.TypedVariable }, value, condition) + { + _typedVariableAssignment = derivedSourceCheck + .GetTypedVariableAssignment(declaredTypeMapperData); + } + + public override Expression AddSourceCondition(Expression value) + { + return (_typedVariableAssignment != null) + ? Block(_typedVariableAssignment, value) + : base.AddSourceCondition(value); + } + } + + private class TypePairGroup + { + public TypePairGroup(IGrouping<Type, DerivedTypePair> typePairGroup) + { + DerivedTargetType = typePairGroup.Key; + TypePairs = typePairGroup.ToArray(); + } + + public Type DerivedTargetType { get; } + + public IList<DerivedTypePair> TypePairs { get; } + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/DictionaryDataSourceFactory.cs b/AgileMapper/DataSources/Factories/DictionaryDataSourceFactory.cs similarity index 97% rename from AgileMapper/DataSources/DictionaryDataSourceFactory.cs rename to AgileMapper/DataSources/Factories/DictionaryDataSourceFactory.cs index 3d3a99d8e..401e819f5 100644 --- a/AgileMapper/DataSources/DictionaryDataSourceFactory.cs +++ b/AgileMapper/DataSources/Factories/DictionaryDataSourceFactory.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.DataSources +namespace AgileObjects.AgileMapper.DataSources.Factories { using System; using System.Collections.Generic; diff --git a/AgileMapper/DataSources/Factories/ExistingOrDefaultValueFallbackDataSourceFactory.cs b/AgileMapper/DataSources/Factories/ExistingOrDefaultValueFallbackDataSourceFactory.cs new file mode 100644 index 000000000..42906afec --- /dev/null +++ b/AgileMapper/DataSources/Factories/ExistingOrDefaultValueFallbackDataSourceFactory.cs @@ -0,0 +1,60 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Members; + using Members.Dictionaries; + + internal static class ExistingOrDefaultValueFallbackDataSourceFactory + { + public static IDataSource Create(IMemberMapperData mapperData) + => new ExistingValueOrDefaultFallbackDataSource(mapperData); + + private class ExistingValueOrDefaultFallbackDataSource : DataSourceBase + { + public ExistingValueOrDefaultFallbackDataSource(IMemberMapperData mapperData) + : base(mapperData.SourceMember, GetValue(mapperData), mapperData) + { + } + + private static Expression GetValue(IMemberMapperData mapperData) + { + if (mapperData.TargetMember.IsEnumerable) + { + return FallbackToCollection(mapperData) + ? mapperData.GetFallbackCollectionValue() + : mapperData.GetTargetMemberDefault(); + } + + if (mapperData.TargetMember.IsReadable && !mapperData.UseMemberInitialisations()) + { + return mapperData.TargetMember.IsSimple + ? Constants.EmptyExpression + : mapperData.GetTargetMemberAccess(); + } + + return mapperData.GetTargetMemberDefault(); + } + + private static bool FallbackToCollection(IBasicMapperData mapperData) + { + if (mapperData.TargetMember.IsDictionary) + { + return true; + } + + if (!(mapperData.TargetMember is DictionaryTargetMember dictionaryTargetMember)) + { + return true; + } + + return dictionaryTargetMember.HasEnumerableEntries; + } + + public override bool IsFallback => true; + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/FallbackDataSourceFactory.cs b/AgileMapper/DataSources/Factories/FallbackDataSourceFactory.cs new file mode 100644 index 000000000..3834d7d1e --- /dev/null +++ b/AgileMapper/DataSources/Factories/FallbackDataSourceFactory.cs @@ -0,0 +1,6 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using Members; + + internal delegate IDataSource FallbackDataSourceFactory(IMemberMapperData mapperData); +} \ No newline at end of file diff --git a/AgileMapper/DataSources/IMaptimeDataSourceFactory.cs b/AgileMapper/DataSources/Factories/IMaptimeDataSourceFactory.cs similarity index 81% rename from AgileMapper/DataSources/IMaptimeDataSourceFactory.cs rename to AgileMapper/DataSources/Factories/IMaptimeDataSourceFactory.cs index 0834e3fa8..25920ac35 100644 --- a/AgileMapper/DataSources/IMaptimeDataSourceFactory.cs +++ b/AgileMapper/DataSources/Factories/IMaptimeDataSourceFactory.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.DataSources +namespace AgileObjects.AgileMapper.DataSources.Factories { using System.Collections.Generic; using Members; diff --git a/AgileMapper/DataSources/Factories/MappingRoot/ComplexTypeMappingRootDataSourceFactory.cs b/AgileMapper/DataSources/Factories/MappingRoot/ComplexTypeMappingRootDataSourceFactory.cs new file mode 100644 index 000000000..83536d01b --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/ComplexTypeMappingRootDataSourceFactory.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using ObjectPopulation; + + internal class ComplexTypeMappingRootDataSourceFactory : IMappingRootDataSourceFactory + { + public bool IsFor(IObjectMappingData mappingData) => true; + + public IDataSource CreateFor(IObjectMappingData mappingData) + => ComplexTypeDataSource.Create(mappingData); + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/MappingRoot/DictionaryMappingRootDataSourceFactory.cs b/AgileMapper/DataSources/Factories/MappingRoot/DictionaryMappingRootDataSourceFactory.cs new file mode 100644 index 000000000..72bff05a0 --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/DictionaryMappingRootDataSourceFactory.cs @@ -0,0 +1,38 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using Members.Dictionaries; + using ObjectPopulation; + + internal class DictionaryMappingRootDataSourceFactory : MappingRootDataSourceFactoryBase + { + public DictionaryMappingRootDataSourceFactory() + : base(new DictionaryMappingExpressionFactory()) + { + } + + public override bool IsFor(IObjectMappingData mappingData) + { + if (mappingData.MapperData.TargetMember.IsDictionary) + { + return true; + } + + if (mappingData.IsRoot) + { + return false; + } + + if (!(mappingData.MapperData.TargetMember is DictionaryTargetMember dictionaryMember)) + { + return false; + } + + if (dictionaryMember.HasSimpleEntries) + { + return true; + } + + return dictionaryMember.HasObjectEntries && !mappingData.IsStandalone(); + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/MappingRoot/EnumMappingRootDataSourceFactory.cs b/AgileMapper/DataSources/Factories/MappingRoot/EnumMappingRootDataSourceFactory.cs new file mode 100644 index 000000000..a48f25231 --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/EnumMappingRootDataSourceFactory.cs @@ -0,0 +1,17 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using NetStandardPolyfills; + using ObjectPopulation; + using ReadableExpressions.Extensions; + + internal class EnumMappingRootDataSourceFactory : MappingRootDataSourceFactoryBase + { + public EnumMappingRootDataSourceFactory() + : base(new EnumMappingExpressionFactory()) + { + } + + public override bool IsFor(IObjectMappingData mappingData) + => mappingData.MapperData.TargetType.GetNonNullableType().IsEnum(); + } +} diff --git a/AgileMapper/DataSources/Factories/MappingRoot/EnumerableMappingRootDataSourceFactory.cs b/AgileMapper/DataSources/Factories/MappingRoot/EnumerableMappingRootDataSourceFactory.cs new file mode 100644 index 000000000..d775bcbbb --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/EnumerableMappingRootDataSourceFactory.cs @@ -0,0 +1,16 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using ObjectPopulation; + using ObjectPopulation.Enumerables; + + internal class EnumerableMappingRootDataSourceFactory : MappingRootDataSourceFactoryBase + { + public EnumerableMappingRootDataSourceFactory() + : base(new EnumerableMappingExpressionFactory()) + { + } + + public override bool IsFor(IObjectMappingData mappingData) + => mappingData.MapperData.TargetMember.IsEnumerable; + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/MappingRoot/IMappingRootDataSourceFactory.cs b/AgileMapper/DataSources/Factories/MappingRoot/IMappingRootDataSourceFactory.cs new file mode 100644 index 000000000..7da3cf766 --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/IMappingRootDataSourceFactory.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using ObjectPopulation; + + internal interface IMappingRootDataSourceFactory + { + bool IsFor(IObjectMappingData mappingData); + + IDataSource CreateFor(IObjectMappingData mappingData); + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/MappingRoot/MappingRootDataSourceFactoryBase.cs b/AgileMapper/DataSources/Factories/MappingRoot/MappingRootDataSourceFactoryBase.cs new file mode 100644 index 000000000..d4ab60814 --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/MappingRootDataSourceFactoryBase.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using ObjectPopulation; + + internal abstract class MappingRootDataSourceFactoryBase : IMappingRootDataSourceFactory + { + private readonly MappingExpressionFactoryBase _mappingExpressionFactory; + + protected MappingRootDataSourceFactoryBase(MappingExpressionFactoryBase mappingExpressionFactory) + { + _mappingExpressionFactory = mappingExpressionFactory; + } + + public abstract bool IsFor(IObjectMappingData mappingData); + + public IDataSource CreateFor(IObjectMappingData mappingData) + { + var mappingExpression = _mappingExpressionFactory.Create(mappingData); + + return new AdHocDataSource( + mappingData.MapperData.SourceMember, + mappingExpression); + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Factories/MappingRoot/QueryProjectionRootDataSourceFactory.cs b/AgileMapper/DataSources/Factories/MappingRoot/QueryProjectionRootDataSourceFactory.cs new file mode 100644 index 000000000..ee41f5e55 --- /dev/null +++ b/AgileMapper/DataSources/Factories/MappingRoot/QueryProjectionRootDataSourceFactory.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories.MappingRoot +{ + using Extensions.Internal; + using ObjectPopulation; + using Queryables; + + internal class QueryProjectionRootDataSourceFactory : MappingRootDataSourceFactoryBase + { + public QueryProjectionRootDataSourceFactory() + : base(new QueryProjectionExpressionFactory()) + { + } + + public override bool IsFor(IObjectMappingData mappingData) + { + var mapperData = mappingData.MapperData; + + return mapperData.IsRoot && + mapperData.TargetMember.IsEnumerable && + (mappingData.MappingContext.RuleSet.Name == Constants.Project) && + mapperData.SourceType.IsQueryable(); + } + } +} diff --git a/AgileMapper/DataSources/Finders/MaptimeDataSourceFinder.cs b/AgileMapper/DataSources/Factories/MaptimeDataSourcesFactory.cs similarity index 80% rename from AgileMapper/DataSources/Finders/MaptimeDataSourceFinder.cs rename to AgileMapper/DataSources/Factories/MaptimeDataSourcesFactory.cs index fb0a0c944..ca17d245d 100644 --- a/AgileMapper/DataSources/Finders/MaptimeDataSourceFinder.cs +++ b/AgileMapper/DataSources/Factories/MaptimeDataSourcesFactory.cs @@ -1,17 +1,16 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders +namespace AgileObjects.AgileMapper.DataSources.Factories { using System.Collections.Generic; using Extensions.Internal; - - internal struct MaptimeDataSourceFinder : IDataSourceFinder + internal static class MaptimeDataSourcesFactory { private static readonly IMaptimeDataSourceFactory[] _mapTimeDataSourceFactories = { default(DictionaryDataSourceFactory) }; - public IEnumerable<IDataSource> FindFor(DataSourceFindContext context) + public static IEnumerable<IDataSource> Create(DataSourceFindContext context) { if (!UseMaptimeDataSources(context, out var maptimeDataSources)) { @@ -40,7 +39,7 @@ private static bool UseMaptimeDataSources( out IEnumerable<IDataSource> maptimeDataSources) { var applicableFactory = _mapTimeDataSourceFactories - .FirstOrDefault(factory => factory.IsFor(context.MapperData)); + .FirstOrDefault(context.MemberMapperData, (md, factory) => factory.IsFor(md)); if (applicableFactory == null) { @@ -48,7 +47,7 @@ private static bool UseMaptimeDataSources( return false; } - maptimeDataSources = applicableFactory.Create(context.ChildMappingData); + maptimeDataSources = applicableFactory.Create(context.MemberMappingData); return true; } } diff --git a/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs b/AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs similarity index 89% rename from AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs rename to AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs index 1f7cb6a0b..f24195f37 100644 --- a/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs +++ b/AgileMapper/DataSources/Factories/MetaMemberDataSourcesFactory.cs @@ -1,8 +1,13 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders +namespace AgileObjects.AgileMapper.DataSources.Factories { using System; using System.Collections.Generic; using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using Extensions; using Extensions.Internal; using Members; @@ -11,16 +16,10 @@ using ObjectPopulation.Enumerables; using ReadableExpressions.Extensions; using TypeConversion; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - using static System.StringComparison; - internal struct MetaMemberDataSourceFinder : IDataSourceFinder + internal static class MetaMemberDataSourcesFactory { - public IEnumerable<IDataSource> FindFor(DataSourceFindContext context) + public static IEnumerable<IDataSource> Create(DataSourceFindContext context) { if (TryGetMetaMemberNameParts(context, out var memberNameParts) && TryGetMetaMember(memberNameParts, context, out var metaMember)) @@ -42,7 +41,7 @@ private static bool TryGetMetaMemberNameParts( { memberNameParts = default(IList<string>); - var targetMemberName = context.MapperData.TargetMember.Name; + var targetMemberName = context.TargetMember.Name; var previousNamePartEndIndex = targetMemberName.Length; var currentMemberName = string.Empty; var noMetaMemberAdded = true; @@ -85,7 +84,7 @@ private static bool TryGetMetaMemberNameParts( default: currentMemberName = memberNamePart + currentMemberName; - if (currentMemberName.StartsWith(NumberOfMetaMemberPart.Name, Ordinal)) + if (currentMemberName.StartsWith(NumberOfMetaMemberPart.Name, StringComparison.Ordinal)) { currentMemberName = currentMemberName.Substring(NumberOfMetaMemberPart.Name.Length); memberNameParts.Add(currentMemberName); @@ -141,7 +140,7 @@ private static bool TryGetMetaMember( var currentMemberPart = metaMember = default(MetaMemberPartBase); Func<IQualifiedMember, QualifiedMember, IObjectMappingData, DataSourceFindContext, IObjectMappingData> currentMappingDataFactory = - (sm, tm, md, c) => c.ChildMappingData.Parent; + (sm, tm, md, c) => c.MemberMappingData.Parent; for (var i = memberNameParts.Count - 1; i >= 0; --i) { @@ -150,7 +149,7 @@ private static bool TryGetMetaMember( switch (memberNamePart) { case HasMetaMemberPart.Name: - if (HasMetaMemberPart.TryCreateFor(context.MapperData, ref currentMemberPart)) + if (HasMetaMemberPart.TryCreateFor(context.MemberMapperData, ref currentMemberPart)) { break; } @@ -158,15 +157,15 @@ private static bool TryGetMetaMember( return false; case FirstMetaMemberPart.Name: - currentMemberPart = new FirstMetaMemberPart(context.MapperData); + currentMemberPart = new FirstMetaMemberPart(context.MemberMapperData); break; case LastMetaMemberPart.Name: - currentMemberPart = new LastMetaMemberPart(context.MapperData); + currentMemberPart = new LastMetaMemberPart(context.MemberMapperData); break; case CountMetaMemberPart.Name: - if (CountMetaMemberPart.TryCreateFor(context.MapperData, ref currentMemberPart)) + if (CountMetaMemberPart.TryCreateFor(context.MemberMapperData, ref currentMemberPart)) { break; } @@ -174,7 +173,7 @@ private static bool TryGetMetaMember( return false; case NumberOfMetaMemberPart.Name: - if (NumberOfMetaMemberPart.TryCreateFor(context.MapperData, ref currentMemberPart)) + if (NumberOfMetaMemberPart.TryCreateFor(context.MemberMapperData, ref currentMemberPart)) { break; } @@ -193,14 +192,14 @@ private static bool TryGetMetaMember( var matchingTargetMember = GlobalContext.Instance .MemberCache .GetTargetMembers(currentMapperData.TargetType) - .FirstOrDefault(m => m.Name == memberNamePart); + .FirstOrDefault(memberNamePart, (mnp, m) => m.Name == mnp); if (matchingTargetMember == null) { matchingTargetMember = GlobalContext.Instance .MemberCache .GetSourceMembers(currentMapperData.SourceType) - .FirstOrDefault(m => m.Name == memberNamePart); + .FirstOrDefault(memberNamePart, (mnp, m) => m.Name == mnp); if (matchingTargetMember == null) { @@ -214,16 +213,17 @@ private static bool TryGetMetaMember( var memberMappingData = currentMappingData.GetChildMappingData(childMemberMapperData); - currentSourceMember = SourceMemberMatcher.GetMatchFor( + var currentSourceMemberMatch = SourceMemberMatcher.GetMatchFor( memberMappingData, - out _, searchParentContexts: false); - if (currentSourceMember == null) + if (!currentSourceMemberMatch.IsUseable) { return false; } + currentSourceMember = currentSourceMemberMatch.SourceMember; + currentMemberPart = new SourceMemberMetaMemberPart( currentSourceMember, currentMapperData, @@ -235,7 +235,7 @@ private static bool TryGetMetaMember( return sm.IsEnumerable ? ObjectMappingDataFactory.ForElement(mappingData) - : ObjectMappingDataFactory.ForChild(sm, tm, 0, md); + : mappingData; }; break; @@ -310,8 +310,10 @@ protected static Expression GetLinqMethodCall( Expression enumerable, EnumerableTypeHelper helper) { + var enumerableType = helper.IsQueryableInterface ? typeof(Queryable) : typeof(Enumerable); + return Expression.Call( - typeof(Enumerable) + enumerableType .GetPublicStaticMethod(methodName, parameterCount: 1) .MakeGenericMethod(helper.ElementType), enumerable); @@ -357,37 +359,18 @@ public override Expression GetAccess(Expression parentInstance) private Expression GetHasCheck(Expression queriedMemberAccess) { - if (QueriedMember.IsEnumerable) - { - return GetHasEnumerableCheck(queriedMemberAccess); - } - - var queriedMemberNotDefault = queriedMemberAccess.GetIsNotDefaultComparison(); - - if (QueriedMember.IsSimple) - { - return queriedMemberNotDefault; - } - - return queriedMemberNotDefault; + return QueriedMember.IsEnumerable + ? GetHasEnumerableCheck(queriedMemberAccess) + : queriedMemberAccess.GetIsNotDefaultComparison(); } private static Expression GetHasEnumerableCheck(Expression enumerableAccess) { var helper = new EnumerableTypeHelper(enumerableAccess.Type); - return helper.IsEnumerableInterface + return helper.IsEnumerableOrQueryable ? GetLinqMethodCall(nameof(Enumerable.Any), enumerableAccess, helper) - : GetEnumerableCountCheck(enumerableAccess, helper); - } - - public static Expression GetEnumerableCountCheck(Expression enumerableAccess, EnumerableTypeHelper helper) - { - var enumerableCount = helper.GetCountFor(enumerableAccess); - var zero = ToNumericConverter<int>.Zero.GetConversionTo(enumerableCount.Type); - var countGreaterThanZero = Expression.GreaterThan(enumerableCount, zero); - - return countGreaterThanZero; + : helper.GetNonZeroCountCheck(enumerableAccess); } } @@ -454,7 +437,7 @@ public override Expression GetAccess(Expression parentInstance) private Expression GetCondition(Expression enumerableAccess, EnumerableTypeHelper helper) { - var enumerableCheck = HasMetaMemberPart.GetEnumerableCountCheck(enumerableAccess, helper); + var enumerableCheck = helper.GetNonZeroCountCheck(enumerableAccess); if (MapperData.RuleSet.Settings.GuardAccessTo(enumerableAccess)) { @@ -470,14 +453,12 @@ private Expression GetCondition(Expression enumerableAccess, EnumerableTypeHelpe private Expression GetOrderedEnumerableAccess(Expression enumerableAccess, EnumerableTypeHelper helper) { - var elementType = _sourceMember.Type; - - if (!elementType.IsComplex()) + if (!_sourceMember.Type.IsComplex()) { return GetLinqMethodCall(LinqSelectionMethodName, enumerableAccess, helper); } - var orderMember = MapperData.GetOrderMember(elementType); + var orderMember = MapperData.GetOrderMember(_sourceMember.Type); if (orderMember == null) { @@ -559,7 +540,7 @@ public override Expression GetAccess(Expression enumerableAccess) { var helper = new EnumerableTypeHelper(enumerableAccess.Type); - var count = helper.IsEnumerableInterface + var count = helper.IsEnumerableOrQueryable ? GetLinqMethodCall(nameof(Enumerable.Count), enumerableAccess, helper) : helper.GetCountFor(enumerableAccess, MapperData.TargetMember.Type.GetNonNullableType()); diff --git a/AgileMapper/DataSources/Factories/SourceMemberDataSourcesFactory.cs b/AgileMapper/DataSources/Factories/SourceMemberDataSourcesFactory.cs new file mode 100644 index 000000000..d61e4d115 --- /dev/null +++ b/AgileMapper/DataSources/Factories/SourceMemberDataSourcesFactory.cs @@ -0,0 +1,101 @@ +namespace AgileObjects.AgileMapper.DataSources.Factories +{ + using System.Collections.Generic; + using Extensions.Internal; + using Members; + + internal static class SourceMemberDataSourcesFactory + { + public static IEnumerable<IDataSource> Create(DataSourceFindContext context) + { + if (context.TargetMember.IsCustom) + { + yield break; + } + + if (context.DoNotUseSourceMemberDataSource()) + { + if (context.DataSourceIndex == 0) + { + if (context.UseFallbackComplexTypeDataSource()) + { + yield return ComplexTypeDataSource.Create(context.DataSourceIndex, context.MemberMappingData); + } + } + else if (context.UseFallbackForConditionalConfiguredDataSource()) + { + yield return context.GetFallbackDataSource(); + } + + if (context.UseConfiguredDataSourcesOnly()) + { + yield break; + } + } + + if (context.ReturnSimpleTypeToTargetDataSources()) + { + var updatedMapperData = new ChildMemberMapperData( + context.MatchingSourceMemberDataSource.SourceMember, + context.TargetMember, + context.MemberMapperData.Parent); + + var configuredRootDataSources = context + .MapperContext + .UserConfigurations + .GetDataSourcesForToTarget(updatedMapperData); + + foreach (var configuredRootDataSource in configuredRootDataSources) + { + yield return configuredRootDataSource; + } + } + + yield return context.MatchingSourceMemberDataSource; + + if (context.UseFallbackDataSource()) + { + yield return context.GetFallbackDataSource(); + } + } + + private static bool DoNotUseSourceMemberDataSource(this DataSourceFindContext context) + { + return !context.BestSourceMemberMatch.IsUseable || + context.ConfiguredDataSources.Any(context.MatchingSourceMemberDataSource, (msmds, cds) => cds.IsSameAs(msmds)); + } + + private static bool UseFallbackComplexTypeDataSource(this DataSourceFindContext context) + { + var targetMember = context.TargetMember; + + return targetMember.IsComplex && !targetMember.IsDictionary && (targetMember.Type != typeof(object)); + } + + private static bool UseFallbackForConditionalConfiguredDataSource(this DataSourceFindContext context) + { + return context.ConfiguredDataSources.Any() && + context.ConfiguredDataSources.Last().IsConditional && + (context.MatchingSourceMemberDataSource.SourceMember != null); + } + + private static bool UseConfiguredDataSourcesOnly(this DataSourceFindContext context) + { + return context.BestSourceMemberMatch.IsUseable || + (context.MatchingSourceMemberDataSource.SourceMember == null); + } + + private static bool ReturnSimpleTypeToTargetDataSources(this DataSourceFindContext context) + { + return context.MatchingSourceMemberDataSource.SourceMember.IsSimple && + context.MapperContext.UserConfigurations.HasConfiguredToTargetDataSources; + } + + private static bool UseFallbackDataSource(this DataSourceFindContext context) + { + return !context.TargetMember.IsReadOnly && + context.MatchingSourceMemberDataSource.IsConditional && + (context.MatchingSourceMemberDataSource.IsValid || context.ConfiguredDataSources.Any()); + } + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/Finders/DataSourceFindContext.cs b/AgileMapper/DataSources/Finders/DataSourceFindContext.cs deleted file mode 100644 index e5d72f387..000000000 --- a/AgileMapper/DataSources/Finders/DataSourceFindContext.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders -{ - using System.Collections.Generic; - using Extensions; - using Extensions.Internal; - using Members; - - internal class DataSourceFindContext - { - public DataSourceFindContext(IChildMemberMappingData childMappingData) - { - ChildMappingData = childMappingData; - - ConfiguredDataSources = GetConfiguredDataSources(MapperData); - - if (!MapperData.Parent.Context.IsForToTargetMapping) - { - return; - } - - var originalChildMapperData = new ChildMemberMapperData( - MapperData.TargetMember, - MapperData.Parent.OriginalMapperData); - - ConfiguredDataSources = ConfiguredDataSources.Append( - GetConfiguredDataSources(originalChildMapperData)); - } - - private IList<IConfiguredDataSource> GetConfiguredDataSources(IMemberMapperData mapperData) - { - return MapperData - .MapperContext - .UserConfigurations - .GetDataSources(mapperData); - } - - public IChildMemberMappingData ChildMappingData { get; } - - public IMemberMapperData MapperData => ChildMappingData.MapperData; - - public int DataSourceIndex { get; set; } - - public bool StopFind { get; set; } - - public IList<IConfiguredDataSource> ConfiguredDataSources { get; } - - public IDataSource GetFallbackDataSource() - => ChildMappingData.RuleSet.FallbackDataSourceFactory.Create(MapperData); - - public IDataSource GetFinalDataSource(IDataSource foundDataSource, IChildMemberMappingData mappingData = null) - { - if (mappingData == null) - { - mappingData = ChildMappingData; - } - - var childTargetMember = mappingData.MapperData.TargetMember; - - if (UseComplexTypeDataSource(foundDataSource, childTargetMember)) - { - return new ComplexTypeMappingDataSource(foundDataSource, DataSourceIndex, mappingData); - } - - if (childTargetMember.IsEnumerable) - { - return new EnumerableMappingDataSource(foundDataSource, DataSourceIndex, mappingData); - } - - return foundDataSource; - } - - private static bool UseComplexTypeDataSource(IDataSource dataSource, QualifiedMember targetMember) - { - if (!targetMember.IsComplex) - { - return false; - } - - if (targetMember.IsDictionary) - { - return true; - } - - if (targetMember.Type == typeof(object)) - { - return !dataSource.SourceMember.Type.IsSimple(); - } - - if ((dataSource.Value.Type == targetMember.Type) && - (dataSource.SourceMember.Type != targetMember.Type)) - { - return false; - } - - return !targetMember.Type.IsFromBcl(); - } - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/Finders/DataSourceFinder.cs b/AgileMapper/DataSources/Finders/DataSourceFinder.cs deleted file mode 100644 index cf2cab9ca..000000000 --- a/AgileMapper/DataSources/Finders/DataSourceFinder.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders -{ - using System.Collections.Generic; - using System.Linq; - using Members; - - internal struct DataSourceFinder - { - public static DataSourceSet FindFor(IChildMemberMappingData childMappingData) - { - var findContext = new DataSourceFindContext(childMappingData); - var validDataSources = EnumerateDataSources(findContext).ToArray(); - - return new DataSourceSet(findContext.MapperData, validDataSources); - } - - private static IEnumerable<IDataSource> EnumerateDataSources(DataSourceFindContext context) - { - foreach (var finder in EnumerateFinders()) - { - foreach (var dataSource in finder.FindFor(context)) - { - if (!dataSource.IsValid) - { - continue; - } - - yield return dataSource; - - if (!dataSource.IsConditional) - { - yield break; - } - } - - if (context.StopFind) - { - yield break; - } - } - } - - private static IEnumerable<IDataSourceFinder> EnumerateFinders() - { - yield return default(ConfiguredDataSourceFinder); - yield return default(MaptimeDataSourceFinder); - yield return default(SourceMemberDataSourceFinder); - yield return default(MetaMemberDataSourceFinder); - } - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/Finders/IDataSourceFinder.cs b/AgileMapper/DataSources/Finders/IDataSourceFinder.cs deleted file mode 100644 index 658e11e98..000000000 --- a/AgileMapper/DataSources/Finders/IDataSourceFinder.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders -{ - using System.Collections.Generic; - - internal interface IDataSourceFinder - { - IEnumerable<IDataSource> FindFor(DataSourceFindContext context); - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs b/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs deleted file mode 100644 index 18aec53a3..000000000 --- a/AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources.Finders -{ - using Extensions.Internal; - using Members; - using System.Collections.Generic; - - internal struct SourceMemberDataSourceFinder : IDataSourceFinder - { - public IEnumerable<IDataSource> FindFor(DataSourceFindContext context) - { - if (context.MapperData.TargetMember.IsCustom) - { - yield break; - } - - var matchingSourceMemberDataSource = GetSourceMemberDataSourceOrNull(context); - var configuredDataSources = context.ConfiguredDataSources; - var targetMember = context.MapperData.TargetMember; - - if ((matchingSourceMemberDataSource == null) || - configuredDataSources.Any(cds => cds.IsSameAs(matchingSourceMemberDataSource))) - { - if (context.DataSourceIndex == 0) - { - if (UseFallbackComplexTypeMappingDataSource(targetMember)) - { - yield return new ComplexTypeMappingDataSource(context.DataSourceIndex, context.ChildMappingData); - } - } - else if (configuredDataSources.Any() && configuredDataSources.Last().IsConditional) - { - yield return context.GetFallbackDataSource(); - } - - yield break; - } - - if (matchingSourceMemberDataSource.SourceMember.IsSimple && - context.MapperData.MapperContext.UserConfigurations.HasConfiguredToTargetDataSources) - { - var updatedMapperData = new ChildMemberMapperData( - matchingSourceMemberDataSource.SourceMember, - targetMember, - context.MapperData.Parent); - - var configuredRootDataSources = context - .MapperData - .MapperContext - .UserConfigurations - .GetDataSourcesForToTarget(updatedMapperData); - - foreach (var configuredRootDataSource in configuredRootDataSources) - { - yield return configuredRootDataSource; - } - } - - yield return matchingSourceMemberDataSource; - - if (!targetMember.IsReadOnly && - matchingSourceMemberDataSource.IsConditional && - (matchingSourceMemberDataSource.IsValid || configuredDataSources.Any())) - { - yield return context.GetFallbackDataSource(); - } - } - - private static IDataSource GetSourceMemberDataSourceOrNull(DataSourceFindContext context) - { - var bestMatchingSourceMember = SourceMemberMatcher.GetMatchFor( - context.ChildMappingData, - out var contextMappingData); - - if (bestMatchingSourceMember == null) - { - return null; - } - - var sourceMemberDataSource = SourceMemberDataSource - .For(bestMatchingSourceMember, contextMappingData.MapperData); - - return context.GetFinalDataSource(sourceMemberDataSource, contextMappingData); - } - - private static bool UseFallbackComplexTypeMappingDataSource(QualifiedMember targetMember) - => targetMember.IsComplex && !targetMember.IsDictionary && (targetMember.Type != typeof(object)); - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/IDataSource.cs b/AgileMapper/DataSources/IDataSource.cs index f343b67ee..b88649802 100644 --- a/AgileMapper/DataSources/IDataSource.cs +++ b/AgileMapper/DataSources/IDataSource.cs @@ -1,13 +1,13 @@ namespace AgileObjects.AgileMapper.DataSources { using System.Collections.Generic; - using Extensions.Internal; - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; internal interface IDataSource : IConditionallyChainable { @@ -19,10 +19,12 @@ internal interface IDataSource : IConditionallyChainable bool IsConditional { get; } - ICollection<ParameterExpression> Variables { get; } + bool IsFallback { get; } + + IList<ParameterExpression> Variables { get; } - Expression AddPreCondition(Expression population); + Expression AddSourceCondition(Expression value); - Expression AddCondition(Expression value, Expression alternateBranch = null); + Expression FinalisePopulation(Expression population, Expression alternatePopulation = null); } } diff --git a/AgileMapper/DataSources/IDataSourceFactory.cs b/AgileMapper/DataSources/IDataSourceFactory.cs deleted file mode 100644 index 7701f870b..000000000 --- a/AgileMapper/DataSources/IDataSourceFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace AgileObjects.AgileMapper.DataSources -{ - using Members; - - internal interface IDataSourceFactory - { - IDataSource Create(IMemberMapperData mapperData); - } -} \ No newline at end of file diff --git a/AgileMapper/DataSources/IDataSourceSet.cs b/AgileMapper/DataSources/IDataSourceSet.cs new file mode 100644 index 000000000..5259c9d53 --- /dev/null +++ b/AgileMapper/DataSources/IDataSourceSet.cs @@ -0,0 +1,33 @@ +namespace AgileObjects.AgileMapper.DataSources +{ + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Members; + + internal interface IDataSourceSet : IEnumerable<IDataSource> + { + IMemberMapperData MapperData { get; } + + bool None { get; } + + bool HasValue { get; } + + bool IsConditional { get; } + + Expression SourceMemberTypeTest { get; } + + IList<ParameterExpression> Variables { get; } + + IDataSource this[int index] { get; } + + int Count { get; } + + Expression BuildValue(); + + Expression GetFinalValueOrNull(); + } +} \ No newline at end of file diff --git a/AgileMapper/DataSources/SourceMemberDataSource.cs b/AgileMapper/DataSources/SourceMemberDataSource.cs index 29643f238..c3657e3d6 100644 --- a/AgileMapper/DataSources/SourceMemberDataSource.cs +++ b/AgileMapper/DataSources/SourceMemberDataSource.cs @@ -1,21 +1,22 @@ namespace AgileObjects.AgileMapper.DataSources { using System.Collections.Generic; - using Extensions.Internal; - using Members; - using ObjectPopulation; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; + using ObjectPopulation; + using ReadableExpressions.Extensions; internal class SourceMemberDataSource : DataSourceBase { - private SourceMemberDataSource( + public SourceMemberDataSource( IQualifiedMember sourceMember, Expression sourceMemberValue, + Expression condition, IMemberMapperData mapperData) : base( sourceMember, @@ -23,6 +24,14 @@ private SourceMemberDataSource( mapperData) { SourceMemberTypeTest = CreateSourceMemberTypeTest(sourceMemberValue, mapperData); + + if (condition == null) + { + Condition = base.Condition; + return; + } + + Condition = IsConditional ? Expression.AndAlso(base.Condition, condition) : condition; } private static Expression CreateSourceMemberTypeTest(Expression value, IMemberMapperData mapperData) @@ -64,17 +73,6 @@ private static Expression GetRuntimeTypeCheck(UnaryExpression cast, IMemberMappe return memberHasRuntimeType; } - public static SourceMemberDataSource For(IQualifiedMember sourceMember, IMemberMapperData mapperData) - { - sourceMember = sourceMember.RelativeTo(mapperData.SourceMember); - - var sourceMemberValue = sourceMember - .GetQualifiedAccess(mapperData) - .GetConversionTo(sourceMember.Type); - - var sourceMemberDataSource = new SourceMemberDataSource(sourceMember, sourceMemberValue, mapperData); - - return sourceMemberDataSource; - } + public override Expression Condition { get; } } } \ No newline at end of file diff --git a/AgileMapper/DataSources/ValueExpressionBuilders.cs b/AgileMapper/DataSources/ValueExpressionBuilders.cs new file mode 100644 index 000000000..b0e55db9d --- /dev/null +++ b/AgileMapper/DataSources/ValueExpressionBuilders.cs @@ -0,0 +1,76 @@ +namespace AgileObjects.AgileMapper.DataSources +{ + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Extensions.Internal; + using Members; + + internal static class ValueExpressionBuilders + { + public static Expression SingleDataSource(IDataSource dataSource, IMemberMapperData mapperData) + { + var value = dataSource.IsConditional + ? dataSource.Value.ToIfFalseDefaultCondition(dataSource.Condition) + : dataSource.Value; + + return dataSource.AddSourceCondition(value); + } + + public static Expression ConditionTree(IList<IDataSource> dataSources, IMemberMapperData mapperData) + { + var value = SingleDataSource(dataSources.Last(), mapperData); + + for (var i = dataSources.Count - 2; i >= 0;) + { + var dataSource = dataSources[i--]; + + var dataSourceValue = dataSource.IsConditional + ? Expression.Condition( + dataSource.Condition, + dataSource.Value.GetConversionTo(value.Type), + value) + : dataSource.Value; + + value = dataSource.AddSourceCondition(dataSourceValue); + } + + return value; + } + + public static Expression ValueSequence(IList<IDataSource> dataSources, IMemberMapperData mapperData) + { + if (dataSources.HasOne()) + { + return dataSources.First().GetValueSequenceValue(); + } + + var mappingExpressions = dataSources + .ProjectToArray(dataSource => dataSource.GetValueSequenceValue()); + + return Expression.Block(mappingExpressions); + } + + private static Expression GetValueSequenceValue(this IDataSource dataSource) + { + var mapping = dataSource.Value; + + if (dataSource.IsConditional) + { + mapping = Expression.IfThen(dataSource.Condition, mapping); + } + + mapping = dataSource.AddSourceCondition(mapping); + + if (dataSource.Variables.Any()) + { + mapping = Expression.Block(dataSource.Variables, mapping); + } + + return mapping; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Extensions/CollectionData.cs b/AgileMapper/Extensions/CollectionData.cs index 427382ad3..6a1c876f4 100644 --- a/AgileMapper/Extensions/CollectionData.cs +++ b/AgileMapper/Extensions/CollectionData.cs @@ -114,9 +114,9 @@ private static Dictionary<TId, List<TItem>> GetItemsById<TItem, TId>(IEnumerable { return items .WhereNotNull() - .Project(item => new + .Project(idFactory, (f, item) => new { - Id = idFactory.Invoke(item), + Id = f.Invoke(item), Item = item }) .Filter(d => d.Id != null) diff --git a/AgileMapper/Extensions/Internal/EnumerableExtensions.cs b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs index 492cab8f1..303e588ac 100644 --- a/AgileMapper/Extensions/Internal/EnumerableExtensions.cs +++ b/AgileMapper/Extensions/Internal/EnumerableExtensions.cs @@ -39,14 +39,17 @@ public static void AddRange<TContained, TItem>(this List<TContained> items, IEnu } } #endif - [DebuggerStepThrough] public static T First<T>(this IList<T> items) => items[0]; [DebuggerStepThrough] public static T First<T>(this IList<T> items, Func<T, bool> predicate) + => First(items, predicate, (p, item) => p.Invoke(item)); + + [DebuggerStepThrough] + public static T First<TArg, T>(this IList<T> items, TArg argument, Func<TArg, T, bool> predicate) { - if (TryFindMatch(items, predicate, out var match)) + if (TryFindMatch(items, argument, predicate, out var match)) { return match; } @@ -56,7 +59,11 @@ public static T First<T>(this IList<T> items, Func<T, bool> predicate) [DebuggerStepThrough] public static T FirstOrDefault<T>(this IList<T> items, Func<T, bool> predicate) - => TryFindMatch(items, predicate, out var match) ? match : default(T); + => FirstOrDefault(items, predicate, (p, item) => predicate.Invoke(item)); + + [DebuggerStepThrough] + public static T FirstOrDefault<TArg, T>(this IList<T> items, TArg argument, Func<TArg, T, bool> predicate) + => TryFindMatch(items, argument, predicate, out var match) ? match : default(T); [DebuggerStepThrough] public static IEnumerable<T> TakeUntil<T>(this IEnumerable<T> items, Func<T, bool> predicate) @@ -74,12 +81,16 @@ public static IEnumerable<T> TakeUntil<T>(this IEnumerable<T> items, Func<T, boo [DebuggerStepThrough] public static bool TryFindMatch<T>(this IList<T> items, Func<T, bool> predicate, out T match) + => TryFindMatch(items, predicate, (p, item) => p.Invoke(item), out match); + + [DebuggerStepThrough] + public static bool TryFindMatch<TArg, T>(this IList<T> items, TArg argument, Func<TArg, T, bool> predicate, out T match) { - for (int i = 0, n = items.Count; i < n; i++) + for (int i = 0, n = items.Count; i < n;) { - match = items[i]; + match = items[i++]; - if (predicate.Invoke(match)) + if (predicate.Invoke(argument, match)) { return true; } @@ -98,6 +109,10 @@ public static bool TryFindMatch<T>(this IList<T> items, Func<T, bool> predicate, [DebuggerStepThrough] public static bool Any<T>(this IList<T> items, Func<T, bool> predicate) => !items.None(predicate); + [DebuggerStepThrough] + public static bool Any<TArg, T>(this IList<T> items, TArg argument, Func<TArg, T, bool> predicate) + => !None(items, argument, predicate); + [DebuggerStepThrough] public static bool None<T>(this ICollection<T> items) => items.Count == 0; @@ -108,10 +123,13 @@ public static bool TryFindMatch<T>(this IList<T> items, Func<T, bool> predicate, public static bool None<T>(this IEnumerable<T> items) => !items.GetEnumerator().MoveNext(); public static bool None<T>(this IList<T> items, Func<T, bool> predicate) + => None(items, predicate, (p, item) => p.Invoke(item)); + + public static bool None<TArg, T>(this IList<T> items, TArg argument, Func<TArg, T, bool> predicate) { for (int i = 0, n = items.Count; i < n; i++) { - if (predicate.Invoke(items[i])) + if (predicate.Invoke(argument, items[i])) { return false; } @@ -127,6 +145,12 @@ public static bool All<T>(this IList<T> items, Func<T, bool> predicate) public static bool HasOne<T>(this ICollection<T> items) => items.Count == 1; public static TResult[] ProjectToArray<TItem, TResult>(this IList<TItem> items, Func<TItem, TResult> projector) + => ProjectToArray(items, projector, (p, item) => p.Invoke(item)); + + public static TResult[] ProjectToArray<TArg, TItem, TResult>( + this IList<TItem> items, + TArg argument, + Func<TArg, TItem, TResult> projector) { var itemCount = items.Count; @@ -139,19 +163,22 @@ public static TResult[] ProjectToArray<TItem, TResult>(this IList<TItem> items, for (var i = 0; ;) { - result[i] = projector.Invoke(items[i]); + result[i] = projector.Invoke(argument, items[i]); if (++i == itemCount) { - break; + return result; } } - - return result; } public static T[] CopyToArray<T>(this IList<T> items) { + if (items.Count == 0) + { + return Enumerable<T>.EmptyArray; + } + var clonedArray = new T[items.Count]; clonedArray.CopyFrom(items); @@ -159,44 +186,19 @@ public static T[] CopyToArray<T>(this IList<T> items) return clonedArray; } - public static Expression ReverseChain<T>(this IList<T> items) - where T : IConditionallyChainable - { - return Chain( - items, - i => i.Last(), - item => item.AddPreConditionIfNecessary(item.Value), - (valueSoFar, item) => item.AddPreConditionIfNecessary( - Expression.Condition(item.Condition, item.Value, valueSoFar)), - i => i.Reverse()); - } - - public static Expression AddPreConditionIfNecessary(this IConditionallyChainable item, Expression ifTrueBranch) - { - if (item.PreCondition == null) - { - return ifTrueBranch; - } - - return Expression.Condition( - item.PreCondition, - ifTrueBranch, - ifTrueBranch.Type.ToDefaultExpression()); - } - public static Expression Chain<TItem>( this IList<TItem> items, Func<TItem, Expression> seedValueFactory, - Func<Expression, TItem, Expression> itemValueFactory) + Func<Expression, TItem, Expression> chainedValueFactory) { - return Chain(items, i => i.First(), seedValueFactory, itemValueFactory, i => i); + return Chain(items, i => i.First(), seedValueFactory, chainedValueFactory, i => i); } - private static Expression Chain<TItem>( - IList<TItem> items, + public static Expression Chain<TItem>( + this IList<TItem> items, Func<IList<TItem>, TItem> seedFactory, Func<TItem, Expression> seedValueFactory, - Func<Expression, TItem, Expression> itemValueFactory, + Func<Expression, TItem, Expression> chainedValueFactory, Func<IList<TItem>, IEnumerable<TItem>> initialOperation) { if (items.None()) @@ -211,17 +213,20 @@ private static Expression Chain<TItem>( return initialOperation.Invoke(items) .Skip(1) + .WhereNotNull() .Aggregate( seedValueFactory.Invoke(seedFactory.Invoke(items)), - itemValueFactory.Invoke); + (chainedExpression, item) => (chainedExpression == null) + ? seedValueFactory.Invoke(item) + : chainedValueFactory.Invoke(chainedExpression, item)); } - public static void CopyTo<T>(this IList<T> sourceList, List<T> targetList, int startIndex = 0) + public static void CopyTo<T>(this IList<T> sourceList, List<T> targetList) => targetList.AddRange(sourceList); public static void CopyFrom<T>(this IList<T> targetList, IList<T> sourceList, int startIndex = 0) { - for (var i = 0; i < sourceList.Count; i++) + for (var i = 0; i < sourceList.Count && i < targetList.Count; ++i) { targetList[i + startIndex] = sourceList[i]; } @@ -241,6 +246,12 @@ public static T[] Prepend<T>(this IList<T> items, T initialItem) return newArray; } + public static void Insert<T>(this IList<T> items, T item, int insertionOffset) + { + var insertionIndex = items.Count - insertionOffset; + items.Insert(insertionIndex, item); + } + public static T[] Append<T>(this IList<T> array, T extraItem) { switch (array.Count) @@ -265,11 +276,16 @@ public static T[] Append<T>(this IList<T> array, T extraItem) } } - public static T[] Append<T>(this IList<T> array, IList<T> extraItems) + public static IList<T> Append<T>(this IList<T> array, IList<T> extraItems) { - if (array.Count == 1) + if (extraItems.Count == 0) { - return Prepend(extraItems, array[0]); + return array; + } + + if (array.Count == 0) + { + return extraItems; } if (extraItems.Count == 1) @@ -277,6 +293,11 @@ public static T[] Append<T>(this IList<T> array, IList<T> extraItems) return Append(array, extraItems[0]); } + if (array.Count == 1) + { + return Prepend(extraItems, array[0]); + } + var combinedArray = new T[array.Count + extraItems.Count]; combinedArray.CopyFrom(array); diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs index a8d5116b1..38ca128cf 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.Replace.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; - using Caching; #if NET35 using Microsoft.Scripting.Ast; using static Microsoft.Scripting.Ast.ExpressionType; @@ -11,6 +10,7 @@ using System.Linq.Expressions; using static System.Linq.Expressions.ExpressionType; #endif + using Caching; internal static partial class ExpressionExtensions { diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 92e6fb525..0b4ff0b77 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -4,10 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; - using System.Reflection; - using NetStandardPolyfills; - using ObjectPopulation.Enumerables; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; using ReadableExpressions.Translations; @@ -17,6 +13,11 @@ using System.Linq.Expressions; using static System.Linq.Expressions.ExpressionType; #endif + using System.Reflection; + using Members; + using NetStandardPolyfills; + using ObjectPopulation.Enumerables; + using ReadableExpressions.Extensions; internal static partial class ExpressionExtensions { @@ -54,6 +55,9 @@ public static bool IsNullableHasValueAccess(this MemberExpression memberAccess) (memberAccess.Expression.Type.IsNullableType()); } + public static Expression Negate(this Expression expression) + => (expression.NodeType != Not) ? Expression.Not(expression) : ((UnaryExpression)expression).Operand; + [DebuggerStepThrough] public static ConstantExpression ToConstantExpression<T>(this T item) => ToConstantExpression(item, typeof(T)); @@ -65,6 +69,10 @@ public static ConstantExpression ToConstantExpression<TItem>(this TItem item, Ty [DebuggerStepThrough] public static DefaultExpression ToDefaultExpression(this Type type) => Expression.Default(type); + [DebuggerStepThrough] + public static ConditionalExpression ToIfFalseDefaultCondition(this Expression value, Expression condition) + => Expression.Condition(condition, value, value.Type.ToDefaultExpression()); + public static Expression AndTogether(this IList<Expression> expressions) { if (expressions.None()) @@ -125,7 +133,7 @@ public static Expression GetIsNotDefaultComparison(this Expression expression) { if (expression.Type.IsNullableType()) { - return Expression.Property(expression, "HasValue"); + return GetNullableHasValueAccess(expression); } var typeDefault = expression.Type.ToDefaultExpression(); @@ -133,6 +141,12 @@ public static Expression GetIsNotDefaultComparison(this Expression expression) return Expression.NotEqual(expression, typeDefault); } + public static Expression GetNullableHasValueAccess(this Expression expression) + => Expression.Property(expression, "HasValue"); + + public static Expression GetNullableValueAccess(this Expression nullableExpression) + => Expression.Property(nullableExpression, "Value"); + public static Expression GetIndexAccess(this Expression indexedExpression, Expression indexValue) { if (indexedExpression.Type.IsArray) @@ -198,11 +212,16 @@ public static Expression GetCount( ? nameof(Enumerable.LongCount) : nameof(Enumerable.Count); - return Expression.Call( - typeof(Enumerable) - .GetPublicStaticMethod(linqCountMethodName, parameterCount: 1) - .MakeGenericMethod(collectionAccess.Type.GetEnumerableElementType()), - collectionAccess); + var linqCountMethod = typeof(Enumerable) + .GetPublicStaticMethod(linqCountMethodName, parameterCount: 1) + .MakeGenericMethod(collectionAccess.Type.GetEnumerableElementType()); + + if (collectionAccess.Type.IsAssignableTo(linqCountMethod.GetParameters().First().ParameterType)) + { + return Expression.Call(linqCountMethod, collectionAccess); + } + + return null; } public static Expression GetValueOrDefaultCall(this Expression nullableExpression) @@ -229,6 +248,11 @@ public static Expression GetConversionTo(this Expression expression, Type target return expression; } + if ((targetType == typeof(object)) && expression.Type.IsValueType()) + { + return Expression.Convert(expression, typeof(object)); + } + if (expression.Type.GetNonNullableType() == targetType) { return expression.GetValueOrDefaultCall(); @@ -306,7 +330,13 @@ private static MethodInfo GetNonListToArrayConversionMethod(EnumerableTypeHelper [DebuggerStepThrough] public static MethodCallExpression WithToStringCall(this Expression value) - => Expression.Call(value, value.Type.GetPublicInstanceMethod("ToString", parameterCount: 0)); + { + var toStringMethodType = value.Type.IsInterface() + ? typeof(object) + : value.Type; + + return Expression.Call(value, toStringMethodType.GetPublicInstanceMethod("ToString", parameterCount: 0)); + } public static Expression GetReadOnlyCollectionCreation(this Expression enumerable, Type elementType) { @@ -386,7 +416,7 @@ public static Expression GetEmptyInstanceCreation( typeHelper = new EnumerableTypeHelper(enumerableType, elementType); } - if (typeHelper.IsEnumerableInterface) + if (typeHelper.IsEnumerableOrQueryable) { return Expression.Field(null, typeof(Enumerable<>).MakeGenericType(elementType), "Empty"); } @@ -434,75 +464,6 @@ public static bool IsRootedIn(this Expression expression, Expression possiblePar return false; } - public static bool TryGetMappingBody(this Expression value, out Expression mapping) - { - if (value.NodeType == Try) - { - value = ((TryExpression)value).Body; - } - - if ((value.NodeType != Block)) - { - mapping = null; - return false; - } - - var mappingBlock = (BlockExpression)value; - var mappingVariables = mappingBlock.Variables.ToList(); - - if (mappingBlock.Expressions[0].NodeType == Try) - { - mappingBlock = (BlockExpression)((TryExpression)mappingBlock.Expressions[0]).Body; - mappingVariables.AddRange(mappingBlock.Variables); - } - else - { - mappingBlock = (BlockExpression)value; - } - - if (mappingBlock.Expressions.HasOne()) - { - mapping = null; - return false; - } - - mapping = mappingBlock; - - var mappingExpressions = GetMappingExpressions(mapping); - - if (mappingExpressions.HasOne() && - (mappingExpressions[0].NodeType == Block)) - { - mappingBlock = (BlockExpression)mappingExpressions[0]; - mappingVariables.AddRange(mappingBlock.Variables); - mapping = mappingBlock.Update(mappingVariables, mappingBlock.Expressions); - return true; - } - - mapping = mappingVariables.Any() - ? Expression.Block(mappingVariables, mappingExpressions) - : mappingExpressions.HasOne() - ? mappingExpressions[0] - : Expression.Block(mappingExpressions); - - return true; - } - - private static IList<Expression> GetMappingExpressions(Expression mapping) - { - var expressions = new List<Expression>(); - - while (mapping.NodeType == Block) - { - var mappingBlock = (BlockExpression)mapping; - - expressions.AddRange(mappingBlock.Expressions.Take(mappingBlock.Expressions.Count - 1)); - mapping = mappingBlock.Result; - } - - return expressions; - } - public static bool TryGetVariableAssignment(this IList<Expression> mappingExpressions, out BinaryExpression binaryExpression) { if (mappingExpressions.TryFindMatch(exp => exp.NodeType == Assign, out var assignment)) @@ -514,7 +475,6 @@ public static bool TryGetVariableAssignment(this IList<Expression> mappingExpres binaryExpression = null; return false; } - #if NET35 public static LambdaExpression ToDlrExpression(this LinqExp.LambdaExpression linqLambda) => LinqExpressionToDlrExpressionConverter.Convert(linqLambda); @@ -525,5 +485,34 @@ public static Expression<TDelegate> ToDlrExpression<TDelegate>(this LinqExp.Expr public static Expression ToDlrExpression(this LinqExp.Expression linqExpression) => LinqExpressionToDlrExpressionConverter.Convert(linqExpression); #endif + public static TryExpression WrapInTryCatch(this Expression mapping, IMemberMapperData mapperData) + { + var configuredCallback = mapperData.MapperContext.UserConfigurations.GetExceptionCallbackOrNull(mapperData); + var exceptionVariable = Parameters.Create<Exception>("ex"); + + if (configuredCallback == null) + { + var catchBody = Expression.Throw( + MappingException.GetFactoryMethodCall(mapperData, exceptionVariable), + mapping.Type); + + return CreateTryCatch(mapping, exceptionVariable, catchBody); + } + + var configuredCatchBody = configuredCallback + .ToCatchBody(exceptionVariable, mapping.Type, mapperData); + + return CreateTryCatch(mapping, exceptionVariable, configuredCatchBody); + } + + private static TryExpression CreateTryCatch( + Expression mappingBlock, + ParameterExpression exceptionVariable, + Expression catchBody) + { + var catchBlock = Expression.Catch(exceptionVariable, catchBody); + + return Expression.TryCatch(mappingBlock, catchBlock); + } } } diff --git a/AgileMapper/Extensions/Internal/IConditionallyChainable.cs b/AgileMapper/Extensions/Internal/IConditionallyChainable.cs index 894f7c9fc..6c619f709 100644 --- a/AgileMapper/Extensions/Internal/IConditionallyChainable.cs +++ b/AgileMapper/Extensions/Internal/IConditionallyChainable.cs @@ -8,8 +8,6 @@ internal interface IConditionallyChainable { - Expression PreCondition { get; } - Expression Condition { get; } Expression Value { get; } diff --git a/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs index d1d22f324..ed54c48fc 100644 --- a/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs @@ -13,7 +13,7 @@ internal static class StringExpressionExtensions { - public static readonly Expression EmptyString = Expression.Field(null, typeof(string), "Empty"); + public static readonly Expression EmptyString = Expression.Field(null, typeof(string), nameof(string.Empty)); public static readonly Expression Underscore = "_".ToConstantExpression(); private static readonly MethodInfo _stringJoinMethod; @@ -23,7 +23,7 @@ static StringExpressionExtensions() { var stringMethods = typeof(string) .GetPublicStaticMethods() - .Filter(m => m.Name == "Join" || m.Name == "Concat") + .Filter(m => m.Name == nameof(string.Join) || m.Name == nameof(string.Concat)) .Project(m => new { Method = m, @@ -33,26 +33,38 @@ static StringExpressionExtensions() .ToArray(); _stringJoinMethod = stringMethods.First(m => - (m.Method.Name == "Join") && + (m.Method.Name == nameof(string.Join)) && (m.Parameters.Length == 2) && (m.Parameters[0].ParameterType == typeof(string)) && (m.Parameters[1].ParameterType == typeof(string[]))).Method; _stringConcatMethods = stringMethods - .Filter(m => (m.Method.Name == "Concat") && (m.FirstParameterType == typeof(string))) + .Filter(m => (m.Method.Name == nameof(string.Concat)) && (m.FirstParameterType == typeof(string))) .OrderBy(m => m.Parameters.Length) .Project(m => m.Method) .ToArray(); } + public static Expression GetIsNullOrWhiteSpaceCall(Expression stringValue) + { + return Expression.Call( +#if NET35 + typeof(StringExtensions) +#else + typeof(string) +#endif + .GetPublicStaticMethod("IsNullOrWhiteSpace"), + stringValue); + } + public static MethodInfo GetConcatMethod(int parameterCount) - => _stringConcatMethods.First(m => m.GetParameters().Length == parameterCount); + => _stringConcatMethods.First(parameterCount, (pc, m) => m.GetParameters().Length == pc); public static Expression GetStringConcatCall(this IList<Expression> expressions) { if (expressions.None()) { - return string.Empty.ToConstantExpression(); + return EmptyString; } if (expressions.HasOne()) diff --git a/AgileMapper/Extensions/Internal/TypeExtensions.cs b/AgileMapper/Extensions/Internal/TypeExtensions.cs index 048d286ff..83b398db9 100644 --- a/AgileMapper/Extensions/Internal/TypeExtensions.cs +++ b/AgileMapper/Extensions/Internal/TypeExtensions.cs @@ -15,6 +15,9 @@ internal static class TypeExtensions private static readonly Assembly _systemCoreLib = typeof(Func<>).GetAssembly(); #endif + public static string GetSourceValueVariableName(this Type sourceType) + => "source" + sourceType.GetVariableNameInPascalCase(); + public static string GetShortVariableName(this Type type) { var variableName = type.GetVariableNameInPascalCase(); @@ -120,7 +123,7 @@ public static Type[] GetCoercibleNumericTypes(this Type numericType) return Constants .NumericTypeMaxValuesByType - .Filter(kvp => kvp.Value < typeMaxValue) + .Filter(typeMaxValue, (tmv, kvp) => kvp.Value < tmv) .Project(kvp => kvp.Key) .ToArray(); } diff --git a/AgileMapper/Extensions/PublicEnumerableExtensions.cs b/AgileMapper/Extensions/PublicEnumerableExtensions.cs index 33760728b..59ed54978 100644 --- a/AgileMapper/Extensions/PublicEnumerableExtensions.cs +++ b/AgileMapper/Extensions/PublicEnumerableExtensions.cs @@ -24,26 +24,22 @@ public static class PublicEnumerableExtensions /// <returns>An iterator to transform this enumerable.</returns> [DebuggerStepThrough] public static IEnumerable<TResult> Project<TItem, TResult>(this IEnumerable<TItem> items, Func<TItem, TResult> projector) + => Project(items, projector, (p, item) => p.Invoke(item)); + + [DebuggerStepThrough] + internal static IEnumerable<TResult> Project<TItem, TArg, TResult>( + this IEnumerable<TItem> items, + TArg argument, + Func<TArg, TItem, TResult> projector) { foreach (var item in items) { - yield return projector.Invoke(item); + yield return projector.Invoke(argument, item); } } - /// <summary> - /// Project these <paramref name="items"/> to a new enumerable of type <typeparamref name="TResult"/>, - /// using the given <paramref name="projector"/>. - /// </summary> - /// <typeparam name="TItem">The type of object stored in the enumerable.</typeparam> - /// <typeparam name="TResult"> - /// The type of object to which each item in the enumerable will be projected. - /// </typeparam> - /// <param name="items">The items to project.</param> - /// <param name="projector">A Func with which to project each item in the enumerable.</param> - /// <returns>An iterator to transform this enumerable.</returns> [DebuggerStepThrough] - public static IEnumerable<TResult> Project<TItem, TResult>(this IEnumerable<TItem> items, Func<TItem, int, TResult> projector) + internal static IEnumerable<TResult> Project<TItem, TResult>(this IEnumerable<TItem> items, Func<TItem, int, TResult> projector) { var index = 0; @@ -64,10 +60,16 @@ public static IEnumerable<TResult> Project<TItem, TResult>(this IEnumerable<TIte /// </returns> [DebuggerStepThrough] public static IEnumerable<TItem> Filter<TItem>(this IEnumerable<TItem> items, Func<TItem, bool> predicate) + => Filter(items, predicate, (p, item) => p.Invoke(item)); + + internal static IEnumerable<TItem> Filter<TItem, TArg>( + this IEnumerable<TItem> items, + TArg argument, + Func<TArg, TItem, bool> predicate) { foreach (var item in items) { - if (predicate.Invoke(item)) + if (predicate.Invoke(argument, item)) { yield return item; } diff --git a/AgileMapper/MappingDataExtensions.cs b/AgileMapper/MappingDataExtensions.cs index 4b369be75..28b01cdde 100644 --- a/AgileMapper/MappingDataExtensions.cs +++ b/AgileMapper/MappingDataExtensions.cs @@ -1,14 +1,11 @@ namespace AgileObjects.AgileMapper { -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif + using System.Collections.Generic; using DataSources; using Extensions.Internal; using Members; using ObjectPopulation; + using ObjectPopulation.ComplexTypes; internal static class MappingDataExtensions { @@ -16,7 +13,16 @@ public static bool IsStandalone(this IObjectMappingData mappingData) => mappingData.IsRoot || mappingData.MappingTypes.RuntimeTypesNeeded; public static bool IsTargetConstructable(this IObjectMappingData mappingData) - => mappingData.GetTargetObjectCreation() != null; + => GetTargetObjectCreationInfos(mappingData).Any(); + + public static IList<IBasicConstructionInfo> GetTargetObjectCreationInfos(this IObjectMappingData mappingData) + { + return mappingData + .MapperData + .MapperContext + .ConstructionFactory + .GetTargetObjectCreationInfos(mappingData); + } public static bool IsConstructableFromToTargetDataSource(this IObjectMappingData mappingData) => mappingData.GetToTargetDataSourceOrNullForTargetType() != null; @@ -48,15 +54,6 @@ public static IConfiguredDataSource GetToTargetDataSourceOrNullForTargetType(thi return null; } - public static Expression GetTargetObjectCreation(this IObjectMappingData mappingData) - { - return mappingData - .MapperData - .MapperContext - .ConstructionFactory - .GetNewObjectCreation(mappingData); - } - public static bool HasSameTypedConfiguredDataSource(this IObjectMappingData mappingData) { return diff --git a/AgileMapper/MappingRuleSet.cs b/AgileMapper/MappingRuleSet.cs index a84ac6611..eda3bfb17 100644 --- a/AgileMapper/MappingRuleSet.cs +++ b/AgileMapper/MappingRuleSet.cs @@ -1,16 +1,16 @@ namespace AgileObjects.AgileMapper { - using DataSources; - using Extensions.Internal; - using Members.Population; - using ObjectPopulation.Enumerables; - using ObjectPopulation.MapperKeys; - using ObjectPopulation.RepeatedMappings; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using DataSources.Factories; + using Extensions.Internal; + using Members.Population; + using ObjectPopulation.Enumerables; + using ObjectPopulation.MapperKeys; + using ObjectPopulation.RepeatedMappings; internal class MappingRuleSet { @@ -19,35 +19,40 @@ internal class MappingRuleSet public MappingRuleSet( string name, MappingRuleSetSettings settings, - IEnumerablePopulationStrategy enumerablePopulationStrategy, + EnumerablePopulationStrategy enumerablePopulationStrategy, IRepeatMappingStrategy repeatMappingStrategy, - IMemberPopulationFactory populationFactory, - IDataSourceFactory fallbackDataSourceFactory, - IRootMapperKeyFactory rootMapperKeyFactory) + PopulationGuardFactory populationGuardFactory, + FallbackDataSourceFactory fallbackDataSourceFactory, + RootMapperKeyFactory rootMapperKeyFactory) + : this(name) { - Name = name; Settings = settings; EnumerablePopulationStrategy = enumerablePopulationStrategy; RepeatMappingStrategy = repeatMappingStrategy; - PopulationFactory = populationFactory; + PopulationGuardFactory = populationGuardFactory; FallbackDataSourceFactory = fallbackDataSourceFactory; RootMapperKeyFactory = rootMapperKeyFactory; } + public MappingRuleSet(string name) + { + Name = name; + } + public string Name { get; } public Expression NameConstant => _nameConstant ?? (_nameConstant = Name.ToConstantExpression()); public MappingRuleSetSettings Settings { get; } - public IEnumerablePopulationStrategy EnumerablePopulationStrategy { get; } + public EnumerablePopulationStrategy EnumerablePopulationStrategy { get; } public IRepeatMappingStrategy RepeatMappingStrategy { get; } - public IMemberPopulationFactory PopulationFactory { get; } + public PopulationGuardFactory PopulationGuardFactory { get; } - public IDataSourceFactory FallbackDataSourceFactory { get; } + public FallbackDataSourceFactory FallbackDataSourceFactory { get; } - public IRootMapperKeyFactory RootMapperKeyFactory { get; } + public RootMapperKeyFactory RootMapperKeyFactory { get; } } } \ No newline at end of file diff --git a/AgileMapper/MappingRuleSetCollection.cs b/AgileMapper/MappingRuleSetCollection.cs index 8e5baa4da..28dd37d60 100644 --- a/AgileMapper/MappingRuleSetCollection.cs +++ b/AgileMapper/MappingRuleSetCollection.cs @@ -1,7 +1,7 @@ namespace AgileObjects.AgileMapper { using System.Collections.Generic; - using DataSources; + using DataSources.Factories; using Extensions.Internal; using Members.Population; using ObjectPopulation.Enumerables; @@ -17,20 +17,20 @@ internal class MappingRuleSetCollection private static readonly MappingRuleSet _createNew = new MappingRuleSet( Constants.CreateNew, MappingRuleSetSettings.ForInMemoryMapping(allowCloneEntityKeyMapping: true), - default(CopySourceEnumerablePopulationStrategy), + CopySourceEnumerablePopulationStrategy.Create, default(MapRepeatedCallRepeatMappingStrategy), - DefaultMemberPopulationFactory.Instance, - default(ExistingOrDefaultValueDataSourceFactory), - default(RootMapperKeyFactory)); + NullMemberPopulationGuardFactory.Create, + ExistingOrDefaultValueFallbackDataSourceFactory.Create, + DefaultRootMapperKeyFactory.Create); private static readonly MappingRuleSet _overwrite = new MappingRuleSet( Constants.Overwrite, MappingRuleSetSettings.ForInMemoryMapping(rootHasPopulatedTarget: true), - default(OverwriteEnumerablePopulationStrategy), + OverwriteEnumerablePopulationStrategy.Create, default(MapRepeatedCallRepeatMappingStrategy), - DefaultMemberPopulationFactory.Instance, - default(DefaultValueDataSourceFactory), - default(RootMapperKeyFactory)); + NullMemberPopulationGuardFactory.Create, + DefaultValueFallbackDataSourceFactory.Create, + DefaultRootMapperKeyFactory.Create); private static readonly MappingRuleSet _project = new MappingRuleSet( Constants.Project, @@ -38,25 +38,27 @@ internal class MappingRuleSetCollection { UseMemberInitialisation = true, UseSingleRootMappingExpression = true, + AllowEntityKeyMapping = true, AllowCloneEntityKeyMapping = true, + AllowGuardedBindings = true, GuardAccessTo = value => value.Type.IsComplex(), ExpressionIsSupported = value => value.CanBeProjected(), AllowEnumerableAssignment = true }, - default(ProjectSourceEnumerablePopulationStrategy), + ProjectSourceEnumerablePopulationStrategy.Create, default(MapToDepthRepeatMappingStrategy), - DefaultMemberPopulationFactory.Instance, - default(DefaultValueDataSourceFactory), - default(QueryProjectorMapperKeyFactory)); + NullMemberPopulationGuardFactory.Create, + DefaultValueFallbackDataSourceFactory.Create, + QueryProjectorMapperKeyFactory.Create); private static readonly MappingRuleSet _merge = new MappingRuleSet( Constants.Merge, - MappingRuleSetSettings.ForInMemoryMapping(rootHasPopulatedTarget: true), - default(MergeEnumerablePopulationStrategy), + MappingRuleSetSettings.ForInMemoryMapping(rootHasPopulatedTarget: true, allowGuardedBindings: false), + MergeEnumerablePopulationStrategy.Create, default(MapRepeatedCallRepeatMappingStrategy), - new MemberMergePopulationFactory(), - default(ExistingOrDefaultValueDataSourceFactory), - default(RootMapperKeyFactory)); + MemberMergePopulationGuardFactory.Create, + ExistingOrDefaultValueFallbackDataSourceFactory.Create, + DefaultRootMapperKeyFactory.Create); public static readonly MappingRuleSetCollection Default = new MappingRuleSetCollection(_createNew, _overwrite, _project, _merge); @@ -78,6 +80,6 @@ public MappingRuleSetCollection(params MappingRuleSet[] ruleSets) public MappingRuleSet Project => _project; - public MappingRuleSet GetByName(string name) => All.First(rs => rs.Name == name); + public MappingRuleSet GetByName(string name) => All.First(name, (n, rs) => rs.Name == n); } } \ No newline at end of file diff --git a/AgileMapper/MappingRuleSetSettings.cs b/AgileMapper/MappingRuleSetSettings.cs index d21b3775f..45d62c50a 100644 --- a/AgileMapper/MappingRuleSetSettings.cs +++ b/AgileMapper/MappingRuleSetSettings.cs @@ -11,6 +11,7 @@ internal class MappingRuleSetSettings { public static MappingRuleSetSettings ForInMemoryMapping( bool rootHasPopulatedTarget = false, + bool allowGuardedBindings = true, bool allowCloneEntityKeyMapping = false) { return new MappingRuleSetSettings @@ -20,6 +21,7 @@ public static MappingRuleSetSettings ForInMemoryMapping( SourceElementsCouldBeNull = true, UseTryCatch = true, CheckDerivedSourceTypes = true, + AllowGuardedBindings = allowGuardedBindings, AllowCloneEntityKeyMapping = allowCloneEntityKeyMapping, GuardAccessTo = value => true, ExpressionIsSupported = value => true, @@ -44,6 +46,10 @@ public static MappingRuleSetSettings ForInMemoryMapping( public bool CheckDerivedSourceTypes { get; set; } + public bool AllowGuardedBindings { get; set; } + + public bool AllowEntityKeyMapping { get; set; } + public bool AllowCloneEntityKeyMapping { get; set; } public Func<Expression, bool> GuardAccessTo { get; set; } diff --git a/AgileMapper/Members/BasicMapperData.cs b/AgileMapper/Members/BasicMapperData.cs index 37f9b1192..6f8ac1007 100644 --- a/AgileMapper/Members/BasicMapperData.cs +++ b/AgileMapper/Members/BasicMapperData.cs @@ -5,7 +5,20 @@ namespace AgileObjects.AgileMapper.Members internal class BasicMapperData : IBasicMapperData { private readonly IBasicMapperData _parent; - private readonly IQualifiedMember _sourceMember; + + public BasicMapperData( + IQualifiedMember sourceMember, + QualifiedMember targetMember, + IBasicMapperData parent) + : this( + parent.RuleSet, + parent.SourceType, + parent.TargetType, + sourceMember, + targetMember, + parent) + { + } public BasicMapperData( MappingRuleSet ruleSet, @@ -13,10 +26,10 @@ public BasicMapperData( Type targetType, IQualifiedMember sourceMember, QualifiedMember targetMember, - IBasicMapperData parent = null) + IBasicMapperData parent) : this(ruleSet, sourceType, targetType, targetMember, parent) { - _sourceMember = sourceMember; + SourceMember = sourceMember; } public BasicMapperData( @@ -44,9 +57,11 @@ public BasicMapperData( public Type TargetType { get; } + public IQualifiedMember SourceMember { get; } + public QualifiedMember TargetMember { get; } public virtual bool HasCompatibleTypes(ITypePair typePair) - => typePair.HasCompatibleTypes(this, _sourceMember, TargetMember); + => typePair.HasCompatibleTypes(this, SourceMember, TargetMember); } } \ No newline at end of file diff --git a/AgileMapper/Members/ChildMemberMapperData.cs b/AgileMapper/Members/ChildMemberMapperData.cs index 083fb5d88..b486c87f7 100644 --- a/AgileMapper/Members/ChildMemberMapperData.cs +++ b/AgileMapper/Members/ChildMemberMapperData.cs @@ -13,16 +13,9 @@ internal class ChildMemberMapperData : BasicMapperData, IMemberMapperData private bool? _isRepeatMapping; public ChildMemberMapperData(QualifiedMember targetMember, ObjectMapperData parent) - : base( - parent.RuleSet, - parent.SourceType, - parent.TargetType, - parent.SourceMember, - targetMember, - parent) + : base(parent.SourceMember, targetMember, parent) { _useParentForTypeCheck = true; - SourceMember = parent.SourceMember; Parent = parent; Context = new MapperDataContext(this); } @@ -36,7 +29,6 @@ public ChildMemberMapperData(IQualifiedMember sourceMember, QualifiedMember targ targetMember, parent) { - SourceMember = sourceMember; Parent = parent; Context = new MapperDataContext(this); } @@ -55,8 +47,6 @@ public ChildMemberMapperData(IQualifiedMember sourceMember, QualifiedMember targ public ParameterExpression MappingDataObject => Parent.MappingDataObject; - public IQualifiedMember SourceMember { get; } - public Expression SourceObject => Parent.SourceObject; public Expression TargetObject => Parent.TargetObject; diff --git a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs index 546aeebad..1768dc7eb 100644 --- a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs @@ -1,7 +1,9 @@ namespace AgileObjects.AgileMapper.Members.Dictionaries { using System; + using System.Collections.Generic; using System.Dynamic; + using System.Linq; using Caching; using Extensions; using Extensions.Internal; @@ -259,11 +261,7 @@ public override Expression GetPopulation(Expression value, IMemberMapperData map } var keyedAccess = GetKeyedAccess(mapperData); - - var convertedValue = - GetCheckedValueOrNull(value, keyedAccess, mapperData) ?? - mapperData.GetValueConversion(value, ValueType); - + var convertedValue = mapperData.GetValueConversion(value, ValueType); var keyedAssignment = keyedAccess.AssignTo(convertedValue); return keyedAssignment; @@ -273,67 +271,68 @@ private bool ValueIsFlattening(Expression value, out Expression flattening) { if (HasObjectEntries || HasSimpleEntries) { - return value.TryGetMappingBody(out flattening); + return TryGetMappingBody(value, out flattening); } flattening = null; return false; } - private Expression GetCheckedValueOrNull(Expression value, Expression keyedAccess, IMemberMapperData mapperData) + private static bool TryGetMappingBody(Expression value, out Expression mapping) { - if (HasSimpleEntries) + if (value.NodeType == Try) { - return null; + value = ((TryExpression)value).Body; } - if ((value.NodeType != Block) && (value.NodeType != Try) || mapperData.TargetIsDefinitelyUnpopulated()) + if ((value.NodeType != Block)) { - return mapperData.SourceMember.IsEnumerable ? value.GetConversionTo(ValueType) : null; + mapping = null; + return false; } - var checkedAccess = GetAccessChecked(mapperData); - var existingValue = checkedAccess.Variables.First(); + var mappingBlock = (BlockExpression)value; - if (value.NodeType == Try) + if (mappingBlock.Expressions.HasOne()) + { + mapping = null; + return false; + } + + var mappingExpressions = GetMappingExpressions(mappingBlock); + + if (mappingExpressions.HasOne() && + (mappingExpressions[0].NodeType == Block)) { - return GetCheckedTryCatch((TryExpression)value, keyedAccess, checkedAccess, existingValue); + IList<ParameterExpression> mappingVariables = mappingBlock.Variables; + mappingBlock = (BlockExpression)mappingExpressions[0]; + mappingVariables = mappingVariables.Append(mappingBlock.Variables); + mapping = mappingBlock.Update(mappingVariables, mappingBlock.Expressions); + return true; } - var checkedValue = ((BlockExpression)value).Replace(keyedAccess, existingValue); + mapping = mappingBlock.Variables.Any() + ? Expression.Block(mappingBlock.Variables, mappingExpressions) + : mappingExpressions.HasOne() + ? mappingExpressions[0] + : Expression.Block(mappingExpressions); - return checkedValue.Update( - checkedValue.Variables.Append(existingValue), - checkedValue.Expressions.Prepend(checkedAccess.Expressions.First())); + return true; } - private static Expression GetCheckedTryCatch( - TryExpression tryCatchValue, - Expression keyedAccess, - Expression checkedAccess, - ParameterExpression existingValue) + private static IList<Expression> GetMappingExpressions(Expression mapping) { - var existingValueOrDefault = Expression.Condition( - checkedAccess, - existingValue, - existingValue.Type.ToDefaultExpression()); - - var replacements = new ExpressionReplacementDictionary(1) { [keyedAccess] = existingValueOrDefault }; - - var updatedCatchHandlers = tryCatchValue - .Handlers - .ProjectToArray(handler => handler.Update( - handler.Variable, - handler.Filter.Replace(replacements), - handler.Body.Replace(replacements))); - - var updatedTryCatch = tryCatchValue.Update( - tryCatchValue.Body, - updatedCatchHandlers, - tryCatchValue.Finally, - tryCatchValue.Fault); - - return Expression.Block(new[] { existingValue }, updatedTryCatch); + var expressions = new List<Expression>(); + + while (mapping.NodeType == Block) + { + var mappingBlock = (BlockExpression)mapping; + + expressions.AddRange(mappingBlock.Expressions.Take(mappingBlock.Expressions.Count - 1)); + mapping = mappingBlock.Result; + } + + return expressions; } public DictionaryTargetMember WithTypeOf(Member sourceMember) @@ -362,7 +361,7 @@ private bool CreateNonDictionaryChildMembers(Type sourceType) // entry and we're mapping from a source of type object, we switch from // mapping to flattened entries to mapping entire objects: return HasObjectEntries && - LeafMember.IsEnumerableElement() && + this.IsEnumerableElement() && (MemberChain[Depth - 2] == _rootDictionaryMember.LeafMember) && (sourceType == typeof(object)); } diff --git a/AgileMapper/Members/ExpressionInfoFinder.cs b/AgileMapper/Members/ExpressionInfoFinder.cs index 753acc74f..05a27e2a6 100644 --- a/AgileMapper/Members/ExpressionInfoFinder.cs +++ b/AgileMapper/Members/ExpressionInfoFinder.cs @@ -3,11 +3,6 @@ namespace AgileObjects.AgileMapper.Members using System; using System.Collections.Generic; using System.Linq; - using Extensions; - using Extensions.Internal; - using NetStandardPolyfills; - using ReadableExpressions.Extensions; - using TypeConversion; #if NET35 using Microsoft.Scripting.Ast; using static Microsoft.Scripting.Ast.ExpressionType; @@ -15,6 +10,10 @@ namespace AgileObjects.AgileMapper.Members using System.Linq.Expressions; using static System.Linq.Expressions.ExpressionType; #endif + using Extensions; + using Extensions.Internal; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; using static Member; internal class ExpressionInfoFinder @@ -34,9 +33,18 @@ public ExpressionInfoFinder(Expression mappingDataObject) _mappingDataObject = mappingDataObject; } - public ExpressionInfo FindIn(Expression expression, bool targetCanBeNull) + public ExpressionInfo FindIn( + Expression expression, + bool targetCanBeNull = false, + bool checkMultiInvocations = true, + bool invertNestedAccessChecks = false) { - var finder = new ExpressionInfoFinderInstance(_mappingDataObject, targetCanBeNull); + var finder = new ExpressionInfoFinderInstance( + _mappingDataObject, + targetCanBeNull, + checkMultiInvocations, + invertNestedAccessChecks); + var info = finder.FindIn(expression); return info; @@ -46,21 +54,35 @@ private class ExpressionInfoFinderInstance : ExpressionVisitor { private readonly Expression _mappingDataObject; private readonly bool _includeTargetNullChecking; - private readonly ICollection<Expression> _stringMemberAccessSubjects; - private readonly ICollection<string> _nullCheckSubjects; - private readonly Dictionary<string, Expression> _nestedAccessesByPath; + private readonly bool _checkMultiInvocations; + private readonly bool _invertNestedAccessChecks; + private ICollection<Expression> _stringMemberAccessSubjects; + private ICollection<string> _nullCheckSubjects; + private Dictionary<string, Expression> _nestedAccessesByPath; private ICollection<Expression> _allInvocations; private ICollection<Expression> _multiInvocations; - public ExpressionInfoFinderInstance(Expression mappingDataObject, bool targetCanBeNull) + public ExpressionInfoFinderInstance( + Expression mappingDataObject, + bool targetCanBeNull, + bool checkMultiInvocations, + bool invertNestedAccessChecks) { _mappingDataObject = mappingDataObject; _includeTargetNullChecking = targetCanBeNull; - _stringMemberAccessSubjects = new List<Expression>(); - _nullCheckSubjects = new List<string>(); - _nestedAccessesByPath = new Dictionary<string, Expression>(); + _checkMultiInvocations = checkMultiInvocations; + _invertNestedAccessChecks = invertNestedAccessChecks; } + private ICollection<Expression> StringMemberAccessSubjects + => _stringMemberAccessSubjects ?? (_stringMemberAccessSubjects = new List<Expression>()); + + private ICollection<string> NullCheckSubjects + => _nullCheckSubjects ?? (_nullCheckSubjects = new List<string>()); + + private Dictionary<string, Expression> NestedAccessesByPath + => _nestedAccessesByPath ?? (_nestedAccessesByPath = new Dictionary<string, Expression>()); + private ICollection<Expression> AllInvocations => _allInvocations ?? (_allInvocations = new List<Expression>()); @@ -71,14 +93,14 @@ public ExpressionInfo FindIn(Expression expression) { Visit(expression); - if (_nestedAccessesByPath.None() && _multiInvocations.NoneOrNull()) + if ((_nestedAccessesByPath == null) && (_multiInvocations == null)) { return EmptyExpressionInfo; } var nestedAccessChecks = GetNestedAccessChecks(); - var multiInvocations = _multiInvocations?.Any() == true + var multiInvocations = _checkMultiInvocations && _multiInvocations?.Any() == true ? _multiInvocations.OrderBy(inv => inv.ToString()).ToArray() : Enumerable<Expression>.EmptyArray; @@ -87,41 +109,76 @@ public ExpressionInfo FindIn(Expression expression) private Expression GetNestedAccessChecks() { - if (_nestedAccessesByPath.None()) + if (_nestedAccessesByPath == null) { return null; } - return _nestedAccessesByPath - .Values - .Reverse() - .Project(GetAccessCheck) - .Aggregate( - default(Expression), - (accessChecksSoFar, accessCheck) => (accessChecksSoFar != null) - ? Expression.AndAlso(accessChecksSoFar, accessCheck) - : accessCheck); + var nestedAccessCount = _nestedAccessesByPath.Count; + + if (nestedAccessCount == 1) + { + return GetAccessCheck(_nestedAccessesByPath.Values.First()); + } + + var nestedAccessCheckChain = default(Expression); + + foreach (var nestedAccessCheck in _nestedAccessesByPath.Values.Reverse().Project(GetAccessCheck)) + { + if (nestedAccessCheckChain == null) + { + nestedAccessCheckChain = nestedAccessCheck; + continue; + } + + nestedAccessCheckChain = _invertNestedAccessChecks + ? Expression.OrElse(nestedAccessCheckChain, nestedAccessCheck) + : Expression.AndAlso(nestedAccessCheckChain, nestedAccessCheck); + } + + return nestedAccessCheckChain; } - private static Expression GetAccessCheck(Expression access) + private Expression GetAccessCheck(Expression access) { - Expression count; - switch (access.NodeType) { case ArrayIndex: - count = Expression.ArrayLength(((BinaryExpression)access).Left); - break; + var arrayIndexAccess = (BinaryExpression)access; + var arrayLength = Expression.ArrayLength(arrayIndexAccess.Left); + var arrayIndexValue = arrayIndexAccess.Right; + + return _invertNestedAccessChecks + ? Expression.Equal(arrayLength, arrayIndexValue) + : Expression.GreaterThan(arrayLength, arrayIndexValue); case Index: - count = ((IndexExpression)access).Object.GetCount(); - break; + var index = (IndexExpression)access; + var indexKeyType = index.Indexer.GetGetter().GetParameters().First().ParameterType; + + if (!indexKeyType.IsNumeric()) + { + goto default; + } + + var count = index.Object.GetCount(); + + if (count == null) + { + goto default; + } + + var indexValue = index.Arguments.First().GetConversionTo(count.Type); + + return _invertNestedAccessChecks + ? Expression.LessThanOrEqual(count, indexValue) + : Expression.GreaterThan(count, indexValue); default: - return access.GetIsNotDefaultComparison(); + return _invertNestedAccessChecks + ? access.GetIsDefaultComparison() + : access.GetIsNotDefaultComparison(); } - - return Expression.GreaterThan(count, ToNumericConverter<int>.Zero); } protected override Expression VisitBinary(BinaryExpression binary) @@ -172,7 +229,7 @@ private static bool IsRelevantArrayIndexAccess(BinaryExpression binary) protected override Expression VisitMember(MemberExpression memberAccess) { - if (IsRootObject(memberAccess)) + if ((memberAccess.Expression == null) || IsRootObject(memberAccess)) { return base.VisitMember(memberAccess); } @@ -188,33 +245,34 @@ protected override Expression VisitMember(MemberExpression memberAccess) return base.VisitMember(memberAccess); } - private bool IsRootObject(MemberExpression memberAccess) => !IsNotRootObject(memberAccess); + private bool IsNotRootObject(MemberExpression memberAccess) => !IsRootObject(memberAccess); - private bool IsNotRootObject(MemberExpression memberAccess) + private bool IsRootObject(MemberExpression memberAccess) { if (memberAccess.Member.Name == nameof(IMappingData.Parent)) { // ReSharper disable once PossibleNullReferenceException - return !memberAccess.Member.DeclaringType.Name + return memberAccess.Member.DeclaringType.Name .StartsWith(nameof(IMappingData), StringComparison.Ordinal); } if (memberAccess.Expression != _mappingDataObject) - { - return true; - } - - if (memberAccess.Member.Name == nameof(IMappingData<int, int>.EnumerableIndex)) { return false; } - if (memberAccess.Member.Name == RootSourceMemberName) + switch (memberAccess.Member.Name) { - return false; - } + case nameof(IMappingData<int, int>.EnumerableIndex): + case RootSourceMemberName: + return true; - return _includeTargetNullChecking || (memberAccess.Member.Name != RootTargetMemberName); + case RootTargetMemberName: + return _includeTargetNullChecking; + + default: + return false; + } } protected override Expression VisitIndex(IndexExpression indexAccess) @@ -280,14 +338,14 @@ private static bool IsNullableGetValueOrDefaultCall(MethodCallExpression methodC private void AddExistingNullCheck(Expression checkedAccess) { - _nullCheckSubjects.Add(checkedAccess.ToString()); + NullCheckSubjects.Add(checkedAccess.ToString()); } private void AddStringMemberAccessSubjectIfAppropriate(Expression member) { if ((member?.Type == typeof(string)) && AccessSubjectCouldBeNull(member)) { - _stringMemberAccessSubjects.Add(member); + StringMemberAccessSubjects.Add(member); } } @@ -317,8 +375,7 @@ private bool AccessSubjectCouldBeNull(Expression expression) return false; } - return (expression.NodeType != MemberAccess) || - IsNotRootObject(memberAccess); + return (expression.NodeType != MemberAccess) || IsNotRootObject(memberAccess); } protected override Expression VisitInvocation(InvocationExpression invocation) @@ -330,6 +387,11 @@ protected override Expression VisitInvocation(InvocationExpression invocation) private void AddInvocationIfNecessary(Expression invocation) { + if (!_checkMultiInvocations) + { + return; + } + if (_allInvocations?.Contains(invocation) != true) { AllInvocations.Add(invocation); @@ -361,7 +423,8 @@ private bool GuardMemberAccess(Expression memberAccess) return false; } - if ((memberAccess.Type == typeof(string)) && !_stringMemberAccessSubjects.Contains(memberAccess)) + if ((memberAccess.Type == typeof(string)) && + (_stringMemberAccessSubjects?.Contains(memberAccess) != true)) { return false; } @@ -406,13 +469,13 @@ private void AddMemberAccess(Expression memberAccess) { var memberAccessString = memberAccess.ToString(); - if (_nullCheckSubjects.Contains(memberAccessString) || - _nestedAccessesByPath.ContainsKey(memberAccessString)) + if (_nullCheckSubjects?.Contains(memberAccessString) == true || + _nestedAccessesByPath?.ContainsKey(memberAccessString) == true) { return; } - _nestedAccessesByPath.Add(memberAccessString, memberAccess); + NestedAccessesByPath.Add(memberAccessString, memberAccess); } } diff --git a/AgileMapper/Members/IBasicMapperData.cs b/AgileMapper/Members/IBasicMapperData.cs index c71cd0add..66c72372d 100644 --- a/AgileMapper/Members/IBasicMapperData.cs +++ b/AgileMapper/Members/IBasicMapperData.cs @@ -8,6 +8,8 @@ internal interface IBasicMapperData : ITypePair IBasicMapperData Parent { get; } + IQualifiedMember SourceMember { get; } + QualifiedMember TargetMember { get; } bool HasCompatibleTypes(ITypePair typePair); diff --git a/AgileMapper/Members/IMemberMapperData.cs b/AgileMapper/Members/IMemberMapperData.cs index 207e38684..a31dfcb88 100644 --- a/AgileMapper/Members/IMemberMapperData.cs +++ b/AgileMapper/Members/IMemberMapperData.cs @@ -21,8 +21,6 @@ internal interface IMemberMapperData : IBasicMapperData ParameterExpression MappingDataObject { get; } - IQualifiedMember SourceMember { get; } - Expression SourceObject { get; } Expression TargetObject { get; } diff --git a/AgileMapper/Members/Member.cs b/AgileMapper/Members/Member.cs index eb70b5d06..a575ab8d2 100644 --- a/AgileMapper/Members/Member.cs +++ b/AgileMapper/Members/Member.cs @@ -257,7 +257,7 @@ private static class TargetMemberCache<T> public static readonly Member MemberInstance = RootTarget(typeof(T)); } - public bool Equals(Member otherMember) => otherMember._hashCode == _hashCode; + public bool Equals(Member otherMember) => otherMember?._hashCode == _hashCode; public override int GetHashCode() => _hashCode; diff --git a/AgileMapper/Members/MemberCache.cs b/AgileMapper/Members/MemberCache.cs index 980ce4ee1..2be10b356 100644 --- a/AgileMapper/Members/MemberCache.cs +++ b/AgileMapper/Members/MemberCache.cs @@ -33,7 +33,7 @@ public IList<Member> GetSourceMembers(Type sourceType) var properties = GetProperties(key.Type, OnlyGettable); var methods = GetMethods(key.Type, OnlyRelevantCallable, Member.GetMethod); - return GetMembers(fields, properties, methods); + return GetAllMembers(key.Type, GetSourceMembers, fields, properties, methods); }); } @@ -58,14 +58,14 @@ public IList<Member> GetTargetMembers(Type targetType) var fieldsAndProperties = fields .Concat(properties) - .Project(m => + .Project(constructorParameterNames, (cpns, m) => { - m.HasMatchingCtorParameter = constructorParameterNames.Contains(m.Name, OrdinalIgnoreCase); + m.HasMatchingCtorParameter = cpns.Contains(m.Name, OrdinalIgnoreCase); return m; }) .ToArray(); - return GetMembers(fieldsAndProperties, methods); + return GetAllMembers(key.Type, GetTargetMembers, fieldsAndProperties, methods); }); } @@ -131,13 +131,38 @@ private static bool OnlyCallableSetters(MethodInfo method) #endregion - private static IList<Member> GetMembers(params IEnumerable<Member>[] members) + private static IList<Member> GetAllMembers( + Type memberType, + Func<Type, IList<Member>> membersFactory, + params IEnumerable<Member>[] members) { - var allMembers = members - .SelectMany(m => m) - .ToArray(); + if (!memberType.IsInterface()) + { + return GetMembers(members); + } + + var interfaceTypes = memberType.GetAllInterfaces(); + + if (interfaceTypes.Length == 0) + { + return GetMembers(members); + } + + var membersCount = members.Length; + var interfaceCount = interfaceTypes.Length; - return allMembers; + var allMembers = new IEnumerable<Member>[membersCount + interfaceCount]; + allMembers.CopyFrom(members); + + for (var i = 0; i < interfaceCount; ++i) + { + allMembers[i + membersCount] = membersFactory.Invoke(interfaceTypes[i]); + } + + return GetMembers(allMembers); } + + private static IList<Member> GetMembers(params IEnumerable<Member>[] members) + => members.SelectMany(m => m).ToArray(); } } diff --git a/AgileMapper/Members/MemberExtensions.cs b/AgileMapper/Members/MemberExtensions.cs index a2ef0e3a8..2e0a71d5e 100644 --- a/AgileMapper/Members/MemberExtensions.cs +++ b/AgileMapper/Members/MemberExtensions.cs @@ -5,6 +5,12 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; + using LinqExp = System.Linq.Expressions; +#else + using System.Linq.Expressions; +#endif using System.Reflection; using Configuration; using Extensions; @@ -13,12 +19,6 @@ using ObjectPopulation; using ReadableExpressions; using ReadableExpressions.Extensions; -#if NET35 - using Microsoft.Scripting.Ast; - using LinqExp = System.Linq.Expressions; -#else - using System.Linq.Expressions; -#endif using static System.StringComparison; using static Constants; using static Member; @@ -77,13 +77,6 @@ public static string GetFriendlyMemberPath( public static bool IsUnmappable(this QualifiedMember member, out string reason) { - if (member.Depth < 2) - { - // Either the root member, QualifiedMember.All or QualifiedMember.None: - reason = null; - return false; - } - if (IsStructNonSimpleMember(member)) { reason = member.Type.GetFriendlyName() + " member on a struct"; @@ -148,9 +141,16 @@ public static Expression GetQualifiedAccess(this IEnumerable<Member> memberChain (accessSoFar, member) => member.GetAccess(accessSoFar)); } + [DebuggerStepThrough] + public static bool IsEnumerableElement(this QualifiedMember member) => member.LeafMember.IsEnumerableElement(); + [DebuggerStepThrough] public static bool IsEnumerableElement(this Member member) => member.MemberType == MemberType.EnumerableElement; + [DebuggerStepThrough] + public static bool IsConstructorParameter(this QualifiedMember member) + => member.LeafMember.MemberType == MemberType.ConstructorParameter; + public static IList<string> ExtendWith( this ICollection<string> parentJoinedNames, string[] memberMatchingNames, @@ -168,8 +168,8 @@ public static bool CouldMatch(this IList<string> memberNames, IList<string> othe } return otherMemberNames - .Any(otherJoinedName => (otherJoinedName == RootMemberName) || memberNames - .Any(joinedName => (joinedName == RootMemberName) || otherJoinedName.StartsWithIgnoreCase(joinedName))); + .Any(memberNames, (mns, otherJoinedName) => (otherJoinedName == RootMemberName) || mns + .Any(otherJoinedName, (ojn, joinedName) => (joinedName == RootMemberName) || ojn.StartsWithIgnoreCase(joinedName))); } public static bool Match(this ICollection<string> memberNames, ICollection<string> otherMemberNames) @@ -281,6 +281,30 @@ public static QualifiedMember ToSourceMember( mapperContext); } + public static QualifiedMember ToSourceMemberOrNull( + this LambdaExpression memberAccess, + MapperContext mapperContext, + out string failureReason) + { + var hasUnsupportedNodeType = false; + var sourceMember = memberAccess.ToSourceMember(mapperContext, nt => hasUnsupportedNodeType = true); + + if (hasUnsupportedNodeType) + { + failureReason = $"Unable to determine source member from '{memberAccess.Body.ToReadableString()}'"; + return null; + } + + if (sourceMember == null) + { + failureReason = $"Source member {memberAccess.Body.ToReadableString()} is not readable"; + return null; + } + + failureReason = null; + return sourceMember; + } + public static QualifiedMember ToTargetMemberOrNull( this LambdaExpression memberAccess, Type targetType, @@ -371,7 +395,7 @@ private static QualifiedMember CreateMember( var memberAccess = memberAccesses[i++]; var memberName = GetMemberName(memberAccess); var members = membersFactory.Invoke(parentMember.Type); - var member = members.FirstOrDefault(m => m.Name == memberName); + var member = members.FirstOrDefault(memberName, (mn, m) => m.Name == mn); if (member == null) { diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 6751fdb26..e61a1fa90 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -4,20 +4,23 @@ namespace AgileObjects.AgileMapper.Members using System.Collections.Generic; using System.Diagnostics; using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using System.Reflection; using Configuration; + using Configuration.MemberIgnores.SourceValueFilters; using DataSources; + using DataSources.Factories; using Dictionaries; using Extensions; using Extensions.Internal; using NetStandardPolyfills; using ObjectPopulation; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif using static Member; + using static System.StringComparison; internal static class MemberMapperDataExtensions { @@ -26,7 +29,10 @@ public static bool TargetTypeIsEntity(this IMemberMapperData mapperData) public static bool IsEntity(this IMemberMapperData mapperData, Type type, out Member idMember) { - if (type == null) + if ((type == null) || + type.Name.EndsWith("ViewModel", Ordinal) || + type.Name.EndsWith("Dto", Ordinal) || + type.Name.EndsWith("DataTransferObject", Ordinal)) { idMember = null; return false; @@ -182,36 +188,11 @@ public static DictionarySourceMember GetDictionarySourceMemberOrNull(this IMembe public static void RegisterTargetMemberDataSourcesIfRequired( this IMemberMapperData mapperData, - DataSourceSet dataSources) + IDataSourceSet dataSources) { mapperData.Parent.DataSourcesByTargetMember.Add(mapperData.TargetMember, dataSources); } - public static bool TargetMemberIsUnmappable(this IMemberMapperData mapperData, out string reason) - { - if (!mapperData.RuleSet.Settings.AllowSetMethods && - (mapperData.TargetMember.LeafMember.MemberType == MemberType.SetMethod)) - { - reason = "Set methods are unsupported by rule set '" + mapperData.RuleSet.Name + "'"; - return true; - } - - if (mapperData.TargetMember.LeafMember.HasMatchingCtorParameter && - ((mapperData.Parent?.IsRoot != true) || - !mapperData.RuleSet.Settings.RootHasPopulatedTarget)) - { - reason = "Expected to be populated by constructor parameter"; - return true; - } - - return TargetMemberIsUnmappable( - mapperData, - mapperData.TargetMember, - md => md.MapperContext.UserConfigurations.QueryDataSourceFactories(md), - mapperData.MapperContext.UserConfigurations, - out reason); - } - public static bool TargetMemberIsUnmappable<TTMapperData>( this TTMapperData mapperData, QualifiedMember targetMember, @@ -235,8 +216,9 @@ public static bool TargetMemberIsUnmappable<TTMapperData>( // If we're here: // 1. TargetMember is an Entity key - // 2. No configuration exists to allow Entity key Mapping - // 3. No configured data sources exist + // 2. The rule set doesn't allow entity key mapping + // 3. No configuration exists to allow Entity key Mapping + // 4. No configured data sources exist if (mapperData.RuleSet.Settings.AllowCloneEntityKeyMapping && (mapperData.SourceType == mapperData.TargetType)) @@ -250,7 +232,7 @@ public static bool TargetMemberIsUnmappable<TTMapperData>( [DebuggerStepThrough] public static bool TargetMemberIsEnumerableElement(this IBasicMapperData mapperData) - => mapperData.TargetMember.LeafMember.IsEnumerableElement(); + => mapperData.TargetMember.IsEnumerableElement(); [DebuggerStepThrough] public static bool TargetMemberHasInitAccessibleValue(this IMemberMapperData mapperData) @@ -303,7 +285,7 @@ private static bool TargetMemberHasRecursiveObjectGraph(QualifiedMember targetMe var nonSimpleChildMembers = GetTargetMembers(mappingType) .Filter(m => !m.IsSimple) - .Project(cm => GetNonEnumerableChildMember(targetMember, cm)) + .Project(targetMember, GetNonEnumerableChildMember) .ToArray(); if (nonSimpleChildMembers.None()) @@ -342,11 +324,11 @@ private static bool TargetMemberEverRepeatsWithin(QualifiedMember parentMember, } var sameTypedChildMembers = nonSimpleChildMembers - .Filter(cm => (cm.IsEnumerable ? cm.ElementType : cm.Type) == subjectMember.Type) + .Filter(subjectMember, (sm, cm) => (cm.IsEnumerable ? cm.ElementType : cm.Type) == sm.Type) .ToArray(); if (sameTypedChildMembers - .Project(cm => GetNonEnumerableChildMember(parentMember, cm)) + .Project(parentMember, GetNonEnumerableChildMember) .Any(cm => cm != subjectMember)) { return true; @@ -413,6 +395,9 @@ public static Expression GetFallbackCollectionValue(this IMemberMapperData mappe return emptyEnumerable.GetConversionTo(targetMember.Type); } + public static IList<ConfiguredSourceValueFilter> GetSourceValueFilters(this IMemberMapperData mapperData, Type sourceValueType) + => mapperData.MapperContext.UserConfigurations.GetSourceValueFilters(mapperData, sourceValueType); + public static bool CanConvert(this IMemberMapperData mapperData, Type sourceType, Type targetType) => mapperData.MapperContext.ValueConverters.CanConvert(sourceType, targetType); diff --git a/AgileMapper/Members/NamingSettings.cs b/AgileMapper/Members/NamingSettings.cs index e7a271d8c..03fb37cee 100644 --- a/AgileMapper/Members/NamingSettings.cs +++ b/AgileMapper/Members/NamingSettings.cs @@ -195,7 +195,7 @@ private bool IsIdentifier(Member member) potentialIds.InsertRange(0, new[] { "Id", "Identifier" }); return _customNameMatchers - .Project(customNameMatcher => customNameMatcher.Match(member.Name)) + .Project(member, (m, customNameMatcher) => customNameMatcher.Match(m.Name)) .Any(memberNameMatch => memberNameMatch.Success && potentialIds.Contains(GetMemberName(memberNameMatch))); diff --git a/AgileMapper/Members/Population/DefaultMemberPopulationFactory.cs b/AgileMapper/Members/Population/DefaultMemberPopulationFactory.cs deleted file mode 100644 index c1c8206e5..000000000 --- a/AgileMapper/Members/Population/DefaultMemberPopulationFactory.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ - using Extensions.Internal; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - - internal class DefaultMemberPopulationFactory : MemberPopulationFactoryBase - { - public static readonly IMemberPopulationFactory Instance = new DefaultMemberPopulationFactory(); - - protected override Expression GetPopulationGuard(IMemberPopulationContext context) - => context.PopulateCondition; - - protected override Expression GetGuardedBindingValue(Expression bindingValue, Expression populationGuard) - { - if (populationGuard == null) - { - return bindingValue; - } - - return Expression.Condition( - populationGuard, - bindingValue, - bindingValue.Type.ToDefaultExpression()); - } - - public override Expression GetGuardedPopulation( - Expression population, - Expression populationGuard, - bool useSingleExpression) - { - return useSingleExpression - ? population - : base.GetGuardedPopulation(population, populationGuard, false); - } - } -} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulationContext.cs b/AgileMapper/Members/Population/IMemberPopulationContext.cs deleted file mode 100644 index 277554294..000000000 --- a/AgileMapper/Members/Population/IMemberPopulationContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - using DataSources; - - internal interface IMemberPopulationContext - { - IMemberMapperData MapperData { get; } - - bool IsSuccessful { get; } - - DataSourceSet DataSources { get; } - - Expression PopulateCondition { get; } - } -} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulator.cs b/AgileMapper/Members/Population/IMemberPopulator.cs index ff8c9146b..aed10148c 100644 --- a/AgileMapper/Members/Population/IMemberPopulator.cs +++ b/AgileMapper/Members/Population/IMemberPopulator.cs @@ -12,6 +12,8 @@ internal interface IMemberPopulator bool CanPopulate { get; } + Expression PopulateCondition { get; } + Expression GetPopulation(); } } \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberMergePopulationFactory.cs b/AgileMapper/Members/Population/MemberMergePopulationGuardFactory.cs similarity index 74% rename from AgileMapper/Members/Population/MemberMergePopulationFactory.cs rename to AgileMapper/Members/Population/MemberMergePopulationGuardFactory.cs index f81f14429..a2e745b35 100644 --- a/AgileMapper/Members/Population/MemberMergePopulationFactory.cs +++ b/AgileMapper/Members/Population/MemberMergePopulationGuardFactory.cs @@ -6,12 +6,12 @@ namespace AgileObjects.AgileMapper.Members.Population using System.Linq.Expressions; #endif - internal class MemberMergePopulationFactory : MemberPopulationFactoryBase + internal static class MemberMergePopulationGuardFactory { - protected override Expression GetPopulationGuard(IMemberPopulationContext context) + public static Expression Create(IMemberPopulator populator) { - var mapperData = context.MapperData; - var populateCondition = context.PopulateCondition; + var mapperData = populator.MapperData; + var populateCondition = populator.PopulateCondition; if (SkipPopulationGuarding(mapperData)) { @@ -51,8 +51,5 @@ private static bool SkipPopulationGuarding(IBasicMapperData mapperData) return skipObjectValueGuarding; } - - protected override Expression GetGuardedBindingValue(Expression bindingValue, Expression populationGuard) - => bindingValue; } } \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulationContext.cs b/AgileMapper/Members/Population/MemberPopulationContext.cs new file mode 100644 index 000000000..e3e7dddc0 --- /dev/null +++ b/AgileMapper/Members/Population/MemberPopulationContext.cs @@ -0,0 +1,92 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Configuration; + using Configuration.MemberIgnores; + using DataSources.Factories; + using ObjectPopulation; + + internal class MemberPopulationContext + { + private IList<ConfiguredMemberIgnoreBase> _relevantMemberIgnores; + private ConfiguredMemberIgnoreBase _memberIgnore; + private bool _memberIgnoreChecked; + private DataSourceFindContext _dataSourceFindContext; + + public MemberPopulationContext(IObjectMappingData mappingData) + { + MappingData = mappingData; + } + + public MappingRuleSet RuleSet => MappingContext.RuleSet; + + public MapperContext MapperContext => MappingContext.MapperContext; + + private UserConfigurationSet UserConfigurations => MapperContext.UserConfigurations; + + public IMappingContext MappingContext => MappingData.MappingContext; + + public IObjectMappingData MappingData { get; } + + private ObjectMapperData MapperData => MappingData.MapperData; + + public IMemberMapperData MemberMapperData { get; private set; } + + public QualifiedMember TargetMember => MemberMapperData.TargetMember; + + private IList<ConfiguredMemberIgnoreBase> RelevantMemberIgnores + => _relevantMemberIgnores ?? + (_relevantMemberIgnores = UserConfigurations.GetRelevantMemberIgnores(MemberMapperData)); + + public ConfiguredMemberIgnoreBase MemberIgnore + { + get + { + if (_memberIgnoreChecked) + { + return _memberIgnore; + } + + _memberIgnoreChecked = true; + return _memberIgnore = RelevantMemberIgnores.FindMatch(MemberMapperData); + } + } + + public bool TargetMemberIsUnconditionallyIgnored(out Expression populateCondition) + { + if (MemberIgnore == null) + { + populateCondition = null; + return false; + } + + populateCondition = _memberIgnore.GetConditionOrNull(MemberMapperData); + return (populateCondition == null); + } + + public DataSourceFindContext GetDataSourceFindContext() + { + var memberMappingData = MappingData.GetChildMappingData(MemberMapperData); + + if (_dataSourceFindContext == null) + { + _dataSourceFindContext = new DataSourceFindContext(memberMappingData); + } + + return _dataSourceFindContext.With(memberMappingData); + } + + public MemberPopulationContext With(QualifiedMember targetMember) + { + MemberMapperData = new ChildMemberMapperData(targetMember, MapperData); + _memberIgnore = null; + _memberIgnoreChecked = false; + return this; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs deleted file mode 100644 index 1e4b35824..000000000 --- a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ - using System.Collections.Generic; - using Extensions.Internal; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - - internal abstract class MemberPopulationFactoryBase : IMemberPopulationFactory - { - public Expression GetPopulation(IMemberPopulationContext context) - { - if (!context.IsSuccessful) - { - return context.DataSources.ValueExpression; - } - - var useSingleExpression = context.MapperData.UseMemberInitialisations(); - var populationGuard = GetPopulationGuard(context); - - var population = useSingleExpression - ? GetBinding(context, populationGuard) - : context.MapperData.TargetMember.IsReadOnly - ? GetReadOnlyMemberPopulation(context) - : context.DataSources.GetPopulationExpression(); - - if (context.DataSources.Variables.Any()) - { - population = GetPopulationWithVariables(population, context.DataSources.Variables); - } - - return GetGuardedPopulation(population, populationGuard, useSingleExpression); - } - - private static Expression GetPopulationWithVariables(Expression population, IList<ParameterExpression> variables) - { - if (population.NodeType != ExpressionType.Block) - { - return Expression.Block(variables, population); - } - - var populationBlock = (BlockExpression)population; - - if (populationBlock.Variables.Any()) - { - variables = variables.Append(populationBlock.Variables); - } - - return populationBlock.Update(variables, populationBlock.Expressions); - } - - protected abstract Expression GetPopulationGuard(IMemberPopulationContext context); - - private Expression GetBinding(IMemberPopulationContext context, Expression populationGuard) - { - var bindingValue = context.DataSources.ValueExpression; - var guardedBindingValue = GetGuardedBindingValue(bindingValue, populationGuard); - var binding = context.MapperData.GetTargetMemberPopulation(guardedBindingValue); - - return binding; - } - - protected abstract Expression GetGuardedBindingValue(Expression bindingValue, Expression populationGuard); - - private static Expression GetReadOnlyMemberPopulation(IMemberPopulationContext context) - { - var dataSourcesValue = context.DataSources.ValueExpression; - var targetMemberAccess = context.MapperData.GetTargetMemberAccess(); - var targetMemberNotNull = targetMemberAccess.GetIsNotDefaultComparison(); - - return Expression.IfThen(targetMemberNotNull, dataSourcesValue); - } - - public virtual Expression GetGuardedPopulation( - Expression population, - Expression populationGuard, - bool useSingleExpression) - { - return (populationGuard != null) - ? Expression.IfThen(populationGuard, population) - : population; - } - } -} \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulator.cs b/AgileMapper/Members/Population/MemberPopulator.cs index 91cd82338..79d189b97 100644 --- a/AgileMapper/Members/Population/MemberPopulator.cs +++ b/AgileMapper/Members/Population/MemberPopulator.cs @@ -2,58 +2,53 @@ namespace AgileObjects.AgileMapper.Members.Population { using System; using System.Linq; - using Configuration; - using DataSources; - using ReadableExpressions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using DataSources; + using Extensions.Internal; + using ReadableExpressions; - internal class MemberPopulator : IMemberPopulationContext, IMemberPopulator + internal class MemberPopulator : IMemberPopulator { - private MemberPopulator(DataSourceSet dataSources, Expression populateCondition = null) + private readonly IDataSourceSet _dataSources; + + private MemberPopulator(IDataSourceSet dataSources, Expression populateCondition = null) { - DataSources = dataSources; + _dataSources = dataSources; PopulateCondition = populateCondition; } #region Factory Methods - public static IMemberPopulator WithRegistration( - IChildMemberMappingData mappingData, - DataSourceSet dataSources, - Expression populateCondition) + public static IMemberPopulator WithRegistration(IDataSourceSet dataSources, Expression populateCondition) { - var memberPopulation = WithoutRegistration(mappingData, dataSources, populateCondition); + var memberPopulation = WithoutRegistration(dataSources, populateCondition); memberPopulation.MapperData.RegisterTargetMemberDataSourcesIfRequired(dataSources); return memberPopulation; } - public static IMemberPopulator WithoutRegistration( - IChildMemberMappingData mappingData, - DataSourceSet dataSources, - Expression populateCondition = null) - { - return new MemberPopulator(dataSources, populateCondition); - } + public static IMemberPopulator WithoutRegistration(IDataSourceSet dataSources, Expression populateCondition = null) + => new MemberPopulator(dataSources, populateCondition); - public static IMemberPopulator Unmappable(IMemberMapperData mapperData, string reason) - => CreateNullMemberPopulation(mapperData, targetMember => $"No way to populate {targetMember.Name} ({reason})"); + public static IMemberPopulator Unmappable(MemberPopulationContext context, string reason) + => CreateNullMemberPopulator(context, targetMember => $"No way to populate {targetMember.Name} ({reason})"); - public static IMemberPopulator IgnoredMember(IMemberMapperData mapperData, ConfiguredIgnoredMember configuredIgnore) - => CreateNullMemberPopulation(mapperData, configuredIgnore.GetIgnoreMessage); + public static IMemberPopulator IgnoredMember(MemberPopulationContext context) + => CreateNullMemberPopulator(context, context.MemberIgnore.GetIgnoreMessage); - public static IMemberPopulator NoDataSource(IMemberMapperData mapperData) + public static IMemberPopulator NoDataSource(MemberPopulationContext context) { - var noDataSources = CreateNullDataSourceSet(mapperData, GetNoDataSourceMessage); + var noDataSources = CreateNullDataSourceSet(context.MemberMapperData, GetNoDataSourceMessage); - mapperData.RegisterTargetMemberDataSourcesIfRequired(noDataSources); + context.MemberMapperData.RegisterTargetMemberDataSourcesIfRequired(noDataSources); - return new MemberPopulator(noDataSources); + return context.MappingContext.AddUnsuccessfulMemberPopulations + ? new MemberPopulator(noDataSources) : null; } private static string GetNoDataSourceMessage(QualifiedMember targetMember) @@ -63,37 +58,132 @@ private static string GetNoDataSourceMessage(QualifiedMember targetMember) : $"No data source for {targetMember.Name} or any of its child members"; } - private static MemberPopulator CreateNullMemberPopulation( - IMemberMapperData mapperData, + private static MemberPopulator CreateNullMemberPopulator( + MemberPopulationContext context, Func<QualifiedMember, string> commentFactory) { - return new MemberPopulator(CreateNullDataSourceSet(mapperData, commentFactory)); + return context.MappingContext.AddUnsuccessfulMemberPopulations + ? new MemberPopulator(CreateNullDataSourceSet(context.MemberMapperData, commentFactory)) + : null; } - private static DataSourceSet CreateNullDataSourceSet( + private static IDataSourceSet CreateNullDataSourceSet( IMemberMapperData mapperData, Func<QualifiedMember, string> commentFactory) { - return new DataSourceSet( - mapperData, + return DataSourceSet.For( new NullDataSource( - ReadableExpression.Comment(commentFactory.Invoke(mapperData.TargetMember)))); + ReadableExpression.Comment(commentFactory.Invoke(mapperData.TargetMember))), + mapperData); } #endregion - public IMemberMapperData MapperData => DataSources.MapperData; - - public bool IsSuccessful => CanPopulate; + public IMemberMapperData MapperData => _dataSources.MapperData; - public bool CanPopulate => DataSources.HasValue; - - public DataSourceSet DataSources { get; } + public bool CanPopulate => _dataSources.HasValue; public Expression PopulateCondition { get; } public Expression GetPopulation() - => MapperData.RuleSet.PopulationFactory.GetPopulation(this); + { + if (!CanPopulate) + { + return _dataSources.BuildValue(); + } + + var populationGuard = MapperData + .RuleSet + .PopulationGuardFactory + .Invoke(this); + + var useSingleExpression = MapperData.UseMemberInitialisations(); + + var population = useSingleExpression + ? GetBinding(populationGuard) + : MapperData.TargetMember.IsReadOnly + ? GetReadOnlyMemberPopulation() + : GetPopulationExpression(); + + if (_dataSources.Variables.Any()) + { + population = GetPopulationWithVariables(population); + } + + if (useSingleExpression && MapperData.RuleSet.Settings.AllowGuardedBindings) + { + return population; + } + + return (populationGuard != null) + ? Expression.IfThen(populationGuard, population) + : population; + } + + private Expression GetBinding(Expression populationGuard) + { + var bindingValue = _dataSources.BuildValue(); + + if (MapperData.RuleSet.Settings.AllowGuardedBindings && (populationGuard != null)) + { + bindingValue = bindingValue.ToIfFalseDefaultCondition(populationGuard); + } + + return MapperData.GetTargetMemberPopulation(bindingValue); + } + + private Expression GetReadOnlyMemberPopulation() + { + var targetMemberAccess = MapperData.GetTargetMemberAccess(); + var targetMemberNotNull = targetMemberAccess.GetIsNotDefaultComparison(); + + return Expression.IfThen(targetMemberNotNull, _dataSources.BuildValue()); + } + + private Expression GetPopulationExpression() + { + var finalValue = _dataSources.GetFinalValueOrNull(); + var excludeFinalValue = finalValue == null; + var finalDataSourceIndex = _dataSources.Count - 1; + + Expression population = null; + + for (var i = finalDataSourceIndex; i >= 0; --i) + { + var dataSource = _dataSources[i]; + + if (i == finalDataSourceIndex) + { + if (excludeFinalValue) + { + continue; + } + + population = MapperData.GetTargetMemberPopulation(finalValue); + population = dataSource.FinalisePopulation(population); + continue; + } + + var memberPopulation = MapperData.GetTargetMemberPopulation(dataSource.Value); + population = dataSource.FinalisePopulation(memberPopulation, population); + } + + return population; + } + + private Expression GetPopulationWithVariables(Expression population) + { + if (population.NodeType != ExpressionType.Block) + { + return Expression.Block(_dataSources.Variables, population); + } + + var populationBlock = (BlockExpression)population; + + return Expression.Block( + _dataSources.Variables.Append(populationBlock.Variables), + populationBlock.Expressions); + } #region ExcludeFromCodeCoverage #if DEBUG @@ -101,6 +191,6 @@ public Expression GetPopulation() #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/Population/MemberPopulatorFactory.cs b/AgileMapper/Members/Population/MemberPopulatorFactory.cs index 9d2844a6a..91ae9f837 100644 --- a/AgileMapper/Members/Population/MemberPopulatorFactory.cs +++ b/AgileMapper/Members/Population/MemberPopulatorFactory.cs @@ -2,17 +2,11 @@ namespace AgileObjects.AgileMapper.Members.Population { using System; using System.Collections.Generic; - using Configuration; - using DataSources.Finders; + using DataSources.Factories; using Extensions; using Extensions.Internal; using Members; using ObjectPopulation; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif internal class MemberPopulatorFactory { @@ -20,7 +14,7 @@ internal class MemberPopulatorFactory GlobalContext.Instance .MemberCache .GetTargetMembers(mapperData.TargetType) - .ProjectToArray(tm => mapperData.TargetMember.Append(tm))); + .ProjectToArray(mapperData.TargetMember.Append)); private readonly Func<ObjectMapperData, IEnumerable<QualifiedMember>> _targetMembersFactory; @@ -31,69 +25,71 @@ public MemberPopulatorFactory(Func<ObjectMapperData, IEnumerable<QualifiedMember public IEnumerable<IMemberPopulator> Create(IObjectMappingData mappingData) { + var populationContext = new MemberPopulationContext(mappingData); + return _targetMembersFactory .Invoke(mappingData.MapperData) - .Project(tm => - { - var memberPopulation = Create(tm, mappingData); - - if (memberPopulation.CanPopulate || - mappingData.MappingContext.AddUnsuccessfulMemberPopulations) - { - return memberPopulation; - } - - return null; - }) + .Project(populationContext, (ctx, tm) => Create(ctx.With(tm))) .WhereNotNull(); } - private static IMemberPopulator Create(QualifiedMember targetMember, IObjectMappingData mappingData) + private static IMemberPopulator Create(MemberPopulationContext context) { - var childMapperData = new ChildMemberMapperData(targetMember, mappingData.MapperData); - - if (childMapperData.TargetMemberIsUnmappable(out var reason)) + if (TargetMemberIsUnmappable(context, out var reason)) { - return MemberPopulator.Unmappable(childMapperData, reason); + return MemberPopulator.Unmappable(context, reason); } - if (TargetMemberIsUnconditionallyIgnored( - childMapperData, - out var configuredIgnore, - out var populateCondition)) + if (context.TargetMemberIsUnconditionallyIgnored(out var populateCondition)) { - return MemberPopulator.IgnoredMember(childMapperData, configuredIgnore); + return MemberPopulator.IgnoredMember(context); } - var childMappingData = mappingData.GetChildMappingData(childMapperData); - var dataSources = DataSourceFinder.FindFor(childMappingData); + var dataSourceFindContext = context.GetDataSourceFindContext(); + var dataSources = DataSourceSetFactory.CreateFor(dataSourceFindContext); if (dataSources.None) { - return MemberPopulator.NoDataSource(childMapperData); + return MemberPopulator.NoDataSource(context); } - return MemberPopulator.WithRegistration(childMappingData, dataSources, populateCondition); + return MemberPopulator.WithRegistration(dataSources, populateCondition); } - private static bool TargetMemberIsUnconditionallyIgnored( - IMemberMapperData mapperData, - out ConfiguredIgnoredMember configuredIgnore, - out Expression populateCondition) + private static bool TargetMemberIsUnmappable(MemberPopulationContext context, out string reason) { - configuredIgnore = mapperData - .MapperContext - .UserConfigurations - .GetMemberIgnoreOrNull(mapperData); + if (!context.RuleSet.Settings.AllowSetMethods && + (context.TargetMember.LeafMember.MemberType == MemberType.SetMethod)) + { + reason = "Set methods are unsupported by rule set '" + context.RuleSet.Name + "'"; + return true; + } - if (configuredIgnore == null) + if (TargetMemberWillBePopulatedByCtor(context)) + { + reason = "Expected to be populated by constructor parameter"; + return true; + } + + return context.MemberMapperData.TargetMemberIsUnmappable( + context.TargetMember, + md => md.MapperContext.UserConfigurations.QueryDataSourceFactories(md), + context.MapperContext.UserConfigurations, + out reason); + } + + private static bool TargetMemberWillBePopulatedByCtor(MemberPopulationContext context) + { + if (!context.TargetMember.LeafMember.HasMatchingCtorParameter || + (context.RuleSet.Settings.RootHasPopulatedTarget && context.MappingData.IsRoot)) { - populateCondition = null; return false; } - populateCondition = configuredIgnore.GetConditionOrNull(mapperData); - return (populateCondition == null); + var creationInfos = context.MappingData.GetTargetObjectCreationInfos(); + + return creationInfos.Any() && + creationInfos.All(ci => ci.IsUnconditional && ci.HasCtorParameterFor(context.TargetMember.LeafMember)); } } } \ No newline at end of file diff --git a/AgileMapper/Members/Population/NullMemberPopulationGuardFactory.cs b/AgileMapper/Members/Population/NullMemberPopulationGuardFactory.cs new file mode 100644 index 000000000..a9cb690b7 --- /dev/null +++ b/AgileMapper/Members/Population/NullMemberPopulationGuardFactory.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + + internal static class NullMemberPopulationGuardFactory + { + public static Expression Create(IMemberPopulator populator) + => populator.PopulateCondition; + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulationFactory.cs b/AgileMapper/Members/Population/PopulationGuardFactory.cs similarity index 54% rename from AgileMapper/Members/Population/IMemberPopulationFactory.cs rename to AgileMapper/Members/Population/PopulationGuardFactory.cs index 0d1582192..577ea8995 100644 --- a/AgileMapper/Members/Population/IMemberPopulationFactory.cs +++ b/AgileMapper/Members/Population/PopulationGuardFactory.cs @@ -6,8 +6,5 @@ namespace AgileObjects.AgileMapper.Members.Population using System.Linq.Expressions; #endif - internal interface IMemberPopulationFactory - { - Expression GetPopulation(IMemberPopulationContext context); - } + internal delegate Expression PopulationGuardFactory(IMemberPopulator populator); } \ No newline at end of file diff --git a/AgileMapper/Members/QualifiedMember.cs b/AgileMapper/Members/QualifiedMember.cs index d819b891a..b8a94d423 100644 --- a/AgileMapper/Members/QualifiedMember.cs +++ b/AgileMapper/Members/QualifiedMember.cs @@ -83,10 +83,7 @@ private QualifiedMember(Member leafMember, MapperContext mapperContext) _childMemberCache = mapperContext.Cache.CreateNew<Member, QualifiedMember>(default(HashCodeComparer<Member>)); } - RegistrationName = (LeafMember.MemberType != MemberType.ConstructorParameter) - ? Name - : "ctor:" + Name; - + RegistrationName = this.IsConstructorParameter() ? "ctor:" + Name : Name; IsReadOnly = IsReadable && !leafMember.IsWriteable; } diff --git a/AgileMapper/Members/SourceMemberMatch.cs b/AgileMapper/Members/SourceMemberMatch.cs new file mode 100644 index 000000000..b4dbeaef8 --- /dev/null +++ b/AgileMapper/Members/SourceMemberMatch.cs @@ -0,0 +1,57 @@ +namespace AgileObjects.AgileMapper.Members +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using DataSources; + using Extensions.Internal; + + internal class SourceMemberMatch + { + public static readonly SourceMemberMatch Null = new SourceMemberMatch(); + + private SourceMemberMatch() + { + } + + public SourceMemberMatch( + IQualifiedMember sourceMember, + IChildMemberMappingData contextMappingData, + Expression condition, + bool isUseable = true) + { + SourceMember = sourceMember; + ContextMappingData = contextMappingData; + Condition = condition; + IsUseable = isUseable; + } + + public bool IsUseable { get; } + + public IChildMemberMappingData ContextMappingData { get; } + + public IQualifiedMember SourceMember { get; } + + public Expression Condition { get; } + + public IDataSource CreateDataSource() + { + var mapperData = ContextMappingData.MapperData; + var sourceMember = SourceMember.RelativeTo(mapperData.SourceMember); + + var sourceMemberValue = sourceMember + .GetQualifiedAccess(mapperData) + .GetConversionTo(sourceMember.Type); + + var sourceMemberDataSource = new SourceMemberDataSource( + sourceMember, + sourceMemberValue, + Condition, + mapperData); + + return sourceMemberDataSource; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/SourceMemberMatchContext.cs b/AgileMapper/Members/SourceMemberMatchContext.cs new file mode 100644 index 000000000..31c907f73 --- /dev/null +++ b/AgileMapper/Members/SourceMemberMatchContext.cs @@ -0,0 +1,106 @@ +namespace AgileObjects.AgileMapper.Members +{ + using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Configuration; + using Configuration.MemberIgnores; + using Extensions.Internal; + using NetStandardPolyfills; + + internal class SourceMemberMatchContext + { + private IList<ConfiguredSourceMemberIgnoreBase> _relevantSourceMemberIgnores; + private IQualifiedMember _parentSourceMember; + + public SourceMemberMatchContext( + IChildMemberMappingData memberMappingData, + bool searchParentContexts = true) + { + MemberMappingData = memberMappingData; + SearchParentContexts = searchParentContexts; + } + + private MapperContext MapperContext => MemberMapperData.MapperContext; + + private UserConfigurationSet UserConfigurations => MapperContext.UserConfigurations; + + public IChildMemberMappingData MemberMappingData { get; private set; } + + public IMemberMapperData MemberMapperData => MemberMappingData.MapperData; + + public QualifiedMember TargetMember => MemberMapperData.TargetMember; + + public IQualifiedMember ParentSourceMember => _parentSourceMember ?? MemberMapperData.SourceMember; + + public bool SearchParentContexts { get; } + + public IQualifiedMember MatchingSourceMember { get; set; } + + public SourceMemberMatch SourceMemberMatch { get; set; } + + public bool TypesAreCompatible + => MemberMapperData.TargetType.IsAssignableTo(MemberMapperData.SourceType); + + public bool HasSourceMemberIgnores => RelevantSourceMemberIgnores.Any(); + + private IList<ConfiguredSourceMemberIgnoreBase> RelevantSourceMemberIgnores + => _relevantSourceMemberIgnores ?? + (_relevantSourceMemberIgnores = UserConfigurations.GetRelevantSourceMemberIgnores(MemberMapperData)); + + public ConfiguredSourceMemberIgnoreBase GetSourceMemberIgnoreOrNull(IQualifiedMember sourceMember) + => RelevantSourceMemberIgnores.FindMatch(new BasicMapperData(sourceMember, TargetMember, MemberMapperData)); + + public SourceMemberMatch CreateSourceMemberMatch(IQualifiedMember matchingSourceMember = null, bool isUseable = true) + { + if (matchingSourceMember == null) + { + matchingSourceMember = MatchingSourceMember; + } + + var ignoreCondition = GetSourceMemberCondition(matchingSourceMember); + + matchingSourceMember = MapperContext + .QualifiedMemberFactory + .GetFinalSourceMember(matchingSourceMember, TargetMember); + + return new SourceMemberMatch( + matchingSourceMember, + MemberMappingData, + ignoreCondition, + isUseable); + } + + private Expression GetSourceMemberCondition(IQualifiedMember sourceMember) + { + if (!HasSourceMemberIgnores) + { + return null; + } + + var matchingIgnore = GetSourceMemberIgnoreOrNull(sourceMember); + + return (matchingIgnore?.HasConfiguredCondition == true) + ? matchingIgnore.GetConditionOrNull(MemberMapperData) + : null; + } + + public SourceMemberMatchContext With(IQualifiedMember parentSourceMember) + { + _parentSourceMember = parentSourceMember; + return this; + } + + public SourceMemberMatchContext With(IChildMemberMappingData memberMappingData) + { + MemberMappingData = memberMappingData; + _parentSourceMember = null; + MatchingSourceMember = null; + SourceMemberMatch = null; + return this; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/SourceMemberMatcher.cs b/AgileMapper/Members/SourceMemberMatcher.cs index fa5e0a1d2..41ad5c785 100644 --- a/AgileMapper/Members/SourceMemberMatcher.cs +++ b/AgileMapper/Members/SourceMemberMatcher.cs @@ -7,48 +7,109 @@ internal static class SourceMemberMatcher { - public static IQualifiedMember GetMatchFor( - IChildMemberMappingData targetData, - out IChildMemberMappingData contextMappingData, + public static SourceMemberMatch GetMatchFor( + IChildMemberMappingData targetMappingData, bool searchParentContexts = true) { - var parentSourceMember = targetData.MapperData.SourceMember; + return GetMatchFor(new SourceMemberMatchContext(targetMappingData, searchParentContexts)); + } + + public static SourceMemberMatch GetMatchFor(SourceMemberMatchContext context) + { + if (context.ParentSourceMember.IsSimple) + { + return SourceMemberMatch.Null; + } - if (parentSourceMember.IsSimple) + if (ExactMemberMatchExists(context)) { - contextMappingData = null; - return null; + return context.CreateSourceMemberMatch(); + } + + if (TryFindSourceMemberMatch(context) || TryFindParentContextSourceMemberMatch(context)) + { + return context.SourceMemberMatch; + } + + return (context.MatchingSourceMember != null) + ? context.CreateSourceMemberMatch(isUseable: false) + : SourceMemberMatch.Null; + } + + private static bool ExactMemberMatchExists(SourceMemberMatchContext context) + { + var filter = context.TypesAreCompatible + ? (Func<IMemberMapperData, Member, bool>)MembersAreTheSame + : MembersMatch; + + var matchingSourceMember = QuerySourceMembers(context, filter).FirstOrDefault(); + + if (matchingSourceMember == null) + { + return false; } - if (ExactMatchingSourceMemberExists(parentSourceMember, targetData, out var matchingMember)) + context.MatchingSourceMember = matchingSourceMember; + + return TypesAreCompatible(matchingSourceMember.Type, context.MemberMapperData); + } + + private static bool MembersAreTheSame(IMemberMapperData mapperData, Member sourceMember) + => mapperData.TargetMember.LeafMember.Equals(sourceMember); + + private static bool MembersMatch(IMemberMapperData mapperData, Member sourceMember) + { + if (MembersAreTheSame(mapperData, sourceMember)) { - contextMappingData = targetData; - return GetFinalSourceMember(matchingMember, targetData); + return true; } - matchingMember = EnumerateSourceMembers(parentSourceMember, targetData) - .FirstOrDefault(sm => IsMatchingMember(sm, targetData.MapperData)); + return mapperData + .SourceMember + .Append(sourceMember) + .Matches(mapperData.TargetMember); + } - if (matchingMember != null) + private static IEnumerable<IQualifiedMember> QuerySourceMembers( + SourceMemberMatchContext context, + Func<IMemberMapperData, Member, bool> filter) + { + IEnumerable<Member> members = GlobalContext.Instance + .MemberCache + .GetSourceMembers(context.ParentSourceMember.Type); + + if (!context.MemberMapperData.RuleSet.Settings.AllowGetMethods) { - contextMappingData = targetData; - return GetFinalSourceMember(matchingMember, targetData); + members = members.Filter(m => m.MemberType != MemberType.GetMethod); } - if (searchParentContexts) + var qualifiedMembers = members + .Filter(context.MemberMapperData, filter.Invoke) + .Project(context.ParentSourceMember.Append); + + if (context.HasSourceMemberIgnores) { - return GetParentContextMatchOrNull(targetData, out contextMappingData); + qualifiedMembers = qualifiedMembers.Filter(context, IsNotUnconditionallyIgnored); } - contextMappingData = null; - return null; + return qualifiedMembers; } - private static IQualifiedMember GetParentContextMatchOrNull( - IChildMemberMappingData targetData, - out IChildMemberMappingData contextMappingData) + private static bool IsNotUnconditionallyIgnored(SourceMemberMatchContext context, IQualifiedMember sourceMember) { - var mappingData = targetData.Parent; + var matchingIgnore = context.GetSourceMemberIgnoreOrNull(sourceMember); + + return matchingIgnore?.HasConfiguredCondition != false; + } + + private static bool TryFindParentContextSourceMemberMatch(SourceMemberMatchContext context) + { + if (!context.SearchParentContexts) + { + return false; + } + + var mappingData = context.MemberMappingData.Parent; while (mappingData.Parent != null) { @@ -60,150 +121,91 @@ private static IQualifiedMember GetParentContextMatchOrNull( mappingData = mappingData.Parent; - var childMapperData = new ChildMemberMapperData(targetData.MapperData.TargetMember, mappingData.MapperData); - contextMappingData = mappingData.GetChildMappingData(childMapperData); - - var matchingMember = EnumerateSourceMembers(mappingData.MapperData.SourceMember, contextMappingData) - .FirstOrDefault(sm => IsMatchingMember(sm, targetData.MapperData)); + var childMapperData = new ChildMemberMapperData(context.TargetMember, mappingData.MapperData); + var contextMappingData = mappingData.GetChildMappingData(childMapperData); - if (matchingMember != null) + if (TryFindSourceMemberMatch(context.With(contextMappingData))) { - return GetFinalSourceMember(matchingMember, targetData); + return true; } } - contextMappingData = null; - return null; + return false; } - private static bool ExactMatchingSourceMemberExists( - IQualifiedMember parentSourceMember, - IChildMemberMappingData targetData, - out IQualifiedMember matchingMember) + private static bool TryFindSourceMemberMatch(SourceMemberMatchContext context) { - var sourceMember = QuerySourceMembers( - parentSourceMember, - targetData, - MembersMatch) - .FirstOrDefault(); + var candidateSourceMembers = EnumerateSourceMembers(context) + .Filter(context.TargetMember.Matches); - if ((sourceMember == null) || !TypesAreCompatible(sourceMember.Type, targetData.MapperData)) + foreach (var sourceMember in candidateSourceMembers) { - matchingMember = null; - return false; - } - - matchingMember = parentSourceMember.Append(sourceMember); - return true; - } + if (TypesAreCompatible(sourceMember.Type, context.MemberMapperData)) + { + context.SourceMemberMatch = context.CreateSourceMemberMatch(sourceMember); + return true; + } - private static bool MembersMatch(IChildMemberMappingData mappingData, Member sourceMember) - { - if (mappingData.MapperData.TargetMember.LeafMember.Equals(sourceMember)) - { - return true; + if (context.MatchingSourceMember == null) + { + context.MatchingSourceMember = sourceMember; + } } - return mappingData - .MapperData - .SourceMember - .Append(sourceMember) - .Matches(mappingData.MapperData.TargetMember); - } - - private static IEnumerable<Member> QuerySourceMembers( - IQualifiedMember parentMember, - IChildMemberMappingData mappingData, - Func<IChildMemberMappingData, Member, bool> filter) - { - var members = GlobalContext - .Instance - .MemberCache - .GetSourceMembers(parentMember.Type) - .Filter(m => filter.Invoke(mappingData, m)); - - return mappingData.RuleSet.Settings.AllowGetMethods - ? members - : members.Filter(m => m.MemberType != MemberType.GetMethod); - } - - private static IQualifiedMember GetFinalSourceMember( - IQualifiedMember sourceMember, - IChildMemberMappingData targetData) - { - return targetData - .MapperData - .MapperContext - .QualifiedMemberFactory - .GetFinalSourceMember(sourceMember, targetData.MapperData.TargetMember); + return false; } - private static IEnumerable<IQualifiedMember> EnumerateSourceMembers( - IQualifiedMember parentMember, - IChildMemberMappingData rootData) + private static IEnumerable<IQualifiedMember> EnumerateSourceMembers(SourceMemberMatchContext context) { - yield return parentMember; + yield return context.ParentSourceMember; - if (!parentMember.CouldMatch(rootData.MapperData.TargetMember)) + if (!context.ParentSourceMember.CouldMatch(context.TargetMember)) { yield break; } - var parentMemberType = rootData.Parent.GetSourceMemberRuntimeType(parentMember); + var parentMemberType = context.MemberMappingData.Parent + .GetSourceMemberRuntimeType(context.ParentSourceMember); - if (parentMemberType != parentMember.Type) + if (parentMemberType != context.ParentSourceMember.Type) { - parentMember = parentMember.WithType(parentMemberType); - yield return parentMember; + context.With(context.ParentSourceMember.WithType(parentMemberType)); + yield return context.ParentSourceMember; - if (parentMember.IsSimple) + if (context.ParentSourceMember.IsSimple) { yield break; } } - var relevantSourceMembers = QuerySourceMembers( - parentMember, - rootData, - MembersHaveCompatibleTypes); + var relevantChildSourceMembers = QuerySourceMembers(context, MembersHaveCompatibleTypes); - foreach (var sourceMember in relevantSourceMembers) + foreach (var childMember in relevantChildSourceMembers) { - var childMember = parentMember.Append(sourceMember); - - if (sourceMember.IsSimple) + if (childMember.IsSimple) { yield return childMember; continue; } - foreach (var qualifiedMember in EnumerateSourceMembers(childMember, rootData)) + foreach (var qualifiedMember in EnumerateSourceMembers(context.With(childMember))) { yield return qualifiedMember; } } } - private static bool MembersHaveCompatibleTypes(IChildMemberMappingData rootData, Member sourceMember) + private static bool MembersHaveCompatibleTypes(IMemberMapperData rootMapperData, Member sourceMember) { if (!sourceMember.IsSimple) { return true; } - var targetMember = rootData.MapperData.TargetMember; - - if (targetMember.IsSimple) - { - return true; - } - - return targetMember.Type == typeof(object); + return rootMapperData.TargetMember.IsSimple || + rootMapperData.TargetMember.Type == typeof(object); } - private static bool IsMatchingMember(IQualifiedMember sourceMember, IMemberMapperData mapperData) - => mapperData.TargetMember.Matches(sourceMember) && TypesAreCompatible(sourceMember.Type, mapperData); - private static bool TypesAreCompatible(Type sourceType, IMemberMapperData mapperData) => mapperData.CanConvert(sourceType, mapperData.TargetMember.Type); } diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index 0844e5cc8..217972517 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -12,7 +12,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using Caching; using Configuration; using DataSources; - using DataSources.Finders; + using DataSources.Factories; using Extensions; using Extensions.Internal; using MapperKeys; @@ -22,74 +22,52 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes internal class ComplexTypeConstructionFactory { - private readonly ICache<ConstructionKey, Construction> _constructorsCache; + private readonly ICache<ConstructionKey, IList<IConstructionInfo>> _constructionInfosCache; + private readonly ICache<ConstructionKey, Construction> _constructionsCache; public ComplexTypeConstructionFactory(CacheSet mapperScopedCacheSet) { - _constructorsCache = mapperScopedCacheSet.CreateScoped<ConstructionKey, Construction>(); + _constructionInfosCache = mapperScopedCacheSet.CreateScoped<ConstructionKey, IList<IConstructionInfo>>(); + _constructionsCache = mapperScopedCacheSet.CreateScoped<ConstructionKey, Construction>(); } - public Expression GetNewObjectCreation(IObjectMappingData mappingData) + public IList<IBasicConstructionInfo> GetTargetObjectCreationInfos(IObjectMappingData mappingData) + => GetTargetObjectCreationInfos(mappingData, out _).ProjectToArray(c => (IBasicConstructionInfo)c); + + private IList<IConstructionInfo> GetTargetObjectCreationInfos( + IObjectMappingData mappingData, + out ConstructionKey constructionKey) { - var objectCreation = _constructorsCache.GetOrAdd(new ConstructionKey(mappingData), key => + return _constructionInfosCache.GetOrAdd(constructionKey = new ConstructionKey(mappingData), key => { - var constructions = new List<Construction>(); + IList<IConstructionInfo> constructionInfos = new List<IConstructionInfo>(); - AddConfiguredConstructions( - constructions, + AddConfiguredConstructionInfos( + constructionInfos, key, out var otherConstructionRequired); if (otherConstructionRequired && !key.MappingData.MapperData.TargetType.IsAbstract()) { - AddAutoConstructions(constructions, key); - } - - if (constructions.None()) - { - key.MappingData = null; - return null; + AddAutoConstructionInfos(constructionInfos, key); } - var construction = Construction.For(constructions, key); - key.AddSourceMemberTypeTesterIfRequired(); key.MappingData = null; - return construction; + return constructionInfos.None() + ? Enumerable<IConstructionInfo>.EmptyArray + : constructionInfos; }); - - if (objectCreation == null) - { - return null; - } - - mappingData.MapperData.Context.UsesMappingDataObjectAsParameter = objectCreation.UsesMappingDataObjectParameter; - - var creationExpression = objectCreation.GetConstruction(mappingData); - - return creationExpression; - } - - public Expression GetFactoryMethodObjectCreationOrNull(IObjectMappingData mappingData) - { - var key = new ConstructionKey(mappingData); - var factoryData = GetGreediestAvailableFactories(key); - - return factoryData.Any() - ? factoryData.First().Construction.With(key).GetConstruction(mappingData) - : null; } - private static void AddConfiguredConstructions( - ICollection<Construction> constructions, + private static void AddConfiguredConstructionInfos( + ICollection<IConstructionInfo> constructionInfos, ConstructionKey key, out bool otherConstructionRequired) { var mapperData = key.MappingData.MapperData; - otherConstructionRequired = true; - var configuredFactories = mapperData .MapperContext .UserConfigurations @@ -97,84 +75,86 @@ private static void AddConfiguredConstructions( foreach (var configuredFactory in configuredFactories) { - var configuredConstruction = new Construction(configuredFactory, mapperData); + var configuredConstructionInfo = new ConfiguredFactoryInfo(configuredFactory, mapperData); - constructions.Add(configuredConstruction); + constructionInfos.Add(configuredConstructionInfo); - if (configuredConstruction.IsUnconditional) + if (configuredConstructionInfo.IsUnconditional) { otherConstructionRequired = false; return; } } + + otherConstructionRequired = true; } - private static void AddAutoConstructions(IList<Construction> constructions, ConstructionKey key) + private static void AddAutoConstructionInfos(IList<IConstructionInfo> constructionInfos, ConstructionKey key) { var mapperData = key.MappingData.MapperData; - var greediestAvailableFactories = GetGreediestAvailableFactories(key); - var greediestUnconditionalFactory = greediestAvailableFactories.LastOrDefault(f => f.IsUnconditional); + var greediestAvailableFactoryInfos = GetGreediestAvailableFactoryInfos(key); + var greediestUnconditionalFactoryInfo = greediestAvailableFactoryInfos.LastOrDefault(f => f.IsUnconditional); var constructors = mapperData.TargetInstance.Type .GetPublicInstanceConstructors() .ToArray(); - var greediestAvailableNewings = constructors.Any() - ? GetGreediestAvailableNewings(constructors, key, greediestUnconditionalFactory) - : Enumerable<ConstructionData<ConstructorInfo>>.EmptyArray; - int i; - for (i = 0; i < greediestAvailableFactories.Length; i++) + for (i = 0; i < greediestAvailableFactoryInfos.Length;) { - greediestAvailableFactories[i].AddTo(constructions, key); + greediestAvailableFactoryInfos[i++].AddTo(constructionInfos, key); } - for (i = 0; i < greediestAvailableNewings.Length; i++) + if (constructors.Any()) { - greediestAvailableNewings[i].AddTo(constructions, key); + var greediestAvailableNewingInfos = GetGreediestAvailableNewingInfos( + constructors, + key, + greediestUnconditionalFactoryInfo); + + for (i = 0; i < greediestAvailableNewingInfos.Length;) + { + greediestAvailableNewingInfos[i++].AddTo(constructionInfos, key); + } } - if (constructions.None() && mapperData.TargetMemberIsUserStruct()) + if (constructionInfos.None() && mapperData.TargetMemberIsUserStruct()) { - constructions.Add(Construction.NewStruct(mapperData.TargetInstance.Type)); + constructionInfos.Add(new StructInfo(mapperData.TargetInstance.Type)); } } - private static ConstructionData<MethodInfo>[] GetGreediestAvailableFactories(ConstructionKey key) + private static ConstructionDataInfo<MethodInfo>[] GetGreediestAvailableFactoryInfos(ConstructionKey key) { var mapperData = key.MappingData.MapperData; var candidateFactoryMethods = mapperData.TargetInstance.Type .GetPublicStaticMethods() - .Filter(m => IsFactoryMethod(m, mapperData.TargetInstance.Type)); + .Filter(mapperData.TargetInstance.Type, IsFactoryMethod); - return CreateConstructionData( - candidateFactoryMethods, - fm => new ConstructionData<MethodInfo>(fm, Expression.Call, key, priority: 1)); + return CreateConstructionInfo(candidateFactoryMethods, fm => new FactoryMethodInfo(fm, key)); } - private static bool IsFactoryMethod(MethodInfo method, Type targetType) + private static bool IsFactoryMethod(Type targetType, MethodInfo method) { return (method.ReturnType == targetType) && (method.Name.StartsWith("Create", Ordinal) || method.Name.StartsWith("Get", Ordinal)); } - private static ConstructionData<ConstructorInfo>[] GetGreediestAvailableNewings( + private static ConstructionDataInfo<ConstructorInfo>[] GetGreediestAvailableNewingInfos( IEnumerable<ConstructorInfo> constructors, ConstructionKey key, - ConstructionData<MethodInfo> greediestUnconditionalFactory) + IBasicConstructionInfo greediestUnconditionalFactoryInfo) { var candidateConstructors = constructors - .Filter(ctor => IsCandidateCtor(ctor, greediestUnconditionalFactory)); + .Filter(greediestUnconditionalFactoryInfo, IsCandidateCtor); - return CreateConstructionData( - candidateConstructors, - ctor => new ConstructionData<ConstructorInfo>(ctor, Expression.New, key, priority: 0)); + return CreateConstructionInfo(candidateConstructors, ctor => new ObjectNewingInfo(ctor, key)); } - private static bool IsCandidateCtor(MethodBase ctor, ConstructionData<MethodInfo> candidateFactoryMethod) + private static bool IsCandidateCtor(IBasicConstructionInfo candidateFactoryMethod, MethodBase ctor) { var ctorCarameters = ctor.GetParameters(); @@ -187,12 +167,12 @@ private static bool IsNotCopyConstructor(Type type, IList<ParameterInfo> ctorPar { // If the constructor takes an instance of itself, we'll potentially end // up in an infinite loop figuring out how to create instances for it: - return ctorParameters.None(p => p.ParameterType == type); + return ctorParameters.None(type, (t, p) => p.ParameterType == t); } - private static ConstructionData<T>[] CreateConstructionData<T>( + private static ConstructionDataInfo<T>[] CreateConstructionInfo<T>( IEnumerable<T> invokables, - Func<T, ConstructionData<T>> dataFactory) + Func<T, ConstructionDataInfo<T>> dataFactory) where T : MethodBase { return invokables @@ -203,7 +183,47 @@ private static ConstructionData<T>[] CreateConstructionData<T>( .ToArray(); } - public void Reset() => _constructorsCache.Empty(); + public Expression GetTargetObjectCreation(IObjectMappingData mappingData) + { + var cachedInfos = GetTargetObjectCreationInfos(mappingData, out var constructionKey); + + if (cachedInfos.None()) + { + return null; + } + + constructionKey.MappingData = mappingData; + constructionKey.Infos = cachedInfos; + + var cachedConstruction = _constructionsCache.GetOrAdd(constructionKey, key => + { + var constructions = key.Infos.ProjectToArray(info => info.ToConstruction()); + var construction = Construction.For(constructions, key); + + key.AddSourceMemberTypeTesterIfRequired(); + key.MappingData = null; + + return construction; + }); + + mappingData.MapperData.Context.UsesMappingDataObjectAsParameter = cachedConstruction.UsesMappingDataObjectParameter; + + var constructionExpression = cachedConstruction.GetConstruction(mappingData); + + return constructionExpression; + } + + public Expression GetFactoryMethodObjectCreationOrNull(IObjectMappingData mappingData) + { + var key = new ConstructionKey(mappingData); + var factoryData = GetGreediestAvailableFactoryInfos(key); + + return factoryData.Any() + ? factoryData.First().ToConstruction().With(key).GetConstruction(mappingData) + : null; + } + + public void Reset() => _constructionsCache.Empty(); #region Helper Classes @@ -221,6 +241,8 @@ public ConstructionKey(IObjectMappingData mappingData) _targetMember = mappingData.MapperData.TargetMember; } + public IList<IConstructionInfo> Infos { get; set; } + public override bool Equals(object obj) { var otherKey = (ConstructionKey)obj; @@ -240,21 +262,82 @@ public override bool Equals(object obj) public override int GetHashCode() => 0; } - private class ConstructionData<TInvokable> : IConstructionInfo - where TInvokable : MethodBase + private interface IConstructionInfo : IBasicConstructionInfo, IComparable<IConstructionInfo> + { + Construction ToConstruction(); + } + + private abstract class ConstructionInfoBase : IConstructionInfo { - private readonly Tuple<QualifiedMember, DataSourceSet>[] _argumentDataSources; + public bool IsConfigured { get; protected set; } + + public bool IsUnconditional { get; protected set; } + + public int ParameterCount { get; protected set; } + + public int Priority { get; protected set; } + + public virtual bool HasCtorParameterFor(Member targetMember) => false; + + public abstract Construction ToConstruction(); + + public int CompareTo(IConstructionInfo other) + { + // ReSharper disable once ImpureMethodCallOnReadonlyValueField + var isConfiguredComparison = other.IsConfigured.CompareTo(IsConfigured); + + if (isConfiguredComparison != 0) + { + return isConfiguredComparison; + } + + var conditionalComparison = IsUnconditional.CompareTo(other.IsUnconditional); - public ConstructionData( + if (conditionalComparison != 0) + { + return conditionalComparison; + } + + var paramCountComparison = ParameterCount.CompareTo(other.ParameterCount); + + if (paramCountComparison != 0) + { + return paramCountComparison; + } + + var priorityComparison = other.Priority.CompareTo(Priority); + + return priorityComparison; + } + } + + private sealed class ConfiguredFactoryInfo : ConstructionInfoBase + { + private readonly ConfiguredObjectFactory _configuredFactory; + private readonly IMemberMapperData _mapperData; + + public ConfiguredFactoryInfo(ConfiguredObjectFactory configuredFactory, IMemberMapperData mapperData) + { + _configuredFactory = configuredFactory; + _mapperData = mapperData; + IsConfigured = true; + IsUnconditional = !_configuredFactory.HasConfiguredCondition; + } + + public override Construction ToConstruction() => new Construction(_configuredFactory, _mapperData); + } + + private abstract class ConstructionDataInfo<TInvokable> : ConstructionInfoBase + where TInvokable : MethodBase + { + protected ConstructionDataInfo( TInvokable invokable, - Func<TInvokable, IList<Expression>, Expression> constructionFactory, ConstructionKey key, int priority) { - var argumentDataSources = GetArgumentDataSources(invokable, key); - - CanBeInvoked = argumentDataSources.All(ds => ds.Item2.HasValue); - ParameterCount = argumentDataSources.Length; + ArgumentDataSources = GetArgumentDataSources(invokable, key); + CanBeInvoked = ArgumentDataSources.All(ds => ds.HasValue); + ParameterCount = ArgumentDataSources.Length; Priority = priority; if (!CanBeInvoked) @@ -262,27 +345,119 @@ public ConstructionData( return; } + IsUnconditional = !ArgumentDataSources.Any(ds => ds.IsConditional && ds.MapperData.TargetMember.IsComplex); + } + + private static IDataSourceSet[] GetArgumentDataSources(TInvokable invokable, ConstructionKey key) + { + return invokable + .GetParameters() + .ProjectToArray(key.MappingData, (mappingData, p) => + { + var parameterMapperData = new ChildMemberMapperData( + mappingData.MapperData.TargetMember.Append(Member.ConstructorParameter(p)), + mappingData.MapperData); + + var memberMappingData = mappingData.GetChildMappingData(parameterMapperData); + var dataSources = DataSourceSetFactory.CreateFor(new DataSourceFindContext(memberMappingData)); + + return dataSources; + }); + } + + public IDataSourceSet[] ArgumentDataSources { get; } + + public bool CanBeInvoked { get; } + + public void AddTo(IList<IConstructionInfo> constructionInfos, ConstructionKey key) + { + if (ParameterCount > 0) + { + var dataSources = key.MappingData.MapperData.DataSourcesByTargetMember; + + var relevantDataSourceSets = ArgumentDataSources + .Filter(dataSources, (dss, ds) => !dss.ContainsKey(ds.MapperData.TargetMember)); + + foreach (var dataSourceSet in relevantDataSourceSets) + { + dataSources.Add(dataSourceSet.MapperData.TargetMember, dataSourceSet); + } + } + + constructionInfos.AddThenSort(this); + } + + public abstract Expression GetConstructionExpression(IList<Expression> argumentValues); + + public override Construction ToConstruction() => new ConstructionData<TInvokable>(this).Construction; + } + + private sealed class ObjectNewingInfo : ConstructionDataInfo<ConstructorInfo> + { + private readonly ConstructorInfo _ctor; + private readonly string[] _parameterNames; + + public ObjectNewingInfo(ConstructorInfo ctor, ConstructionKey key) + : base(ctor, key, priority: 0) + { + _ctor = ctor; + _parameterNames = _ctor.GetParameters().ProjectToArray(p => p.Name); + } + + public override bool HasCtorParameterFor(Member targetMember) + => _parameterNames.Contains(targetMember.Name, StringComparer.OrdinalIgnoreCase); + + public override Expression GetConstructionExpression(IList<Expression> argumentValues) + => Expression.New(_ctor, argumentValues); + } + + private sealed class FactoryMethodInfo : ConstructionDataInfo<MethodInfo> + { + private readonly MethodInfo _factoryMethod; + + public FactoryMethodInfo(MethodInfo factoryMethod, ConstructionKey key) + : base(factoryMethod, key, priority: 1) + { + _factoryMethod = factoryMethod; + } + + public override Expression GetConstructionExpression(IList<Expression> argumentValues) + => Expression.Call(_factoryMethod, argumentValues); + } + + private sealed class StructInfo : ConstructionInfoBase + { + private readonly Type _targetType; + + public StructInfo(Type targetType) + { + _targetType = targetType; IsUnconditional = true; + } + + public override Construction ToConstruction() => new Construction(Expression.New(_targetType)); + } + private sealed class ConstructionData<TInvokable> + where TInvokable : MethodBase + { + public ConstructionData(ConstructionDataInfo<TInvokable> info) + { Expression constructionExpression; - if (argumentDataSources.None()) + if (info.ArgumentDataSources.None()) { - constructionExpression = constructionFactory.Invoke(invokable, Enumerable<Expression>.EmptyArray); - Construction = new Construction(this, constructionExpression); + constructionExpression = info.GetConstructionExpression(Enumerable<Expression>.EmptyArray); + Construction = new Construction(constructionExpression); return; } - _argumentDataSources = argumentDataSources; - var variables = default(List<ParameterExpression>); - var argumentValues = new List<Expression>(ParameterCount); + var argumentValues = new List<Expression>(info.ParameterCount); var condition = default(Expression); - foreach (var argumentDataSource in argumentDataSources) + foreach (var dataSources in info.ArgumentDataSources) { - var dataSources = argumentDataSource.Item2; - if (dataSources.Variables.Any()) { if (variables == null) @@ -295,15 +470,13 @@ public ConstructionData( } } - argumentValues.Add(dataSources.ValueExpression); + argumentValues.Add(dataSources.BuildValue()); - if (!argumentDataSource.Item1.IsComplex || !dataSources.IsConditional) + if (info.IsUnconditional) { continue; } - IsUnconditional = false; - var dataSourceCondition = BuildConditions(dataSources); if (condition == null) @@ -315,31 +488,14 @@ public ConstructionData( condition = Expression.AndAlso(condition, dataSourceCondition); } - constructionExpression = constructionFactory.Invoke(invokable, argumentValues); + constructionExpression = info.GetConstructionExpression(argumentValues); Construction = variables.NoneOrNull() - ? new Construction(this, constructionExpression, condition) - : new Construction(this, Expression.Block(variables, constructionExpression), condition); + ? new Construction(constructionExpression, condition) + : new Construction(Expression.Block(variables, constructionExpression), condition); } - private static Tuple<QualifiedMember, DataSourceSet>[] GetArgumentDataSources(TInvokable invokable, ConstructionKey key) - { - return invokable - .GetParameters() - .ProjectToArray(p => - { - var parameterMapperData = new ChildMemberMapperData( - key.MappingData.MapperData.TargetMember.Append(Member.ConstructorParameter(p)), - key.MappingData.MapperData); - - var memberMappingData = key.MappingData.GetChildMappingData(parameterMapperData); - var dataSources = DataSourceFinder.FindFor(memberMappingData); - - return Tuple.Create(memberMappingData.MapperData.TargetMember, dataSources); - }); - } - - private static Expression BuildConditions(DataSourceSet dataSources) + private static Expression BuildConditions(IDataSourceSet dataSources) { var conditions = default(Expression); @@ -357,87 +513,54 @@ private static Expression BuildConditions(DataSourceSet dataSources) return conditions; } - public bool CanBeInvoked { get; } - - public bool IsUnconditional { get; } - - public int ParameterCount { get; } - - public int Priority { get; } - public Construction Construction { get; } - - public void AddTo(IList<Construction> constructions, ConstructionKey key) - { - if (ParameterCount > 0) - { - var dataSources = key.MappingData.MapperData.DataSourcesByTargetMember; - - foreach (var memberAndDataSourceSet in _argumentDataSources.Filter(ads => !dataSources.ContainsKey(ads.Item1))) - { - dataSources.Add(memberAndDataSourceSet.Item1, memberAndDataSourceSet.Item2); - } - } - - constructions.AddSorted(Construction); - } - } - - private interface IConstructionInfo - { - int ParameterCount { get; } - - int Priority { get; } } - private class Construction : IConditionallyChainable, IComparable<Construction> + private class Construction { + private readonly Expression _condition; private readonly Expression _construction; - private readonly bool _isConfigured; - private readonly IConstructionInfo _info; private ParameterExpression _mappingDataObject; public Construction(ConfiguredObjectFactory configuredFactory, IMemberMapperData mapperData) : this(configuredFactory.Create(mapperData), configuredFactory.GetConditionOrNull(mapperData)) { UsesMappingDataObjectParameter = configuredFactory.UsesMappingDataObjectParameter; - _isConfigured = true; } - public Construction(IConstructionInfo info, Expression construction, Expression condition = null) - : this(construction, condition) - { - _info = info; - } - - private Construction(IList<Construction> constructions) - : this(constructions.ReverseChain()) - { - UsesMappingDataObjectParameter = constructions.Any(c => c.UsesMappingDataObjectParameter); - } - - private Construction(Expression construction, Expression condition = null) + public Construction( + Expression construction, + Expression condition = null, + bool usesMappingDataObjectParameter = false) { _construction = construction; - Condition = condition; + _condition = condition; + UsesMappingDataObjectParameter = usesMappingDataObjectParameter; } #region Factory Methods - public static Construction NewStruct(Type type) + public static Construction For(IList<Construction> constructions, ConstructionKey key) { - var parameterlessNew = Expression.New(type); + if (constructions.HasOne()) + { + return constructions.First().With(key); + } - return new Construction(parameterlessNew); + var construction = new Construction( + ReverseChain(constructions), + usesMappingDataObjectParameter: constructions.Any(c => c.UsesMappingDataObjectParameter)); + + return construction.With(key); } - public static Construction For(IList<Construction> constructions, ConstructionKey key) + private static Expression ReverseChain(IList<Construction> constructions) { - var construction = constructions.HasOne() - ? constructions.First() - : new Construction(constructions); - - return construction.With(key); + return constructions.Chain( + cs => cs.Last(), + item => item._construction, + (valueSoFar, item) => Expression.Condition(item._condition, item._construction, valueSoFar), + i => i.Reverse()); } public Construction With(ConstructionKey key) @@ -448,47 +571,10 @@ public Construction With(ConstructionKey key) #endregion - public Expression PreCondition => null; - - public bool IsUnconditional => Condition == null; - - public Expression Condition { get; } - - Expression IConditionallyChainable.Value => _construction; - public bool UsesMappingDataObjectParameter { get; } public Expression GetConstruction(IObjectMappingData mappingData) => _construction.Replace(_mappingDataObject, mappingData.MapperData.MappingDataObject); - - public int CompareTo(Construction other) - { - // ReSharper disable once ImpureMethodCallOnReadonlyValueField - var isConfiguredComparison = other._isConfigured.CompareTo(_isConfigured); - - if (isConfiguredComparison != 0) - { - return isConfiguredComparison; - } - - var conditionalComparison = IsUnconditional.CompareTo(other.IsUnconditional); - - if (conditionalComparison != 0) - { - return conditionalComparison; - } - - var paramCountComparison = _info.ParameterCount.CompareTo(other._info.ParameterCount); - - if (paramCountComparison != 0) - { - return paramCountComparison; - } - - var priorityComparison = other._info.Priority.CompareTo(_info.Priority); - - return priorityComparison; - } } #endregion diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs index aab418bf8..a2762e877 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs @@ -1,15 +1,17 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; + using System.Linq; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using DataSources; + using DataSources.Factories; using Extensions.Internal; using Members; using NetStandardPolyfills; - using ReadableExpressions; using ReadableExpressions.Extensions; internal class ComplexTypeMappingExpressionFactory : MappingExpressionFactoryBase @@ -20,7 +22,7 @@ internal class ComplexTypeMappingExpressionFactory : MappingExpressionFactoryBas private readonly PopulationExpressionFactoryBase _multiStatementPopulationFactory; private readonly IList<ISourceShortCircuitFactory> _shortCircuitFactories; - private ComplexTypeMappingExpressionFactory() + public ComplexTypeMappingExpressionFactory() { _memberInitPopulationFactory = new MemberInitPopulationExpressionFactory(); _multiStatementPopulationFactory = new MultiStatementPopulationExpressionFactory(); @@ -31,57 +33,99 @@ private ComplexTypeMappingExpressionFactory() }; } - public override bool IsFor(IObjectMappingData mappingData) => true; - - protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) + protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out string reason) { if (mappingData.MapperData.TargetCouldBePopulated()) { // If a target complex type is readonly or unconstructable // we still try to map to it using an existing non-null value: - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } if (mappingData.IsTargetConstructable()) { - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } var targetType = mappingData.MapperData.TargetType; - if (targetType.IsAbstract() && DerivedTypesExistForTarget(mappingData)) + if (targetType.IsAbstract() && DerivedTypesExistForTarget(mappingData.MapperData)) { - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } - nullMappingBlock = Expression.Block( - ReadableExpression.Comment("Cannot construct an instance of " + targetType.GetFriendlyName()), - targetType.ToDefaultExpression()); - + reason = "Cannot construct an instance of " + targetType.GetFriendlyName(); return true; } - private static bool DerivedTypesExistForTarget(IObjectMappingData mappingData) + private static bool DerivedTypesExistForTarget(IMemberMapperData mapperData) { - var configuredImplementationTypePairs = mappingData - .MapperData + var configuredImplementationTypePairs = mapperData .MapperContext .UserConfigurations .DerivedTypes - .GetImplementationTypePairsFor(mappingData.MapperData, mappingData.MapperData.MapperContext); + .GetImplementationTypePairsFor(mapperData, mapperData.MapperContext); return configuredImplementationTypePairs.Any() || - mappingData.MapperData.GetDerivedTargetTypes().Any(); + mapperData.GetDerivedTargetTypes().Any(); } #region Short-Circuits - protected override IEnumerable<Expression> GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData) + protected override bool ShortCircuitMapping(MappingCreationContext context, out Expression mapping) + { + var derivedTypeDataSources = DerivedComplexTypeDataSourcesFactory.CreateFor(context.MappingData); + + if (derivedTypeDataSources.None()) + { + return base.ShortCircuitMapping(context, out mapping); + } + + var derivedTypeDataSourceSet = DataSourceSet.For( + derivedTypeDataSources, + context.MapperData, + ValueExpressionBuilders.ValueSequence); + + mapping = derivedTypeDataSourceSet.BuildValue(); + + if (derivedTypeDataSources.Last().IsConditional) + { + context.MappingExpressions.Add(mapping); + return false; + } + + var shortCircuitReturns = GetShortCircuitReturns(context.MappingData).ToArray(); + + if (shortCircuitReturns.Any()) + { + context.MappingExpressions.AddRange(shortCircuitReturns); + } + + if (mapping.NodeType == ExpressionType.Goto) + { + mapping = ((GotoExpression)mapping).Value; + context.MappingExpressions.Add(context.MapperData.GetReturnLabel(mapping)); + } + else + { + context.MappingExpressions.Add(mapping); + context.MappingExpressions.Add(context.MapperData.GetReturnLabel(mapping.Type.ToDefaultExpression())); + } + + mapping = Expression.Block(context.MappingExpressions); + return true; + } + + protected override IEnumerable<Expression> GetShortCircuitReturns(IObjectMappingData mappingData) { var mapperData = mappingData.MapperData; if (SourceObjectCouldBeNull(mapperData)) { + var returnNull = Expression.Return( + mapperData.ReturnLabelTarget, + mapperData.TargetType.ToDefaultExpression()); + yield return Expression.IfThen(mapperData.SourceObject.GetIsDefaultComparison(), returnNull); } @@ -146,13 +190,10 @@ private static Expression GetAlreadyMappedObjectShortCircuitOrNull(ObjectMapperD } private bool TryGetShortCircuitFactory(ObjectMapperData mapperData, out ISourceShortCircuitFactory applicableFactory) - => _shortCircuitFactories.TryFindMatch(f => f.IsFor(mapperData), out applicableFactory); + => _shortCircuitFactories.TryFindMatch(mapperData, (md, f) => f.IsFor(md), out applicableFactory); #endregion - protected override Expression GetDerivedTypeMappings(IObjectMappingData mappingData) - => DerivedComplexTypeMappingsFactory.CreateFor(mappingData); - protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context) { var expressionFactory = context.MapperData.UseMemberInitialisations() diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs b/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs new file mode 100644 index 000000000..1af3d52c5 --- /dev/null +++ b/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs @@ -0,0 +1,17 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes +{ + using Members; + + internal interface IBasicConstructionInfo + { + bool IsConfigured { get; } + + bool IsUnconditional { get; } + + int ParameterCount { get; } + + int Priority { get; } + + bool HasCtorParameterFor(Member targetMember); + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs index 0784408f6..14c5010a2 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs @@ -1,13 +1,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; - using Extensions.Internal; - using Members.Population; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members.Population; internal class MemberInitPopulationExpressionFactory : PopulationExpressionFactoryBase { @@ -18,9 +18,9 @@ protected override IEnumerable<Expression> GetPopulationExpressionsFor( yield return memberPopulator.GetPopulation(); } - protected override Expression GetNewObjectCreation(IObjectMappingData mappingData, IList<Expression> memberPopulations) + protected override Expression GetTargetObjectCreation(IObjectMappingData mappingData, IList<Expression> memberPopulations) { - var objectCreation = base.GetNewObjectCreation(mappingData, memberPopulations); + var objectCreation = base.GetTargetObjectCreation(mappingData, memberPopulations); if (objectCreation == null) { diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/MultiStatementPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/MultiStatementPopulationExpressionFactory.cs index a9e8cdb3e..91830fe5a 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/MultiStatementPopulationExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/MultiStatementPopulationExpressionFactory.cs @@ -1,13 +1,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; - using Members; - using Members.Population; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Members; + using Members.Population; using static CallbackPosition; internal class MultiStatementPopulationExpressionFactory : PopulationExpressionFactoryBase diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs index 8b2ffa94a..60d57499e 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -2,15 +2,15 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; using System.Linq; - using Extensions.Internal; - using Members; - using Members.Population; - using NetStandardPolyfills; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; + using Members.Population; + using NetStandardPolyfills; using static CallbackPosition; internal abstract class PopulationExpressionFactoryBase @@ -18,7 +18,7 @@ internal abstract class PopulationExpressionFactoryBase public IEnumerable<Expression> GetPopulation(MappingCreationContext context) { var mappingData = context.MappingData; - var mapperData = context.MapperData; + var mapperData = mappingData.MapperData; GetCreationCallbacks(context, out var preCreationCallback, out var postCreationCallback); @@ -26,16 +26,26 @@ public IEnumerable<Expression> GetPopulation(MappingCreationContext context) if (context.InstantiateLocalVariable && mapperData.Context.UseLocalVariable) { - yield return preCreationCallback; - - var assignCreatedObject = postCreationCallback != null; + if (preCreationCallback != null) + { + yield return preCreationCallback; + } + + var hasPostCreationCallback = postCreationCallback != null; + var assignCreatedObject = hasPostCreationCallback; yield return GetLocalVariableInstantiation(assignCreatedObject, populationsAndCallbacks, mappingData); - yield return postCreationCallback; + if (hasPostCreationCallback) + { + yield return postCreationCallback; + } } - yield return GetObjectRegistrationCallOrNull(mapperData); + if (IncludeObjectRegistration(mapperData)) + { + yield return GetObjectRegistrationCall(mapperData); + } foreach (var population in populationsAndCallbacks) { @@ -92,7 +102,7 @@ private Expression GetLocalVariableInstantiation( IObjectMappingData mappingData) { var localVariableValue = TargetObjectResolutionFactory.GetObjectResolution( - GetNewObjectCreation, + GetTargetObjectCreation, mappingData, memberPopulations, assignCreatedObject); @@ -100,24 +110,28 @@ private Expression GetLocalVariableInstantiation( return mappingData.MapperData.LocalVariable.AssignTo(localVariableValue); } - protected virtual Expression GetNewObjectCreation( + protected virtual Expression GetTargetObjectCreation( IObjectMappingData mappingData, IList<Expression> memberPopulations) { - return mappingData.GetTargetObjectCreation(); + return mappingData + .MapperData + .MapperContext + .ConstructionFactory + .GetTargetObjectCreation(mappingData); } #region Object Registration - private static Expression GetObjectRegistrationCallOrNull(ObjectMapperData mapperData) + private static bool IncludeObjectRegistration(ObjectMapperData mapperData) { - if (mapperData.TargetTypeWillNotBeMappedAgain || - !mapperData.CacheMappedObjects || - !mapperData.RuleSet.Settings.AllowObjectTracking) - { - return null; - } + return mapperData.CacheMappedObjects && + mapperData.RuleSet.Settings.AllowObjectTracking && + !mapperData.TargetTypeWillNotBeMappedAgain; + } + private static Expression GetObjectRegistrationCall(ObjectMapperData mapperData) + { var registerMethod = typeof(IObjectMappingDataUntyped) .GetPublicInstanceMethod(nameof(IObjectMappingDataUntyped.Register)) .MakeGenericMethod(mapperData.SourceType, mapperData.TargetType); diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs index eb0af0ebd..ba9114b4e 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/SourceDictionaryShortCircuitFactory.cs @@ -99,7 +99,7 @@ private static Expression GetFallbackValue(IObjectMappingData mappingData) return mappingData.MappingContext .RuleSet .FallbackDataSourceFactory - .Create(mappingData.MapperData) + .Invoke(mappingData.MapperData) .Value; } } diff --git a/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs b/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs deleted file mode 100644 index d3e5663a9..000000000 --- a/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs +++ /dev/null @@ -1,485 +0,0 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation -{ - using System; - using System.Collections.Generic; - using System.Linq; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - using Configuration; - using Extensions; - using Extensions.Internal; - using Members; - using NetStandardPolyfills; - - internal static class DerivedComplexTypeMappingsFactory - { - public static Expression CreateFor(IObjectMappingData declaredTypeMappingData) - { - var declaredTypeMapperData = declaredTypeMappingData.MapperData; - - if (DoNotMapDerivedTypes(declaredTypeMapperData)) - { - return Constants.EmptyExpression; - } - - var derivedSourceTypes = declaredTypeMapperData.RuleSet.Settings.CheckDerivedSourceTypes - ? declaredTypeMapperData.GetDerivedSourceTypes() - : Constants.EmptyTypeArray; - - var derivedTargetTypes = GetDerivedTargetTypesIfNecessary(declaredTypeMappingData); - var derivedTypePairs = GetTypePairsFor(declaredTypeMapperData, declaredTypeMapperData); - - if (derivedSourceTypes.None() && derivedTargetTypes.None() && derivedTypePairs.None()) - { - return Constants.EmptyExpression; - } - - var derivedTypeMappingExpressions = new List<Expression>(); - - AddDeclaredSourceTypeMappings( - derivedTypePairs, - declaredTypeMappingData, - derivedTypeMappingExpressions, - out var declaredTypeHasUnconditionalTypePair); - - if (declaredTypeHasUnconditionalTypePair && derivedSourceTypes.None()) - { - return derivedTypeMappingExpressions.First(); - } - - var typedObjectVariables = new List<ParameterExpression>(); - - AddDerivedSourceTypeMappings( - derivedSourceTypes, - declaredTypeMappingData, - typedObjectVariables, - derivedTypeMappingExpressions); - - AddDerivedTargetTypeMappings( - declaredTypeMappingData, - derivedTargetTypes, - derivedTypeMappingExpressions); - - if (derivedTypeMappingExpressions.None()) - { - return Constants.EmptyExpression; - } - - return typedObjectVariables.Any() - ? Expression.Block(typedObjectVariables, derivedTypeMappingExpressions) - : derivedTypeMappingExpressions.HasOne() - ? derivedTypeMappingExpressions.First() - : Expression.Block(derivedTypeMappingExpressions); - } - - private static bool DoNotMapDerivedTypes(IMemberMapperData mapperData) - { - if (mapperData.Context.IsForDerivedType) - { - return !mapperData.TargetType.IsInterface(); - } - - return mapperData.HasSameSourceAsParent(); - } - - private static ICollection<Type> GetDerivedTargetTypesIfNecessary(IObjectMappingData mappingData) - { - if (mappingData.MapperData.TargetIsDefinitelyUnpopulated()) - { - return Constants.EmptyTypeArray; - } - - return mappingData.MapperData.GetDerivedTargetTypes(); - } - - private static void AddDeclaredSourceTypeMappings( - IEnumerable<DerivedTypePair> derivedTypePairs, - IObjectMappingData declaredTypeMappingData, - ICollection<Expression> derivedTypeMappingExpressions, - out bool declaredTypeHasUnconditionalTypePair) - { - var declaredTypeMapperData = declaredTypeMappingData.MapperData; - - derivedTypePairs = derivedTypePairs - .OrderBy(tp => tp.DerivedSourceType, TypeComparer.MostToLeastDerived); - - foreach (var derivedTypePair in derivedTypePairs) - { - var condition = GetTypePairCondition(derivedTypePair, declaredTypeMapperData); - - var sourceValue = GetDerivedTypeSourceValue( - derivedTypePair, - declaredTypeMappingData, - out var sourceValueCondition); - - var derivedTypeMapping = DerivedMappingFactory.GetDerivedTypeMapping( - declaredTypeMappingData, - sourceValue, - derivedTypePair.DerivedTargetType); - - if (sourceValueCondition != null) - { - derivedTypeMapping = Expression.Condition( - sourceValueCondition, - derivedTypeMapping, - derivedTypeMapping.Type.ToDefaultExpression()); - } - - var returnMappingResult = Expression.Return(declaredTypeMapperData.ReturnLabelTarget, derivedTypeMapping); - - declaredTypeHasUnconditionalTypePair = (condition == null); - - if (declaredTypeHasUnconditionalTypePair) - { - derivedTypeMappingExpressions.Add(returnMappingResult); - return; - } - - var ifConditionThenMap = Expression.IfThen(condition, returnMappingResult); - - derivedTypeMappingExpressions.Add(ifConditionThenMap); - } - - declaredTypeHasUnconditionalTypePair = false; - } - - private static Expression GetTypePairCondition(DerivedTypePair derivedTypePair, IMemberMapperData declaredTypeMapperData) - { - var condition = GetTargetValidCheckOrNull(derivedTypePair.DerivedTargetType, declaredTypeMapperData); - - if (!derivedTypePair.HasConfiguredCondition) - { - return condition; - } - - var pairCondition = derivedTypePair.GetConditionOrNull(declaredTypeMapperData); - - return (condition != null) ? Expression.AndAlso(pairCondition, condition) : pairCondition; - } - - private static Expression GetDerivedTypeSourceValue( - DerivedTypePair derivedTypePair, - IObjectMappingData declaredTypeMappingData, - out Expression sourceValueCondition) - { - if (!derivedTypePair.IsImplementationPairing) - { - sourceValueCondition = null; - return declaredTypeMappingData.MapperData.SourceObject; - } - - var implementationMappingData = declaredTypeMappingData - .WithTypes(derivedTypePair.DerivedSourceType, derivedTypePair.DerivedTargetType); - - if (implementationMappingData.IsTargetConstructable()) - { - sourceValueCondition = null; - return declaredTypeMappingData.MapperData.SourceObject; - } - - // Derived Type is an implementation Type for an unconstructable target Type, - // and is itself unconstructable; only way we get here is if a ToTarget data - // source has been configured: - var toTargetDataSource = implementationMappingData - .GetToTargetDataSourceOrNullForTargetType(); - - sourceValueCondition = toTargetDataSource.IsConditional - ? toTargetDataSource.Condition.Replace( - implementationMappingData.MapperData.SourceObject, - declaredTypeMappingData.MapperData.SourceObject, - ExpressionEvaluation.Equivalator) - : null; - - return toTargetDataSource.Value.Replace( - implementationMappingData.MapperData.SourceObject, - declaredTypeMappingData.MapperData.SourceObject, - ExpressionEvaluation.Equivalator); - } - - private static void AddDerivedSourceTypeMappings( - ICollection<Type> derivedSourceTypes, - IObjectMappingData declaredTypeMappingData, - ICollection<ParameterExpression> typedObjectVariables, - IList<Expression> derivedTypeMappingExpressions) - { - if (derivedSourceTypes.None()) - { - return; - } - - var declaredTypeMapperData = declaredTypeMappingData.MapperData; - var insertionOffset = derivedTypeMappingExpressions.Count; - - var orderedDerivedSourceTypes = derivedSourceTypes - .OrderBy(t => t, TypeComparer.MostToLeastDerived); - - foreach (var derivedSourceType in orderedDerivedSourceTypes) - { - var derivedSourceCheck = new DerivedSourceTypeCheck(derivedSourceType); - var typedVariableAssignment = derivedSourceCheck.GetTypedVariableAssignment(declaredTypeMapperData); - - typedObjectVariables.Add(derivedSourceCheck.TypedVariable); - derivedTypeMappingExpressions.Insert(typedVariableAssignment, insertionOffset); - - var targetType = declaredTypeMapperData.TargetType.GetRuntimeTargetType(derivedSourceType); - - var outerCondition = derivedSourceCheck.TypeCheck; - outerCondition = AppendTargetValidCheckIfAppropriate(outerCondition, targetType, declaredTypeMapperData); - - var derivedTypePairs = GetTypePairsFor(derivedSourceType, targetType, declaredTypeMapperData); - - Expression ifSourceVariableIsDerivedTypeThenMap; - - if (derivedTypePairs.None()) - { - ifSourceVariableIsDerivedTypeThenMap = GetIfConditionThenMapExpression( - declaredTypeMappingData, - outerCondition, - derivedSourceCheck.TypedVariable, - targetType); - - derivedTypeMappingExpressions.Insert(ifSourceVariableIsDerivedTypeThenMap, insertionOffset); - continue; - } - - var groupedTypePairs = derivedTypePairs - .GroupBy(tp => tp.DerivedTargetType) - .Project(group => new TypePairGroup(group)) - .OrderBy(tp => tp.DerivedTargetType, TypeComparer.MostToLeastDerived) - .ToArray(); - - var unconditionalDerivedTargetTypeMapping = groupedTypePairs - .Filter(tpg => tpg.TypePairs.None(tp => tp.HasConfiguredCondition)) - .Project(tpg => new - { - tpg.DerivedTargetType, - TypePairsCondition = GetTargetValidCheckOrNull(tpg.DerivedTargetType, declaredTypeMapperData) - }) - .FirstOrDefault(d => d.TypePairsCondition == null); - - if (unconditionalDerivedTargetTypeMapping != null) - { - ifSourceVariableIsDerivedTypeThenMap = GetIfConditionThenMapExpression( - declaredTypeMappingData, - outerCondition, - derivedSourceCheck.TypedVariable, - unconditionalDerivedTargetTypeMapping.DerivedTargetType); - - derivedTypeMappingExpressions.Insert(ifSourceVariableIsDerivedTypeThenMap, insertionOffset); - continue; - } - - ifSourceVariableIsDerivedTypeThenMap = GetMapFromConditionOrDefaultExpression( - declaredTypeMappingData, - outerCondition, - derivedSourceCheck.TypedVariable, - groupedTypePairs, - targetType); - - derivedTypeMappingExpressions.Insert(ifSourceVariableIsDerivedTypeThenMap, insertionOffset); - } - } - - private static void AddDerivedTargetTypeMappings( - IObjectMappingData declaredTypeMappingData, - IEnumerable<Type> derivedTargetTypes, - ICollection<Expression> derivedTypeMappingExpressions) - { - var declaredTypeMapperData = declaredTypeMappingData.MapperData; - - derivedTargetTypes = derivedTargetTypes - .OrderBy(t => t, TypeComparer.MostToLeastDerived); - - foreach (var derivedTargetType in derivedTargetTypes) - { - var targetTypeCondition = GetTargetIsDerivedTypeCheck(derivedTargetType, declaredTypeMapperData); - - var ifDerivedTargetTypeThenMap = GetIfConditionThenMapExpression( - declaredTypeMappingData, - targetTypeCondition, - declaredTypeMapperData.SourceObject, - derivedTargetType); - - derivedTypeMappingExpressions.AddUnlessNullOrEmpty(ifDerivedTargetTypeThenMap); - } - } - - private static Expression GetIfConditionThenMapExpression( - IObjectMappingData mappingData, - Expression condition, - Expression sourceValue, - Type targetType) - { - var returnMappingResult = GetReturnMappingResultExpression(mappingData, sourceValue, targetType); - - if (returnMappingResult == Constants.EmptyExpression) - { - return Constants.EmptyExpression; - } - - var ifConditionThenMap = Expression.IfThen(condition, returnMappingResult); - - return ifConditionThenMap; - } - - private static Expression GetReturnMappingResultExpression( - IObjectMappingData mappingData, - Expression sourceValue, - Type targetType) - { - var mapping = DerivedMappingFactory.GetDerivedTypeMapping(mappingData, sourceValue, targetType); - - if (mapping == Constants.EmptyExpression) - { - return Constants.EmptyExpression; - } - - var returnMappingResult = Expression.Return(mappingData.MapperData.ReturnLabelTarget, mapping); - - return returnMappingResult; - } - - private static Expression GetMapFromConditionOrDefaultExpression( - IObjectMappingData mappingData, - Expression condition, - Expression typedVariable, - IEnumerable<TypePairGroup> typePairGroups, - Type targetType) - { - var mappingExpressions = new List<Expression>(); - - foreach (var typePairGroup in typePairGroups) - { - var typePairsCondition = - GetTypePairsCondition(typePairGroup.TypePairs, mappingData.MapperData) ?? - GetTargetValidCheckOrNull(typePairGroup.DerivedTargetType, mappingData.MapperData); - - var ifTypePairsConditionThenMap = GetIfConditionThenMapExpression( - mappingData, - typePairsCondition, - typedVariable, - typePairGroup.DerivedTargetType); - - mappingExpressions.Add(ifTypePairsConditionThenMap); - } - - var mapToDeclaredTargetType = - GetReturnMappingResultExpression(mappingData, typedVariable, targetType); - - mappingExpressions.Add(mapToDeclaredTargetType); - - var ifSourceVariableIsDerivedTypeThenMap = Expression - .IfThen(condition, Expression.Block(mappingExpressions)); - - return ifSourceVariableIsDerivedTypeThenMap; - } - - private static IList<DerivedTypePair> GetTypePairsFor( - Type derivedSourceType, - Type targetType, - IMemberMapperData mapperData) - { - var pairTestMapperData = new BasicMapperData( - mapperData.RuleSet, - derivedSourceType, - targetType, - mapperData.TargetMember.WithType(targetType), - mapperData.Parent); - - return GetTypePairsFor(pairTestMapperData, mapperData); - } - - private static IList<DerivedTypePair> GetTypePairsFor(IBasicMapperData pairTestMapperData, IMemberMapperData mapperData) - { - var derivedTypePairs = mapperData.MapperContext.UserConfigurations - .DerivedTypes - .GetDerivedTypePairsFor(pairTestMapperData, mapperData.MapperContext); - - return derivedTypePairs; - } - - private static Expression GetTypePairsCondition( - IEnumerable<DerivedTypePair> derivedTypePairs, - IMemberMapperData mapperData) - { - var conditionalPairs = derivedTypePairs - .Filter(pair => pair.HasConfiguredCondition) - .ToArray(); - - var pairConditions = conditionalPairs.Chain( - firstPair => firstPair.GetConditionOrNull(mapperData), - (conditionSoFar, pair) => Expression.OrElse( - conditionSoFar, - pair.GetConditionOrNull(mapperData))); - - return pairConditions; - } - - private static Expression AppendTargetValidCheckIfAppropriate( - Expression condition, - Type targetType, - IMemberMapperData mapperData) - { - if (targetType == mapperData.TargetType) - { - return condition; - } - - var targetIsValid = GetTargetValidCheckOrNull(targetType, mapperData); - - if (targetIsValid == null) - { - return condition; - } - - condition = Expression.AndAlso(condition, targetIsValid); - - return condition; - } - - private static Expression GetTargetValidCheckOrNull(Type targetType, IMemberMapperData mapperData) - { - if (!mapperData.TargetMember.IsReadable || mapperData.TargetIsDefinitelyUnpopulated()) - { - return null; - } - - var targetIsOfDerivedType = GetTargetIsDerivedTypeCheck(targetType, mapperData); - - if (mapperData.TargetIsDefinitelyPopulated()) - { - return targetIsOfDerivedType; - } - - var targetIsNull = mapperData.TargetObject.GetIsDefaultComparison(); - var targetIsValid = Expression.OrElse(targetIsNull, targetIsOfDerivedType); - - return targetIsValid; - } - - private static Expression GetTargetIsDerivedTypeCheck(Type targetType, IMemberMapperData mapperData) - => Expression.TypeIs(mapperData.TargetObject, targetType); - - private static void Insert(this IList<Expression> mappingExpressions, Expression mapping, int insertionOffset) - { - var insertionIndex = mappingExpressions.Count - insertionOffset; - mappingExpressions.Insert(insertionIndex, mapping); - } - - private class TypePairGroup - { - public TypePairGroup(IGrouping<Type, DerivedTypePair> typePairGroup) - { - DerivedTargetType = typePairGroup.Key; - TypePairs = typePairGroup.ToArray(); - } - - public Type DerivedTargetType { get; } - - public IList<DerivedTypePair> TypePairs { get; } - } - } -} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs b/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs index 3b2cb6a87..580d0ef09 100644 --- a/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs +++ b/AgileMapper/ObjectPopulation/DerivedMappingFactory.cs @@ -16,14 +16,32 @@ public static Expression GetDerivedTypeMapping( Expression sourceValue, Type targetType) { + return GetDerivedTypeMapping( + declaredTypeMappingData, + sourceValue, + targetType, + out _); + } + + public static Expression GetDerivedTypeMapping( + IObjectMappingData declaredTypeMappingData, + Expression sourceValue, + Type targetType, + out IObjectMappingData derivedTypeMappingData) + { + derivedTypeMappingData = declaredTypeMappingData.WithTypes(sourceValue.Type, targetType); + var declaredTypeMapperData = declaredTypeMappingData.MapperData; + if (DerivedSourceTypeIsUnconditionallyIgnored(derivedTypeMappingData)) + { + return declaredTypeMapperData.TargetObject.GetConversionTo(targetType); + } + var targetValue = declaredTypeMapperData.TargetMember.IsReadable ? declaredTypeMapperData.TargetObject.GetConversionTo(targetType) : targetType.ToDefaultExpression(); - var derivedTypeMappingData = declaredTypeMappingData.WithTypes(sourceValue.Type, targetType); - if (declaredTypeMappingData.IsRoot) { return GetDerivedTypeRootMapping(derivedTypeMappingData, sourceValue, targetValue); @@ -37,6 +55,22 @@ public static Expression GetDerivedTypeMapping( return GetDerivedTypeChildMapping(derivedTypeMappingData, sourceValue, targetValue); } + private static bool DerivedSourceTypeIsUnconditionallyIgnored(IObjectMappingData derivedTypeMappingData) + { + var derivedTypeMapperData = derivedTypeMappingData.MapperData; + var userConfigurations = derivedTypeMapperData.MapperContext.UserConfigurations; + + if (!userConfigurations.HasSourceMemberIgnores) + { + return false; + } + + var derivedTypeSourceMemberIgnore = userConfigurations + .GetSourceMemberIgnoreOrNull(derivedTypeMapperData); + + return derivedTypeSourceMemberIgnore?.HasConfiguredCondition == false; + } + private static Expression GetDerivedTypeRootMapping( IObjectMappingData derivedTypeMappingData, Expression sourceValue, diff --git a/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs b/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs index 23f7b532d..a8d4734b4 100644 --- a/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs +++ b/AgileMapper/ObjectPopulation/DerivedSourceTypeCheck.cs @@ -1,26 +1,26 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System; - using Extensions.Internal; - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; internal class DerivedSourceTypeCheck { - private readonly Type _derivedSourceType; - public DerivedSourceTypeCheck(Type derivedSourceType) { - _derivedSourceType = derivedSourceType; + DerivedSourceType = derivedSourceType; var typedVariableName = "source" + derivedSourceType.GetVariableNameInPascalCase(); TypedVariable = Expression.Variable(derivedSourceType, typedVariableName); } + public Type DerivedSourceType { get; } + public ParameterExpression TypedVariable { get; } public Expression GetTypedVariableAssignment(IMemberMapperData declaredTypeMapperData) @@ -28,7 +28,7 @@ public Expression GetTypedVariableAssignment(IMemberMapperData declaredTypeMappe public Expression GetTypedVariableAssignment(Expression sourceObject) { - var typeAsConversion = Expression.TypeAs(sourceObject, _derivedSourceType); + var typeAsConversion = Expression.TypeAs(sourceObject, DerivedSourceType); var typedVariableAssignment = TypedVariable.AssignTo(typeAsConversion); return typedVariableAssignment; diff --git a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs index a9726b3ad..584492528 100644 --- a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs @@ -3,9 +3,14 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System; using System.Collections.Generic; using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using System.Reflection; using ComplexTypes; - using DataSources; + using DataSources.Factories; using Enumerables.Dictionaries; using Extensions; using Extensions.Internal; @@ -13,21 +18,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Members.Dictionaries; using Members.Population; using NetStandardPolyfills; - using ReadableExpressions; using ReadableExpressions.Extensions; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif internal class DictionaryMappingExpressionFactory : MappingExpressionFactoryBase { - public static readonly MappingExpressionFactoryBase Instance = new DictionaryMappingExpressionFactory(); - private readonly MemberPopulatorFactory _memberPopulatorFactory; - private DictionaryMappingExpressionFactory() + public DictionaryMappingExpressionFactory() { _memberPopulatorFactory = new MemberPopulatorFactory(GetAllTargetMembers); } @@ -41,7 +38,7 @@ private static IEnumerable<QualifiedMember> GetAllTargetMembers(ObjectMapperData var configuredDataSourceFactories = mapperData.MapperContext .UserConfigurations .QueryDataSourceFactories<ConfiguredDictionaryEntryDataSourceFactory>() - .Filter(dsf => dsf.IsFor(mapperData)) + .Filter(mapperData, (md, dsf) => dsf.IsFor(md)) .ToArray(); if (configuredDataSourceFactories.None()) @@ -57,7 +54,7 @@ private static IEnumerable<QualifiedMember> GetAllTargetMembers(ObjectMapperData return allTargetMembers; } - private static IEnumerable<DictionaryTargetMember> EnumerateAllTargetMembers(ObjectMapperData mapperData) + private static IEnumerable<QualifiedMember> EnumerateAllTargetMembers(ObjectMapperData mapperData) { var sourceMembers = GlobalContext.Instance.MemberCache.GetSourceMembers(mapperData.SourceType); var targetDictionaryMember = (DictionaryTargetMember)mapperData.TargetMember; @@ -208,19 +205,19 @@ private static IEnumerable<Member> GetNestedFlattenedMembers( .ToArray(); } - private static DictionaryTargetMember[] GetConfiguredTargetMembers( + private static QualifiedMember[] GetConfiguredTargetMembers( IEnumerable<ConfiguredDictionaryEntryDataSourceFactory> configuredDataSourceFactories, - IList<DictionaryTargetMember> targetMembersFromSource) + IList<QualifiedMember> targetMembersFromSource) { return configuredDataSourceFactories .GroupBy(dsf => dsf.TargetDictionaryEntryMember.Name) - .Project(group => + .Project(targetMembersFromSource, (tmfs, group) => { - var factory = group.First(); - var targetMember = factory.TargetDictionaryEntryMember; + QualifiedMember targetMember = group.First().TargetDictionaryEntryMember; - targetMember.IsCustom = targetMembersFromSource.None( - sourceMember => sourceMember.RegistrationName == targetMember.Name); + targetMember.IsCustom = tmfs.None( + targetMember.Name, + (tmn, sourceMember) => sourceMember.RegistrationName == tmn); return targetMember.IsCustom ? targetMember : null; }) @@ -230,58 +227,35 @@ private static DictionaryTargetMember[] GetConfiguredTargetMembers( #endregion - public override bool IsFor(IObjectMappingData mappingData) - { - if (mappingData.MapperData.TargetMember.IsDictionary) - { - return true; - } - - if (mappingData.IsRoot) - { - return false; - } - - if (!(mappingData.MapperData.TargetMember is DictionaryTargetMember dictionaryMember)) - { - return false; - } - - if (dictionaryMember.HasSimpleEntries) - { - return true; - } - - return dictionaryMember.HasObjectEntries && !mappingData.IsStandalone(); - } - - protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) + protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out string reason) { if (mappingData.MappingTypes.SourceType.IsDictionary()) { - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } var targetMember = (DictionaryTargetMember)mappingData.MapperData.TargetMember; if ((targetMember.KeyType == typeof(string)) || (targetMember.KeyType == typeof(object))) { - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } - nullMappingBlock = Expression.Block( - ReadableExpression.Comment("Only string- or object-keyed Dictionaries are supported"), - mappingData.MapperData.GetFallbackCollectionValue()); - + reason = "Only string- or object-keyed Dictionaries are supported"; return true; } + protected override Expression GetNullMappingFallbackValue(IMemberMapperData mapperData) + => mapperData.GetFallbackCollectionValue(); + protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context) { + Expression population; + if (!context.MapperData.TargetMember.IsDictionary) { - yield return GetDictionaryPopulation(context.MappingData); - yield break; + population = GetDictionaryPopulation(context.MappingData); + goto ReturnPopulation; } var assignmentFactory = GetDictionaryAssignmentFactoryOrNull(context, out var useAssignmentOnly); @@ -292,11 +266,19 @@ protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationCo yield break; } - var population = GetDictionaryPopulation(context.MappingData); + population = GetDictionaryPopulation(context.MappingData); var assignment = assignmentFactory?.Invoke(context.MappingData); - yield return assignment; - yield return population; + if (assignment != null) + { + yield return assignment; + } + + ReturnPopulation: + if (population != null) + { + yield return population; + } } private static Func<IObjectMappingData, Expression> GetDictionaryAssignmentFactoryOrNull( diff --git a/AgileMapper/ObjectPopulation/EnumMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/EnumMappingExpressionFactory.cs new file mode 100644 index 000000000..90e680005 --- /dev/null +++ b/AgileMapper/ObjectPopulation/EnumMappingExpressionFactory.cs @@ -0,0 +1,42 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + using System.Collections.Generic; + using System.Globalization; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Extensions.Internal; + using Members; + using ReadableExpressions.Extensions; + + internal class EnumMappingExpressionFactory : MappingExpressionFactoryBase + { + protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out string reason) + { + var mapperData = mappingData.MapperData; + + if (mapperData.CanConvert(mapperData.SourceType, mapperData.TargetType)) + { + return base.TargetCannotBeMapped(mappingData, out reason); + } + + reason = string.Format( + CultureInfo.InvariantCulture, + "Unable to convert source Type '{0}' to target enum Type '{1}'", + mapperData.SourceType.GetFriendlyName(), + mapperData.TargetType.GetFriendlyName()); + + return true; + } + + protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context) + { + var mapperData = context.MapperData; + var enumMapping = mapperData.GetValueConversion(mapperData.SourceObject, mapperData.TargetType); + + yield return context.MapperData.LocalVariable.AssignTo(enumMapping); + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs index b527a8ad0..f2e2570a2 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs @@ -6,9 +6,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Linq.Expressions; #endif - internal struct CopySourceEnumerablePopulationStrategy : IEnumerablePopulationStrategy + internal static class CopySourceEnumerablePopulationStrategy { - public Expression GetPopulation(EnumerablePopulationBuilder builder, IObjectMappingData enumerableMappingData) + public static Expression Create(EnumerablePopulationBuilder builder, IObjectMappingData enumerableMappingData) { builder.AssignSourceVariableFromSourceObject(); builder.AssignTargetVariable(); diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index e4a51e34e..860b6c014 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -98,7 +98,7 @@ private Expression AssignDictionaryEntryFromKeyValuePair( var targetEntryAssignmentBlock = (BlockExpression)targetEntryAssignment; return Expression.Block( - targetEntryAssignmentBlock.Variables.Prepend(keyVariable), + targetEntryAssignmentBlock.Variables.Append(keyVariable), targetEntryAssignmentBlock.Expressions.Prepend(keyAssignment)); } @@ -237,7 +237,7 @@ private Expression GetPopulation( { var elementMapping = loopData.GetElementMapping(dictionaryMappingData); - if (dictionaryEntryMember.HasKey && + if (dictionaryEntryMember.HasKey && dictionaryEntryMember.CheckExistingElementValue && dictionaryMappingData.MapperData.TargetCouldBePopulated()) { @@ -256,14 +256,14 @@ private Expression GetPopulation( IObjectMappingData mappingData) { var elementMapperData = new ChildMemberMapperData(dictionaryEntryMember, MapperData); - var elementMappingData = mappingData.GetChildMappingData(elementMapperData); var sourceMember = mappingData.MapperData.SourceMember; var mappingDataSource = new AdHocDataSource(sourceMember, elementMapping); - var mappingDataSources = new DataSourceSet(elementMapperData, mappingDataSource); + var mappingDataSources = DataSourceSet.For(mappingDataSource, elementMapperData); - var memberPopulation = MemberPopulator.WithoutRegistration(elementMappingData, mappingDataSources); - var populationExpression = memberPopulation.GetPopulation(); + var populationExpression = MemberPopulator + .WithoutRegistration(mappingDataSources) + .GetPopulation(); return populationExpression; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs index 4d8903c55..8d76bcbfc 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceInstanceDictionaryAdapter.cs @@ -32,7 +32,7 @@ public override Expression GetSourceValues() var returnEmpty = Expression.Return(returnLabel, emptyTarget); var ifKeyNotFoundShortCircuit = GetKeyNotFoundShortCircuit(returnEmpty); - var isValueIsNullShortCircuit = GetNullValueEntryShortCircuitIfAppropriate(returnEmpty); + var isValueIsNullShortCircuit = DictionaryVariables.GetEntryValueAssignment(); var sourceValueBlock = Expression.Block( new[] { DictionaryVariables.Key, DictionaryVariables.Value }, @@ -46,21 +46,6 @@ public override Expression GetSourceValues() public Expression GetKeyNotFoundShortCircuit(Expression shortCircuitReturn) => DictionaryVariables.GetKeyNotFoundShortCircuit(shortCircuitReturn); - private Expression GetNullValueEntryShortCircuitIfAppropriate(Expression shortCircuitReturn) - { - var valueAssignment = DictionaryVariables.GetEntryValueAssignment(); - - if (shortCircuitReturn.Type.CannotBeNull()) - { - return valueAssignment; - } - - var valueIsNull = DictionaryVariables.Value.GetIsDefaultComparison(); - var ifValueNullShortCircuit = Expression.IfThen(valueIsNull, shortCircuitReturn); - - return Expression.Block(valueAssignment, ifValueNullShortCircuit); - } - public Expression GetEntryValueAccess() => DictionaryVariables.GetEntryValueAccess(); public Expression GetSourceCountAccess() => _defaultAdapter.GetSourceCountAccess(); diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs index fe7240217..6a6255bcd 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryPopulationLoopData.cs @@ -1,12 +1,12 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables.Dictionaries { - using DataSources; - using Extensions.Internal; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using DataSources; + using Extensions.Internal; internal class SourceObjectDictionaryPopulationLoopData : IPopulationLoopData { @@ -73,8 +73,8 @@ public Expression Adapt(LoopExpression loop) DisposeEnumeratorIfNecessary); return Expression.Block( - new[] { _sourceEnumerableFound }.Append(enumerableLoopBlock.Variables), - new[] { assignSourceEnumerableFound }.Append(enumerableLoopBlock.Expressions)); + enumerableLoopBlock.Variables.Append(_sourceEnumerableFound), + enumerableLoopBlock.Expressions.Prepend(assignSourceEnumerableFound)); } public static BinaryExpression GetSourceEnumerableFoundTest( @@ -85,12 +85,7 @@ public static BinaryExpression GetSourceEnumerableFoundTest( } private Expression GetEnumeratorIfNecessary(Expression getEnumeratorCall) - { - return Expression.Condition( - _sourceEnumerableFound, - getEnumeratorCall, - getEnumeratorCall.Type.ToDefaultExpression()); - } + => getEnumeratorCall.ToIfFalseDefaultCondition(_sourceEnumerableFound); private Expression DisposeEnumeratorIfNecessary(Expression disposeEnumeratorCall) => Expression.IfThen(_sourceEnumerableFound, disposeEnumeratorCall); diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs index 4110298b1..e86d2172d 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs @@ -1,41 +1,32 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables { using System.Collections.Generic; - using Extensions.Internal; - using Members; - using ReadableExpressions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; internal class EnumerableMappingExpressionFactory : MappingExpressionFactoryBase { - public static readonly MappingExpressionFactoryBase Instance = new EnumerableMappingExpressionFactory(); - - public override bool IsFor(IObjectMappingData mappingData) - => mappingData.MapperData.TargetMember.IsEnumerable; - - protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) + protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out string reason) { var mapperData = mappingData.MapperData; if (HasCompatibleSourceMember(mapperData)) { - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } if (HasConfiguredToTargetDataSources(mapperData, out var configuredRootDataSources) && configuredRootDataSources.Any(ds => ds.SourceMember.IsEnumerable)) { - return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); + return base.TargetCannotBeMapped(mappingData, out reason); } - nullMappingBlock = Expression.Block( - ReadableExpression.Comment("No source enumerable available"), - mapperData.GetFallbackCollectionValue()); - + reason = "No source enumerable available"; return true; } @@ -43,10 +34,13 @@ private static bool HasCompatibleSourceMember(IMemberMapperData mapperData) { return mapperData.SourceMember.IsEnumerable && mapperData.CanConvert( - mapperData.SourceMember.GetElementMember().Type, + mapperData.SourceMember.GetElementMember().Type, mapperData.TargetMember.GetElementMember().Type); } + protected override Expression GetNullMappingFallbackValue(IMemberMapperData mapperData) + => mapperData.GetFallbackCollectionValue(); + protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context) { if (!HasCompatibleSourceMember(context.MapperData)) @@ -62,7 +56,7 @@ protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationCo yield break; } - yield return context.RuleSet.EnumerablePopulationStrategy.GetPopulation( + yield return context.RuleSet.EnumerablePopulationStrategy.Invoke( context.MapperData.EnumerablePopulationBuilder, context.MappingData); } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index b185fd4d6..716d5716f 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -4,17 +4,17 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using Caching; using Extensions; using Extensions.Internal; using Members; using NetStandardPolyfills; using ReadableExpressions.Extensions; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif internal class EnumerablePopulationBuilder { @@ -22,7 +22,6 @@ internal class EnumerablePopulationBuilder public static readonly MethodInfo EnumerableSelectWithoutIndexMethod; public static readonly MethodInfo ProjectWithoutIndexMethod; - private static readonly MethodInfo _projectWithIndexMethod; private static readonly MethodInfo _queryableSelectMethod; private static readonly MethodInfo _forEachMethod; private static readonly MethodInfo _forEachTupleMethod; @@ -56,11 +55,10 @@ public EnumerablePopulationBuilder(ObjectMapperData mapperData) static EnumerablePopulationBuilder() { var projectMethods = typeof(PublicEnumerableExtensions) - .GetPublicStaticMethods("Project") + .GetPublicStaticMethods(nameof(PublicEnumerableExtensions.Project)) .ToArray(); ProjectWithoutIndexMethod = projectMethods.First(); - _projectWithIndexMethod = projectMethods.Last(); EnumerableSelectWithoutIndexMethod = typeof(Enumerable) .GetPublicStaticMethods(nameof(Enumerable.Select)) @@ -120,28 +118,21 @@ public static implicit operator BlockExpression(EnumerablePopulationBuilder buil #endregion - public static MethodInfo GetProjectionMethodFor(IMemberMapperData mapperData) - { - var counterRequired = false; - - return GetProjectionMethodFor(mapperData, ref counterRequired); - } + private MethodInfo GetProjectionMethod() => GetProjectionMethodFor(MapperData); - private static MethodInfo GetProjectionMethodFor(IMemberMapperData mapperData, ref bool counterRequired) + public static MethodInfo GetProjectionMethodFor(IMemberMapperData mapperData) { if (mapperData.SourceType.IsQueryable()) { - counterRequired = false; return _queryableSelectMethod; } if (mapperData.Context.IsPartOfQueryableMapping()) { - counterRequired = false; return EnumerableSelectWithoutIndexMethod; } - return counterRequired ? _projectWithIndexMethod : ProjectWithoutIndexMethod; + return ProjectWithoutIndexMethod; } public Expression GetCounterIncrement() => Expression.PreIncrementAssign(Counter); @@ -415,9 +406,9 @@ private Expression GetNullTargetConstruction() { var nullTargetVariableType = GetNullTargetVariableType(); - if (SourceTypeHelper.IsEnumerableInterface) + if (SourceTypeHelper.IsEnumerableOrQueryable) { - // Can't use a capacity constructor as unable to count source elements: + // Don't use a capacity constructor as source count not readily available: return Expression.New(nullTargetVariableType); } @@ -595,20 +586,37 @@ private static Expression GetValueCheckedElementMapping( if (mapping.NodeType != ExpressionType.Try) { - return Expression.Block( - new[] { valueVariable }, + return existingElementValueCheck.Update( + existingElementValueCheck.Variables, existingElementValueCheck.Expressions.Append(mapping)); } var mappingTryCatch = (TryExpression)mapping; + Expression mappingTryCatchBody; + + if (mappingTryCatch.Body.NodeType != ExpressionType.Block) + { + mappingTryCatchBody = Expression.Block( + new[] { valueVariable }, + existingElementValueCheck.Expressions.Append(mappingTryCatch.Body)); + } + else + { + var mappingTryCatchBodyBlock = (BlockExpression)mappingTryCatch.Body; + + mappingTryCatchBody = Expression.Block( + mappingTryCatchBodyBlock.Variables.Append(valueVariable), + existingElementValueCheck.Expressions.Append(mappingTryCatchBodyBlock.Expressions)); + } + mapping = mappingTryCatch.Update( - Expression.Block(existingElementValueCheck.Expressions.Append(mappingTryCatch.Body)), + mappingTryCatchBody, mappingTryCatch.Handlers, mappingTryCatch.Finally, mappingTryCatch.Fault); - return Expression.Block(new[] { valueVariable }, mapping); + return mapping; } public Expression GetSimpleElementConversion(Expression sourceElement) @@ -629,48 +637,14 @@ public Expression GetSourceItemsProjection( Expression sourceEnumerableValue, Func<Expression, Expression> projectionLambdaFactory) { - return CreateSourceItemsProjection( - sourceEnumerableValue, - (sourceParameter, counter) => projectionLambdaFactory.Invoke(sourceParameter), - counterRequired: false); - } - - public Expression GetSourceItemsProjection( - Expression sourceEnumerableValue, - Func<Expression, Expression, Expression> projectionLambdaFactory) - { - return CreateSourceItemsProjection(sourceEnumerableValue, projectionLambdaFactory, counterRequired: true); - } - - private Expression CreateSourceItemsProjection( - Expression sourceEnumerableValue, - Func<Expression, Expression, Expression> projectionLambdaFactory, - bool counterRequired) - { - var projectionMethod = GetProjectionMethodFor(MapperData, ref counterRequired); - - ParameterExpression[] projectionLambdaParameters; - Type[] funcTypes; - - if (counterRequired) - { - projectionLambdaParameters = new[] { _sourceElementParameter, Counter }; - funcTypes = new[] { Context.SourceElementType, Counter.Type, Context.TargetElementType }; - } - else - { - projectionLambdaParameters = new[] { _sourceElementParameter }; - funcTypes = new[] { Context.SourceElementType, Context.TargetElementType }; - } - - var projectionFuncType = Expression.GetFuncType(funcTypes); + var projectionFuncType = Expression.GetFuncType(Context.SourceElementType, Context.TargetElementType); - Expression projectionLambda = Expression.Lambda( + var projectionLambda = Expression.Lambda( projectionFuncType, - projectionLambdaFactory.Invoke(_sourceElementParameter, Counter), - projectionLambdaParameters); + projectionLambdaFactory.Invoke(_sourceElementParameter), + _sourceElementParameter); - var typedSelectMethod = projectionMethod.MakeGenericMethod(Context.ElementTypes); + var typedSelectMethod = GetProjectionMethod().MakeGenericMethod(Context.ElementTypes); var typedSelectCall = Expression.Call(typedSelectMethod, sourceEnumerableValue, projectionLambda); return typedSelectCall; @@ -814,8 +788,8 @@ public SourceItemsSelector SourceItemsProjectedToTargetType(IObjectMappingData m } _result = _builder.GetSourceItemsProjection( - sourceEnumerableValue, - (sourceElement, counter) => _builder.GetElementConversion(sourceElement, mappingData)); + sourceEnumerableValue, + sourceElement => _builder.GetElementConversion(sourceElement, mappingData)); return this; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationStrategy.cs new file mode 100644 index 000000000..bfc95600e --- /dev/null +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationStrategy.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +{ +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + + internal delegate Expression EnumerablePopulationStrategy( + EnumerablePopulationBuilder builder, + IObjectMappingData enumerableMappingData); +} diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index cf353efad..c5d5abc41 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -3,15 +3,17 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System; using System.Collections.Generic; using System.Collections.ObjectModel; - using Extensions.Internal; - using Members; - using NetStandardPolyfills; - using ReadableExpressions.Extensions; + using System.Linq; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; + using TypeConversion; internal class EnumerableTypeHelper { @@ -24,6 +26,7 @@ internal class EnumerableTypeHelper private Type _readOnlyCollectionType; private Type _collectionInterfaceType; private Type _enumerableInterfaceType; + private Type _queryableInterfaceType; #if FEATURE_ISET private Type _setInterfaceType; #endif @@ -49,7 +52,11 @@ public bool IsDictionary public bool IsReadOnlyCollection => EnumerableType == ReadOnlyCollectionType; - public bool IsEnumerableInterface => EnumerableType == EnumerableInterfaceType; + private bool IsEnumerableInterface => EnumerableType == EnumerableInterfaceType; + + public bool IsQueryableInterface => EnumerableType == QueryableInterfaceType; + + public bool IsEnumerableOrQueryable => IsEnumerableInterface || IsQueryableInterface; public bool HasCollectionInterface => EnumerableType.IsAssignableTo(CollectionInterfaceType); @@ -61,7 +68,7 @@ public bool IsDictionary public bool IsReadOnly => IsArray || IsReadOnlyCollection; public bool IsDeclaredReadOnly - => IsReadOnly || IsEnumerableInterface || IsReadOnlyCollectionInterface(); + => IsReadOnly || IsEnumerableOrQueryable || IsReadOnlyCollectionInterface(); public bool CouldBeReadOnly() { @@ -111,6 +118,8 @@ private bool IsReadOnlyCollectionInterface() public Type EnumerableInterfaceType => GetEnumerableType(ref _enumerableInterfaceType, typeof(IEnumerable<>)); + public Type QueryableInterfaceType => GetEnumerableType(ref _queryableInterfaceType, typeof(IQueryable<>)); + #if FEATURE_ISET private Type SetInterfaceType => GetEnumerableType(ref _setInterfaceType, typeof(ISet<>)); #endif @@ -190,6 +199,15 @@ private static bool ValueIsNotEnumerableInterface(Expression instance) public Expression GetCountFor(Expression instance, Type countType = null) => instance.GetCount(countType, exp => CollectionInterfaceType); + public Expression GetNonZeroCountCheck(Expression enumerableAccess) + { + var enumerableCount = GetCountFor(enumerableAccess); + var zero = ToNumericConverter<int>.Zero.GetConversionTo(enumerableCount.Type); + var countGreaterThanZero = Expression.GreaterThan(enumerableCount, zero); + + return countGreaterThanZero; + } + public Type GetEmptyInstanceCreationFallbackType() { if (IsArray) diff --git a/AgileMapper/ObjectPopulation/Enumerables/IEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/IEnumerablePopulationStrategy.cs deleted file mode 100644 index 18187c10c..000000000 --- a/AgileMapper/ObjectPopulation/Enumerables/IEnumerablePopulationStrategy.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables -{ -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif - - internal interface IEnumerablePopulationStrategy - { - Expression GetPopulation( - EnumerablePopulationBuilder builder, - IObjectMappingData enumerableMappingData); - } -} diff --git a/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs index fd4a22833..ed0482bed 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs @@ -6,9 +6,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Linq.Expressions; #endif - internal struct MergeEnumerablePopulationStrategy : IEnumerablePopulationStrategy + internal static class MergeEnumerablePopulationStrategy { - public Expression GetPopulation( + public static Expression Create( EnumerablePopulationBuilder builder, IObjectMappingData enumerableMappingData) { diff --git a/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs index 5f8c43a72..389ad4c39 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs @@ -6,9 +6,9 @@ using System.Linq.Expressions; #endif - internal struct OverwriteEnumerablePopulationStrategy : IEnumerablePopulationStrategy + internal static class OverwriteEnumerablePopulationStrategy { - public Expression GetPopulation( + public static Expression Create( EnumerablePopulationBuilder builder, IObjectMappingData enumerableMappingData) { diff --git a/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs b/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs index e479577a3..2f0f57af5 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/PopulationLoopDataExtensions.cs @@ -1,13 +1,15 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables { using System; - using Extensions.Internal; - using TypeConversion; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using DataSources; + using Extensions.Internal; + using TypeConversion; + using Members; internal static class PopulationLoopDataExtensions { @@ -48,6 +50,8 @@ private static BlockExpression GetLoopBody( var ifExitCheckBreakLoop = Expression.IfThen(loopData.LoopExitCheck, breakLoop); var counterIncrement = builder.GetCounterIncrement(); + elementPopulation = ApplySourceFilterIfAppropriate(elementPopulation, loopData, builder); + if (elementPopulation.NodeType != ExpressionType.Block) { return Expression.Block(ifExitCheckBreakLoop, elementPopulation, counterIncrement); @@ -65,5 +69,33 @@ private static BlockExpression GetLoopBody( ? Expression.Block(elementPopulationBlock.Variables, loopExpressions) : Expression.Block(loopExpressions); } + + private static Expression ApplySourceFilterIfAppropriate( + Expression elementPopulation, + IPopulationLoopData loopData, + EnumerablePopulationBuilder builder) + { + if (!builder.MapperData.MapperContext.UserConfigurations.HasSourceValueFilters) + { + return elementPopulation; + } + + var sourceElement = loopData.GetSourceElementValue(); + + var sourceValueFilters = builder.MapperData + .GetSourceValueFilters(sourceElement.Type); + + if (sourceValueFilters.None()) + { + return elementPopulation; + } + + var sourceFilterConditions = sourceValueFilters + .GetFilterConditionsOrNull(sourceElement, builder.MapperData); + + return (sourceFilterConditions != null) + ? Expression.IfThen(sourceFilterConditions, elementPopulation) + : elementPopulation; + } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs index 031aecd31..9a7fa2c0c 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs @@ -6,9 +6,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Linq.Expressions; #endif - internal struct ProjectSourceEnumerablePopulationStrategy : IEnumerablePopulationStrategy + internal static class ProjectSourceEnumerablePopulationStrategy { - public Expression GetPopulation( + public static Expression Create( EnumerablePopulationBuilder builder, IObjectMappingData enumerableMappingData) { diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs index 2a191d31a..4f55ccaae 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs @@ -24,6 +24,6 @@ protected SourceEnumerableAdapterBase(EnumerablePopulationBuilder builder) public virtual Expression GetSourceValues() => Builder.MapperData.SourceObject; public virtual bool UseReadOnlyTargetWrapper => - TargetTypeHelper.IsReadOnly && !SourceTypeHelper.IsEnumerableInterface; + TargetTypeHelper.IsReadOnly && !SourceTypeHelper.IsEnumerableOrQueryable; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MapperDataContext.cs b/AgileMapper/ObjectPopulation/MapperDataContext.cs index 61b52422f..945e48220 100644 --- a/AgileMapper/ObjectPopulation/MapperDataContext.cs +++ b/AgileMapper/ObjectPopulation/MapperDataContext.cs @@ -3,6 +3,8 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System; using Extensions.Internal; using Members; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; internal class MapperDataContext { @@ -41,7 +43,8 @@ private MapperDataContext( private static bool ShouldUseLocalVariable(IBasicMapperData mapperData) { - if (mapperData.TargetMember.IsSimple) + if (mapperData.TargetMember.IsSimple && + !mapperData.TargetType.GetNonNullableType().IsEnum()) { return false; } diff --git a/AgileMapper/ObjectPopulation/MapperKeys/DefaultRootMapperKeyFactory.cs b/AgileMapper/ObjectPopulation/MapperKeys/DefaultRootMapperKeyFactory.cs new file mode 100644 index 000000000..5b1377236 --- /dev/null +++ b/AgileMapper/ObjectPopulation/MapperKeys/DefaultRootMapperKeyFactory.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.MapperKeys +{ + internal static class DefaultRootMapperKeyFactory + { + public static ObjectMapperKeyBase Create(IObjectMappingData mappingData) + { + return new RootObjectMapperKey(mappingData.MappingTypes, mappingData.MappingContext) + { + MappingData = mappingData + }; + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs b/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs deleted file mode 100644 index 103174808..000000000 --- a/AgileMapper/ObjectPopulation/MapperKeys/IRootMapperKeyFactory.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation.MapperKeys -{ - internal interface IRootMapperKeyFactory - { - ObjectMapperKeyBase CreateRootKeyFor(IObjectMappingData mappingData); - } -} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs b/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs index 18e778d77..dcd7e9730 100644 --- a/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs +++ b/AgileMapper/ObjectPopulation/MapperKeys/RootMapperKeyFactory.cs @@ -1,13 +1,4 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.MapperKeys { - internal struct RootMapperKeyFactory : IRootMapperKeyFactory - { - public ObjectMapperKeyBase CreateRootKeyFor(IObjectMappingData mappingData) - { - return new RootObjectMapperKey(mappingData.MappingTypes, mappingData.MappingContext) - { - MappingData = mappingData - }; - } - } + internal delegate ObjectMapperKeyBase RootMapperKeyFactory(IObjectMappingData mappingData); } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs b/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs index 9449c0fd7..99fd063c1 100644 --- a/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingCallbackFactory.cs @@ -28,8 +28,7 @@ public MappingCallbackFactory( public virtual bool AppliesTo(CallbackPosition callbackPosition, IBasicMapperData mapperData) => (CallbackPosition == callbackPosition) && base.AppliesTo(mapperData); - protected override bool MemberPathMatches(IBasicMapperData mapperData) - => mapperData.HasCompatibleTypes(ConfigInfo); + protected override bool TypesMatch(IBasicMapperData mapperData) => TypesAreCompatible(mapperData); public Expression Create(IMemberMapperData mapperData) { diff --git a/AgileMapper/ObjectPopulation/MappingCreationContext.cs b/AgileMapper/ObjectPopulation/MappingCreationContext.cs index 43a388755..7221ab50e 100644 --- a/AgileMapper/ObjectPopulation/MappingCreationContext.cs +++ b/AgileMapper/ObjectPopulation/MappingCreationContext.cs @@ -17,26 +17,24 @@ namespace AgileObjects.AgileMapper.ObjectPopulation internal class MappingCreationContext { private bool _mapperDataHasRootEnumerableVariables; - private List<Expression> _memberMappingExpressions; + private IList<Expression> _memberMappingExpressions; public MappingCreationContext(IObjectMappingData mappingData) { - var mapperData = mappingData.MapperData; - MappingData = mappingData; - MapToNullCondition = GetMapToNullConditionOrNull(mapperData); + MapToNullCondition = GetMapToNullConditionOrNull(MapperData); InstantiateLocalVariable = true; MappingExpressions = new List<Expression>(); - if (mapperData.RuleSet.Settings.UseSingleRootMappingExpression) + if (RuleSet.Settings.UseSingleRootMappingExpression) { return; } - var basicMapperData = mapperData.WithNoTargetMember(); + var basicMapperData = MapperData.WithNoTargetMember(); - PreMappingCallback = basicMapperData.GetMappingCallbackOrNull(Before, mapperData); - PostMappingCallback = basicMapperData.GetMappingCallbackOrNull(After, mapperData); + PreMappingCallback = basicMapperData.GetMappingCallbackOrNull(Before, MapperData); + PostMappingCallback = basicMapperData.GetMappingCallbackOrNull(After, MapperData); } private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperData) @@ -62,11 +60,11 @@ private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperDa public bool InstantiateLocalVariable { get; set; } - public List<Expression> GetMemberMappingExpressions() + public IList<Expression> GetMemberMappingExpressions() { if (_memberMappingExpressions?.Count == MappingExpressions.Count) { - return _memberMappingExpressions ?? new List<Expression>(0); + return _memberMappingExpressions ?? Enumerable<Expression>.EmptyArray; } return _memberMappingExpressions = MappingExpressions.Filter(IsMemberMapping).ToList(); diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 65c24aa42..e3f721590 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -1,55 +1,41 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { - using System; using System.Collections.Generic; using System.Linq; #if NET35 using Microsoft.Scripting.Ast; + using static Microsoft.Scripting.Ast.ExpressionType; #else using System.Linq.Expressions; + using static System.Linq.Expressions.ExpressionType; #endif using DataSources; using Extensions; using Extensions.Internal; using Members; - using NetStandardPolyfills; + using ReadableExpressions; using ReadableExpressions.Extensions; -#if NET35 - using static Microsoft.Scripting.Ast.ExpressionType; -#else - using static System.Linq.Expressions.ExpressionType; -#endif internal abstract class MappingExpressionFactoryBase { - public abstract bool IsFor(IObjectMappingData mappingData); - public Expression Create(IObjectMappingData mappingData) { var mapperData = mappingData.MapperData; - if (TargetCannotBeMapped(mappingData, out var nullMappingBlock)) + if (TargetCannotBeMapped(mappingData, out var reason)) { - return nullMappingBlock; + return Expression.Block( + ReadableExpression.Comment(reason), + GetNullMappingFallbackValue(mapperData)); } - var returnNull = Expression.Return( - mapperData.ReturnLabelTarget, - mapperData.TargetType.ToDefaultExpression()); + var context = new MappingCreationContext(mappingData); - if (MappingAlwaysBranchesToDerivedType(mappingData, out var derivedTypeMappings)) + if (ShortCircuitMapping(context, out var mapping)) { - var shortCircuitReturns = GetShortCircuitReturns(returnNull, mappingData).ToArray(); - - return shortCircuitReturns.Any() - ? Expression.Block(shortCircuitReturns.Append(derivedTypeMappings)) - : derivedTypeMappings; + return mapping; } - var context = new MappingCreationContext(mappingData); - - context.MappingExpressions.AddUnlessNullOrEmpty(derivedTypeMappings); - AddPopulationsAndCallbacks(context); if (NothingIsBeingMapped(context)) @@ -57,54 +43,44 @@ public Expression Create(IObjectMappingData mappingData) return mapperData.IsEntryPoint ? mapperData.TargetObject : Constants.EmptyExpression; } - context.MappingExpressions.InsertRange(0, GetShortCircuitReturns(returnNull, mappingData)); + context.MappingExpressions.InsertRange(0, GetShortCircuitReturns(mappingData)); var mappingBlock = GetMappingBlock(context); if (mapperData.Context.UseMappingTryCatch) { - mappingBlock = WrapInTryCatch(mappingBlock, mapperData); + mappingBlock = mappingBlock.WrapInTryCatch(mapperData); } return mappingBlock; } - protected virtual bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) + protected virtual bool TargetCannotBeMapped(IObjectMappingData mappingData, out string reason) { - nullMappingBlock = null; + reason = null; return false; } - private bool MappingAlwaysBranchesToDerivedType(IObjectMappingData mappingData, out Expression derivedTypeMappings) - { - derivedTypeMappings = GetDerivedTypeMappings(mappingData); + protected virtual Expression GetNullMappingFallbackValue(IMemberMapperData mapperData) + => mapperData.TargetType.ToDefaultExpression(); - if (derivedTypeMappings.NodeType != Goto) - { - return false; - } - - var returnExpression = (GotoExpression)derivedTypeMappings; - derivedTypeMappings = returnExpression.Value; - return true; + protected virtual bool ShortCircuitMapping(MappingCreationContext context, out Expression mapping) + { + mapping = null; + return false; } - protected virtual Expression GetDerivedTypeMappings(IObjectMappingData mappingData) => Constants.EmptyExpression; - - protected virtual IEnumerable<Expression> GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData) + protected virtual IEnumerable<Expression> GetShortCircuitReturns(IObjectMappingData mappingData) => Enumerable<Expression>.Empty; private void AddPopulationsAndCallbacks(MappingCreationContext context) { context.MappingExpressions.AddUnlessNullOrEmpty(context.PreMappingCallback); - context.MappingExpressions.AddRange(GetNonNullObjectPopulation(context)); + context.MappingExpressions.AddRange(GetObjectPopulation(context)); context.MappingExpressions.AddRange(GetConfiguredToTargetDataSourceMappings(context)); context.MappingExpressions.AddUnlessNullOrEmpty(context.PostMappingCallback); } - private IEnumerable<Expression> GetNonNullObjectPopulation(MappingCreationContext context) - => GetObjectPopulation(context).WhereNotNull(); - protected abstract IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context); private IEnumerable<Expression> GetConfiguredToTargetDataSourceMappings(MappingCreationContext context) @@ -225,25 +201,17 @@ private Expression GetMappingBlock(MappingCreationContext context) AdjustForSingleExpressionBlockIfApplicable(context); - if (context.MapperData.UseSingleMappingExpression()) - { - return mappingExpressions.First(); - } + var firstExpression = mappingExpressions.First(); - if (mappingExpressions.HasOne() && (mappingExpressions[0].NodeType == Constant)) + if (context.MapperData.UseSingleMappingExpression()) { - goto CreateFullMappingBlock; + return firstExpression; } Expression returnExpression; - if (mappingExpressions[0].NodeType != Block) + if (firstExpression.NodeType != Block) { - if (mappingExpressions[0].NodeType == MemberAccess) - { - return GetReturnExpression(mappingExpressions[0], context); - } - if (TryAdjustForUnusedLocalVariableIfApplicable(context, out returnExpression)) { return returnExpression; @@ -254,8 +222,6 @@ private Expression GetMappingBlock(MappingCreationContext context) return returnExpression; } - CreateFullMappingBlock: - returnExpression = GetReturnExpression(GetReturnValue(context.MapperData), context); mappingExpressions.Add(context.MapperData.GetReturnLabel(returnExpression)); @@ -397,65 +363,8 @@ private static Expression GetMapToNullConditionOrNull(MappingCreationContext con return Expression.Equal( assignedMember.GetValueOrDefaultCall(), 0.ToConstantExpression(nonNullableIdType)); - } protected virtual Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.TargetInstance; - - private static Expression WrapInTryCatch(Expression mappingBlock, IMemberMapperData mapperData) - { - var configuredCallback = mapperData.MapperContext.UserConfigurations.GetExceptionCallbackOrNull(mapperData); - var exceptionVariable = Parameters.Create<Exception>("ex"); - - if (configuredCallback == null) - { - var catchBody = Expression.Throw( - MappingException.GetFactoryMethodCall(mapperData, exceptionVariable), - mappingBlock.Type); - - return CreateTryCatch(mappingBlock, exceptionVariable, catchBody); - } - - var callbackActionType = configuredCallback.Type.GetGenericTypeArguments()[0]; - - Type[] contextTypes; - Expression contextAccess; - - if (callbackActionType.IsGenericType()) - { - contextTypes = callbackActionType.GetGenericTypeArguments(); - contextAccess = mapperData.GetAppropriateTypedMappingContextAccess(contextTypes); - } - else - { - contextTypes = new[] { mapperData.SourceType, mapperData.TargetType }; - contextAccess = mapperData.MappingDataObject; - } - - var exceptionContextCreateMethod = ObjectMappingExceptionData - .CreateMethod - .MakeGenericMethod(contextTypes); - - var createExceptionContextCall = Expression.Call( - exceptionContextCreateMethod, - contextAccess, - exceptionVariable); - - var callbackInvocation = Expression.Invoke(configuredCallback, createExceptionContextCall); - var returnDefault = mappingBlock.Type.ToDefaultExpression(); - var configuredCatchBody = Expression.Block(callbackInvocation, returnDefault); - - return CreateTryCatch(mappingBlock, exceptionVariable, configuredCatchBody); - } - - private static Expression CreateTryCatch( - Expression mappingBlock, - ParameterExpression exceptionVariable, - Expression catchBody) - { - var catchBlock = Expression.Catch(exceptionVariable, catchBody); - - return Expression.TryCatch(mappingBlock, catchBlock); - } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index 8d34258f7..202a35fa5 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -1,14 +1,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { - using System; - using Extensions; - using Extensions.Internal; - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions; + using Extensions.Internal; + using Members; internal static class MappingFactory { @@ -19,6 +18,13 @@ public static Expression GetChildMapping( IChildMemberMappingData childMappingData) { var childMapperData = childMappingData.MapperData; + + var childObjectMappingData = ObjectMappingDataFactory.ForChild( + sourceMember, + childMapperData.TargetMember, + dataSourceIndex, + childMappingData.Parent); + var targetMemberAccess = childMapperData.GetTargetMemberAccess(); childMapperData.TargetMember.MapCreating(sourceMember.Type); @@ -28,12 +34,6 @@ public static Expression GetChildMapping( targetMemberAccess, childMapperData.EnumerableIndex); - var childObjectMappingData = ObjectMappingDataFactory.ForChild( - sourceMember, - childMapperData.TargetMember, - dataSourceIndex, - childMappingData.Parent); - if (childObjectMappingData.MappingTypes.RuntimeTypesNeeded) { return childMapperData.Parent.GetRuntimeTypedMapping( @@ -129,7 +129,7 @@ public static Expression GetElementMapping( if (elementMapperData.Context.IsStandalone) { - enumerableIndex = Expression.Property(elementMapperData.EnumerableIndex, "Value"); + enumerableIndex = elementMapperData.EnumerableIndex.GetNullableValueAccess(); parentMappingDataObject = typeof(IObjectMappingData).ToDefaultExpression(); } else @@ -143,7 +143,9 @@ public static Expression GetElementMapping( targetElementValue, enumerableIndex); - elementMapperData.Context.IsForNewElement = targetElementValue.NodeType == ExpressionType.Default; + elementMapperData.Context.IsForNewElement = + (targetElementValue.NodeType == ExpressionType.Default) || + (elementMapperData.DeclaredTypeMapperData?.Context.IsForNewElement == true); if (elementMapperData.IsRepeatMapping && elementMapperData.RuleSet.RepeatMappingStrategy.AppliesTo(elementMapperData)) @@ -186,17 +188,20 @@ public static Expression GetInlineMappingBlock( return Constants.EmptyExpression; } - if (mapper.MapperData.Context.UsesMappingDataObject) + var mapperData = mapper.MapperData; + + if (mapperData.Context.UsesMappingDataObject) { return UseLocalValueVariable( - mapper.MapperData.MappingDataObject, + mapperData.MappingDataObject, createMappingDataCall, - mapper.MappingExpression); + mapper.MappingExpression, + mapperData); } return GetDirectAccessMapping( mapper.MappingLambda.Body, - mapper.MapperData, + mapperData, mappingValues, createMappingDataCall); } @@ -214,7 +219,7 @@ private static Expression GetDirectAccessMapping( if (useLocalSourceValueVariable) { - var sourceValueVariableName = GetSourceValueVariableName(mapperData, mappingValues.SourceValue.Type); + var sourceValueVariableName = mappingValues.SourceValue.Type.GetSourceValueVariableName(); sourceValue = Expression.Variable(mappingValues.SourceValue.Type, sourceValueVariableName); sourceValueVariableValue = mappingValues.SourceValue; } @@ -236,7 +241,11 @@ private static Expression GetDirectAccessMapping( .Replace(mapperData.MappingDataObject, createMappingDataCall); return useLocalSourceValueVariable - ? UseLocalValueVariable((ParameterExpression)sourceValue, sourceValueVariableValue, mapping) + ? UseLocalValueVariable( + (ParameterExpression)sourceValue, + sourceValueVariableValue, + mapping, + mapperData) : mapping; } @@ -250,25 +259,6 @@ private static bool ShouldUseLocalSourceValueVariable( SourceAccessCounter.MultipleAccessesExist(sourceValue, mapping); } - private static string GetSourceValueVariableName(IMemberMapperData mapperData, Type sourceType = null) - { - var sourceValueVariableName = "source" + (sourceType ?? mapperData.SourceType).GetVariableNameInPascalCase(); - - var numericSuffix = default(string); - - for (var i = mapperData.MappingDataObject.Name.Length - 1; i > 0; --i) - { - if (!char.IsDigit(mapperData.MappingDataObject.Name[i])) - { - break; - } - - numericSuffix = mapperData.MappingDataObject.Name[i] + numericSuffix; - } - - return sourceValueVariableName + numericSuffix; - } - public static Expression UseLocalSourceValueVariableIfAppropriate( Expression mappingExpression, ObjectMapperData mapperData) @@ -285,13 +275,14 @@ public static Expression UseLocalSourceValueVariableIfAppropriate( return mappingExpression; } - var sourceValueVariableName = GetSourceValueVariableName(mapperData); + var sourceValueVariableName = mapperData.SourceType.GetSourceValueVariableName(); var sourceValueVariable = Expression.Variable(mapperData.SourceType, sourceValueVariableName); return UseLocalValueVariable( sourceValueVariable, mapperData.SourceObject, mappingExpression, + mapperData, performValueReplacement: true); } @@ -309,28 +300,36 @@ public static Expression UseLocalToTargetDataSourceVariableIfAppropriate( return UseLocalValueVariable( toTargetMapperData.MappingDataObject, MappingDataCreationFactory.ForToTarget(mapperData, toTargetDataSourceValue), - mappingExpression); + mappingExpression, + toTargetMapperData); } private static Expression UseLocalValueVariable( ParameterExpression variable, Expression variableValue, Expression body, + IMemberMapperData mapperData, bool performValueReplacement = false) { var variableAssignment = variable.AssignTo(variableValue); - if (body.NodeType != ExpressionType.Try) + if (body.NodeType == ExpressionType.Block) { if (performValueReplacement) { body = body.Replace(variableValue, variable); } - return Expression.Block(new[] { variable }, variableAssignment, body); + var block = (BlockExpression)body; + + return Expression.Block( + block.Variables.Append(variable), + block.Expressions.Prepend(variableAssignment)); } - var tryCatch = (TryExpression)body; + var tryCatch = (body.NodeType != ExpressionType.Try) + ? body.WrapInTryCatch(mapperData) + : (TryExpression)body; body = tryCatch.Update( Expression.Block(variableAssignment, tryCatch.Body.Replace(variableValue, variable)), diff --git a/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs b/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs index 4c24fd16d..2cce9ea83 100644 --- a/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectCreationCallbackFactory.cs @@ -28,8 +28,8 @@ public ObjectCreationCallbackFactory( public override bool AppliesTo(CallbackPosition callbackPosition, IBasicMapperData mapperData) => mapperData.TargetMember.Type.IsAssignableTo(_creationTargetType) && base.AppliesTo(callbackPosition, mapperData); - protected override bool MemberPathMatches(IBasicMapperData mapperData) - => MemberPathHasMatchingSourceAndTargetTypes(mapperData); + protected override bool TypesMatch(IBasicMapperData mapperData) + => SourceAndTargetTypesMatch(mapperData); protected override Expression GetConditionOrNull(IMemberMapperData mapperData, CallbackPosition position) { diff --git a/AgileMapper/ObjectPopulation/ObjectMapper.cs b/AgileMapper/ObjectPopulation/ObjectMapper.cs index 2984e72f1..dcbff9ad3 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapper.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapper.cs @@ -3,15 +3,15 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System; using System.Collections.Generic; using System.Linq; - using Caching; - using MapperKeys; - using NetStandardPolyfills; - using RepeatedMappings; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Caching; + using MapperKeys; + using NetStandardPolyfills; + using RepeatedMappings; internal class ObjectMapper<TSource, TTarget> : IObjectMapper { diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index ead7fd1e6..3738ff6a6 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -5,6 +5,11 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Globalization; using System.Linq; using System.Reflection; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using DataSources; using Enumerables; using Extensions; @@ -14,11 +19,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Members.Sources; using NetStandardPolyfills; using ReadableExpressions.Extensions; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif using static Members.Member; internal class ObjectMapperData : BasicMapperData, IMemberMapperData @@ -29,7 +29,7 @@ internal class ObjectMapperData : BasicMapperData, IMemberMapperData private static readonly MethodInfo _mapRepeatedElementMethod = typeof(IObjectMappingDataUntyped).GetPublicInstanceMethod("MapRepeated", parameterCount: 3); - private readonly List<ObjectMapperData> _childMapperDatas; + private ExpressionInfoFinder _expressionInfoFinder; private ObjectMapperData _entryPointMapperData; private Expression _targetInstance; private ParameterExpression _instanceVariable; @@ -55,11 +55,10 @@ private ObjectMapperData( { MapperContext = mappingData.MappingContext.MapperContext; DeclaredTypeMapperData = OriginalMapperData = declaredTypeMapperData; - _childMapperDatas = new List<ObjectMapperData>(); + ChildMapperDatas = new List<ObjectMapperData>(); DataSourceIndex = dataSourceIndex.GetValueOrDefault(); MappingDataObject = GetMappingDataObject(parent); - SourceMember = sourceMember; var mappingDataType = typeof(IMappingData<,>).MakeGenericType(SourceType, TargetType); SourceObject = GetMappingDataProperty(mappingDataType, RootSourceMemberName); @@ -79,9 +78,7 @@ private ObjectMapperData( ParentObject = GetMappingDataProperty(nameof(Parent)); } - ExpressionInfoFinder = new ExpressionInfoFinder(MappingDataObject); - - DataSourcesByTargetMember = new Dictionary<QualifiedMember, DataSourceSet>(); + DataSourcesByTargetMember = new Dictionary<QualifiedMember, IDataSourceSet>(); ReturnLabelTarget = Expression.Label(TargetType, "Return"); _mappedObjectCachingMode = MapperContext.UserConfigurations.CacheMappedObjects(this); @@ -99,7 +96,7 @@ private ObjectMapperData( return; } - parent._childMapperDatas.Add(this); + parent.ChildMapperDatas.Add(this); Parent = parent; if (!this.TargetMemberIsEnumerableElement()) @@ -341,13 +338,6 @@ private static bool UseExistingMapperData<TSource, TTarget>( // do a runtime-typed child mapping, we're able to reuse the parent MapperData // by finding it from the entry point: var parentMapperData = mappingData.Parent.MapperData; - - if (parentMapperData.ChildMapperDatas.None()) - { - existingMapperData = null; - return false; - } - var membersSource = mappingData.MapperKey.GetMembersSource(parentMapperData); if (!(membersSource is IChildMembersSource childMembersSource)) @@ -403,7 +393,7 @@ private static ObjectMapperData GetMapperDataOrNull( public ObjectMapperData OriginalMapperData { get; set; } - public IList<ObjectMapperData> ChildMapperDatas => _childMapperDatas; + public IList<ObjectMapperData> ChildMapperDatas { get; } public int DataSourceIndex { get; set; } @@ -438,8 +428,6 @@ public QualifiedMember GetTargetMemberFor(string targetMemberRegistrationName) public Expression ParentObject { get; } - public IQualifiedMember SourceMember { get; } - public bool CacheMappedObjects { get => _mappedObjectCachingMode == MappedObjectCachingMode.Cache; @@ -500,7 +488,15 @@ private ParameterExpression CreateInstanceVariable() ?? Expression.Variable(TargetType, TargetType.GetVariableNameInCamelCase()); } - public ExpressionInfoFinder ExpressionInfoFinder { get; } + public ExpressionInfoFinder ExpressionInfoFinder + => _expressionInfoFinder ?? (_expressionInfoFinder = GetExpressionInfoFinder()); + + private ExpressionInfoFinder GetExpressionInfoFinder() + { + return new ExpressionInfoFinder(Context.IsForToTargetMapping + ? OriginalMapperData.MappingDataObject + : MappingDataObject); + } public EnumerablePopulationBuilder EnumerablePopulationBuilder { get; } @@ -580,7 +576,7 @@ public ObjectMapperData GetNearestStandaloneMapperData() public IList<ObjectMapperKeyBase> RepeatedMapperFuncKeys { get; private set; } - public Dictionary<QualifiedMember, DataSourceSet> DataSourcesByTargetMember { get; } + public Dictionary<QualifiedMember, IDataSourceSet> DataSourcesByTargetMember { get; } public Expression GetRuntimeTypedMapping( Expression sourceObject, @@ -668,7 +664,7 @@ public MethodCallExpression GetMapRepeatedCall( MethodInfo mapRepeatedMethod; Expression[] arguments; - if (targetMember.LeafMember.IsEnumerableElement()) + if (targetMember.IsEnumerableElement()) { mapRepeatedMethod = _mapRepeatedElementMethod; diff --git a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs index 7eebbb5c9..c949adde4 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs @@ -2,34 +2,22 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System; using System.Collections.Generic; - using Caching; - using ComplexTypes; - using Enumerables; - using Extensions.Internal; - using MapperKeys; - using Queryables; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Caching; + using DataSources.Factories; + using MapperKeys; internal class ObjectMapperFactory { - private readonly IList<MappingExpressionFactoryBase> _mappingExpressionFactories; private readonly ICache<IRootMapperKey, IObjectMapper> _rootMappersCache; private Dictionary<MapperCreationCallbackKey, Action<IObjectMapper>> _creationCallbacksByKey; public ObjectMapperFactory(CacheSet mapperScopedCacheSet) { - _mappingExpressionFactories = new[] - { - QueryProjectionExpressionFactory.Instance, - DictionaryMappingExpressionFactory.Instance, - EnumerableMappingExpressionFactory.Instance, - ComplexTypeMappingExpressionFactory.Instance - }; - _rootMappersCache = mapperScopedCacheSet.CreateScoped<IRootMapperKey, IObjectMapper>(default(RootMapperKeyComparer)); } @@ -64,10 +52,9 @@ public ObjectMapper<TSource, TTarget> GetOrCreateRoot<TSource, TTarget>(ObjectMa public ObjectMapper<TSource, TTarget> Create<TSource, TTarget>(ObjectMappingData<TSource, TTarget> mappingData) { - var mappingExpressionFactory = _mappingExpressionFactories.First(mef => mef.IsFor(mappingData)); - var mappingExpression = mappingExpressionFactory.Create(mappingData); + var mappingExpression = DataSourceSetFactory.CreateFor(mappingData).BuildValue(); - if (mappingExpression.NodeType == ExpressionType.Default) + if (mappingExpression == Constants.EmptyExpression) { return null; } diff --git a/AgileMapper/ObjectPopulation/ObjectMappingData.cs b/AgileMapper/ObjectPopulation/ObjectMappingData.cs index f21e16dfd..c901ee6ff 100644 --- a/AgileMapper/ObjectPopulation/ObjectMappingData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMappingData.cs @@ -2,17 +2,17 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System; using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using Caching; using Extensions.Internal; using MapperKeys; using Members; using NetStandardPolyfills; using Validation; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif internal class ObjectMappingData<TSource, TTarget> : MappingInstanceData<TSource, TTarget>, @@ -83,7 +83,7 @@ private ObjectMappingData( public IRootMapperKey EnsureRootMapperKey() { - MapperKey = MappingContext.RuleSet.RootMapperKeyFactory.CreateRootKeyFor(this); + MapperKey = MappingContext.RuleSet.RootMapperKeyFactory.Invoke(this); return (IRootMapperKey)MapperKey; } @@ -373,7 +373,7 @@ public IObjectMappingData WithSource(IQualifiedMember newSourceMember) newSourceMappingData.MapperKey = MappingContext .RuleSet .RootMapperKeyFactory - .CreateRootKeyFor(newSourceMappingData); + .Invoke(newSourceMappingData); newSourceMappingData.MapperData.OriginalMapperData = MapperData; newSourceMappingData.MapperData.Context.IsForToTargetMapping = true; diff --git a/AgileMapper/ObjectPopulation/RepeatedMappings/IRepeatMappingStrategy.cs b/AgileMapper/ObjectPopulation/RepeatedMappings/IRepeatMappingStrategy.cs index d4fd514a3..f1ff14372 100644 --- a/AgileMapper/ObjectPopulation/RepeatedMappings/IRepeatMappingStrategy.cs +++ b/AgileMapper/ObjectPopulation/RepeatedMappings/IRepeatMappingStrategy.cs @@ -1,11 +1,11 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.RepeatedMappings { - using Members; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Members; internal interface IRepeatMappingStrategy { diff --git a/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs b/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs index 4c8ce4ba6..4635af4e3 100644 --- a/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs +++ b/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs @@ -2,15 +2,15 @@ { using System.Collections.Generic; using System.Linq; - using Extensions; - using Extensions.Internal; - using Members; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions; + using Extensions.Internal; + using Members; + using ReadableExpressions.Extensions; internal static class ComplexTypeToNullComparisonConverter { @@ -118,7 +118,8 @@ private static bool TryGetEntityMemberIdMember( { idMemberName = entityMemberName + idMemberName; - entityMemberIdMember = sourceMembers.FirstOrDefault(m => m.Name.EqualsIgnoreCase(idMemberName)); + entityMemberIdMember = sourceMembers + .FirstOrDefault(idMemberName, (idmn, m) => m.Name.EqualsIgnoreCase(idmn)); return entityMemberIdMember != null; } diff --git a/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs b/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs index 968e208cd..168d6213e 100644 --- a/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs +++ b/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs @@ -1,27 +1,16 @@ namespace AgileObjects.AgileMapper.Queryables { using System.Collections.Generic; - using Extensions.Internal; - using ObjectPopulation; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using ObjectPopulation; internal class QueryProjectionExpressionFactory : MappingExpressionFactoryBase { - public static readonly MappingExpressionFactoryBase Instance = new QueryProjectionExpressionFactory(); - - public override bool IsFor(IObjectMappingData mappingData) - { - var mapperData = mappingData.MapperData; - - return mapperData.IsRoot && - mapperData.TargetMember.IsEnumerable && - mapperData.SourceType.IsQueryable(); - } - protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context) { var mapperData = context.MapperData; diff --git a/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs b/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs index 379d7f444..21f2643e7 100644 --- a/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs +++ b/AgileMapper/Queryables/QueryProjectorMapperKeyFactory.cs @@ -4,9 +4,9 @@ using ObjectPopulation; using ObjectPopulation.MapperKeys; - internal struct QueryProjectorMapperKeyFactory : IRootMapperKeyFactory + internal static class QueryProjectorMapperKeyFactory { - public ObjectMapperKeyBase CreateRootKeyFor(IObjectMappingData mappingData) + public static ObjectMapperKeyBase Create(IObjectMappingData mappingData) { var providerType = mappingData.GetSource<IQueryable>().Provider.GetType(); diff --git a/AgileMapper/Queryables/Recursion/MapToDepthRepeatMappingStrategy.cs b/AgileMapper/Queryables/Recursion/MapToDepthRepeatMappingStrategy.cs index e76f3d6ef..73d60edac 100644 --- a/AgileMapper/Queryables/Recursion/MapToDepthRepeatMappingStrategy.cs +++ b/AgileMapper/Queryables/Recursion/MapToDepthRepeatMappingStrategy.cs @@ -1,13 +1,13 @@ namespace AgileObjects.AgileMapper.Queryables.Recursion { - using Members; - using ObjectPopulation; - using ObjectPopulation.RepeatedMappings; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Members; + using ObjectPopulation; + using ObjectPopulation.RepeatedMappings; internal struct MapToDepthRepeatMappingStrategy : IRepeatMappingStrategy { diff --git a/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs b/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs index 22fe0d7ab..be3ed0657 100644 --- a/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs +++ b/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs @@ -130,7 +130,7 @@ private static Assembly GetAssemblyOrNull(string loadedAssemblyName) return AppDomain.CurrentDomain .GetAssemblies() - .FirstOrDefault(assembly => assembly.GetName().Name == assemblyName); + .FirstOrDefault(assemblyName, (an, assembly) => assembly.GetName().Name == an); #endif } diff --git a/AgileMapper/TypeConversion/ToBoolConverter.cs b/AgileMapper/TypeConversion/ToBoolConverter.cs index 406fa4a83..9e8135d9e 100644 --- a/AgileMapper/TypeConversion/ToBoolConverter.cs +++ b/AgileMapper/TypeConversion/ToBoolConverter.cs @@ -4,13 +4,13 @@ namespace AgileObjects.AgileMapper.TypeConversion using System.Collections.Generic; using System.Globalization; using System.Linq; - using Extensions.Internal; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using ReadableExpressions.Extensions; internal struct ToBoolConverter : IValueConverter { @@ -45,10 +45,7 @@ public Expression GetConversion(Expression sourceValue, Type targetType) var sourceValueConversion = Expression.Condition( sourceEqualsTrueTests, true.ToConstantExpression(typeof(bool?)), - Expression.Condition( - sourceEqualsFalseTests, - false.ToConstantExpression(typeof(bool?)), - typeof(bool?).ToDefaultExpression())); + false.ToConstantExpression(typeof(bool?)).ToIfFalseDefaultCondition(sourceEqualsFalseTests)); return sourceValueConversion; } diff --git a/AgileMapper/TypeConversion/ToCharacterConverter.cs b/AgileMapper/TypeConversion/ToCharacterConverter.cs index 25a56f2ae..d147ac657 100644 --- a/AgileMapper/TypeConversion/ToCharacterConverter.cs +++ b/AgileMapper/TypeConversion/ToCharacterConverter.cs @@ -1,15 +1,14 @@ namespace AgileObjects.AgileMapper.TypeConversion { using System; - using System.Linq; - using Extensions.Internal; - using NetStandardPolyfills; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; internal struct ToCharacterConverter : IValueConverter { diff --git a/AgileMapper/TypeConversion/ToEnumConverter.cs b/AgileMapper/TypeConversion/ToEnumConverter.cs index 4ae1e743f..708d25302 100644 --- a/AgileMapper/TypeConversion/ToEnumConverter.cs +++ b/AgileMapper/TypeConversion/ToEnumConverter.cs @@ -4,16 +4,16 @@ using System.Collections; using System.Collections.Generic; using System.Linq; - using Configuration; - using Extensions; - using Extensions.Internal; - using NetStandardPolyfills; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Configuration; + using Extensions; + using Extensions.Internal; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; internal class ToEnumConverter : IValueConverter { @@ -379,7 +379,7 @@ private Expression GetEnumToEnumConversion( { return new { - Value = (Expression)targetEnumValues.First(tv => tv.Member.Name == pairedMemberName), + Value = (Expression)targetEnumValues.First(pairedMemberName, (pmn, tv) => tv.Member.Name == pmn), IsCustom = true }; } @@ -387,17 +387,17 @@ private Expression GetEnumToEnumConversion( return new { Value = targetEnumValues - .FirstOrDefault(tv => tv.Member.Name.EqualsIgnoreCase(sv.Member.Name)) ?? + .FirstOrDefault(sv.Member.Name, (name, tv) => tv.Member.Name.EqualsIgnoreCase(name)) ?? fallbackValue, IsCustom = false }; }); var enumPairsConversion = sourceEnumValues - .Project(sv => new + .Project(enumPairs, (eps, sv) => new { SourceValue = sv, - PairedValue = enumPairs[sv] + PairedValue = eps[sv] }) .OrderByDescending(d => d.PairedValue.IsCustom) .Reverse() @@ -495,17 +495,8 @@ private static Expression GetStringValueConversion( numericConversion, nameMatchingConversion); - var valueIsNullOrEmpty = Expression.Call( -#if NET35 - typeof(StringExtensions) -#else - typeof(string) -#endif - .GetPublicStaticMethod("IsNullOrWhiteSpace"), - sourceValue); - var convertedValueOrDefault = Expression.Condition( - valueIsNullOrEmpty, + StringExpressionExtensions.GetIsNullOrWhiteSpaceCall(sourceValue), fallbackValue, numericOrNameConversion); diff --git a/AgileMapper/TypeConversion/ToFormattedStringConverter.cs b/AgileMapper/TypeConversion/ToFormattedStringConverter.cs index ca817cbf6..58ec61490 100644 --- a/AgileMapper/TypeConversion/ToFormattedStringConverter.cs +++ b/AgileMapper/TypeConversion/ToFormattedStringConverter.cs @@ -2,14 +2,14 @@ { using System; using System.Reflection; - using Configuration; - using Extensions.Internal; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Configuration; + using Extensions.Internal; + using ReadableExpressions.Extensions; internal class ToFormattedStringConverter : IValueConverter { @@ -28,7 +28,7 @@ public ToFormattedStringConverter(Type sourceValueType, string formattingString) } _sourceValueType = sourceValueType; - _formattingString = formattingString.ToConstantExpression(typeof(string)); + _formattingString = formattingString.ToConstantExpression(); } public bool CanConvert(Type nonNullableSourceType, Type nonNullableTargetType) @@ -36,6 +36,11 @@ public bool CanConvert(Type nonNullableSourceType, Type nonNullableTargetType) public Expression GetConversion(Expression sourceValue, Type targetType) { + if (sourceValue.Type.IsNullableType()) + { + sourceValue = sourceValue.GetNullableValueAccess(); + } + var toStringCall = Expression.Call(sourceValue, _toStringMethod, _formattingString); return toStringCall; diff --git a/AgileMapper/TypeConversion/ToNumericConverter.cs b/AgileMapper/TypeConversion/ToNumericConverter.cs index 785a27b80..9915ef9a9 100644 --- a/AgileMapper/TypeConversion/ToNumericConverter.cs +++ b/AgileMapper/TypeConversion/ToNumericConverter.cs @@ -2,14 +2,14 @@ { using System; using System.Linq; - using Extensions.Internal; - using NetStandardPolyfills; - using ReadableExpressions.Extensions; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; internal class ToNumericConverter<TNumeric> : TryParseConverter<TNumeric> { @@ -75,26 +75,19 @@ private static Type GetNonEnumSourceType(Expression sourceValue) private static Expression GetBoolToNumericConversion(Expression sourceValue, Type targetType) { - var sourceIsNotNullable = sourceValue.Type == typeof(bool); - - var testValue = sourceIsNotNullable - ? sourceValue - : sourceValue.GetConversionTo<bool>(); + var sourceIsNullable = sourceValue.Type != typeof(bool); var boolConversion = Expression.Condition( - testValue, + sourceIsNullable ? sourceValue.GetConversionTo<bool>() : sourceValue, One.GetConversionTo(targetType), Zero.GetConversionTo(targetType)); - if (sourceIsNotNullable) + if (sourceIsNullable) { - return boolConversion; + boolConversion = boolConversion.ToIfFalseDefaultCondition(sourceValue.GetIsNotDefaultComparison()); } - return Expression.Condition( - sourceValue.GetIsNotDefaultComparison(), - boolConversion, - boolConversion.Type.ToDefaultExpression()); + return boolConversion; } private static bool IsCoercible(Type sourceType) => _coercibleNumericTypes.Contains(sourceType); diff --git a/AgileMapper/TypeConversion/ToStringConverter.cs b/AgileMapper/TypeConversion/ToStringConverter.cs index c5da2a12f..2dbe76ac9 100644 --- a/AgileMapper/TypeConversion/ToStringConverter.cs +++ b/AgileMapper/TypeConversion/ToStringConverter.cs @@ -88,7 +88,7 @@ private static Expression GetDateTimeToStringConversion(Expression sourceValue, if (sourceValue.Type != nonNullableSourceType) { - sourceValue = Expression.Property(sourceValue, "Value"); + sourceValue = sourceValue.GetNullableValueAccess(); } var toStringCall = Expression.Call(sourceValue, toStringMethod, dateTimeFormat); @@ -99,7 +99,7 @@ private static Expression GetDateTimeToStringConversion(Expression sourceValue, public static MethodInfo GetToStringMethodOrNull(Type sourceType, Type argumentType) { var toStringMethod = sourceType - .GetPublicInstanceMethods("ToString") + .GetPublicInstanceMethods(nameof(ToString)) .Project(m => new { Method = m, @@ -118,10 +118,8 @@ private static Expression GetBoolToStringConversion(Expression sourceValue, Type return GetTrueOrFalseTernary(sourceValue); } - var nullTrueOrFalse = Expression.Condition( - Expression.Property(sourceValue, "HasValue"), - GetTrueOrFalseTernary(Expression.Property(sourceValue, "Value")), - typeof(string).ToDefaultExpression()); + var nullTrueOrFalse = GetTrueOrFalseTernary(sourceValue.GetNullableValueAccess()) + .ToIfFalseDefaultCondition(sourceValue.GetNullableHasValueAccess()); return nullTrueOrFalse; } diff --git a/AgileMapper/Validation/EnumMappingMismatchFinder.cs b/AgileMapper/Validation/EnumMappingMismatchFinder.cs index 27d3e1839..d6f319820 100644 --- a/AgileMapper/Validation/EnumMappingMismatchFinder.cs +++ b/AgileMapper/Validation/EnumMappingMismatchFinder.cs @@ -43,7 +43,7 @@ public static ICollection<EnumMappingMismatchSet> FindMismatches(ObjectMapperDat } var mismatchSets = targetMemberDatas - .Project(d => EnumMappingMismatchSet.For(d.TargetMember, d.DataSources, mapperData)) + .Project(mapperData, (md, d) => EnumMappingMismatchSet.For(d.TargetMember, d.DataSources, md)) .Filter(m => m.Any) .ToArray(); @@ -64,10 +64,10 @@ public static Expression Process(Expression lambda, ObjectMapperData mapperData) finder.Visit(lambda); var assignmentReplacements = finder._assignmentsByMismatchSet - .SelectMany(kvp => kvp.Value.Project(assignment => new + .SelectMany(kvp => kvp.Value.Project(kvp.Key, (k, assignment) => new { Assignment = assignment, - AssignmentWithWarning = (Expression)Expression.Block(kvp.Key.Warnings, assignment) + AssignmentWithWarning = (Expression)Expression.Block(k.Warnings, assignment) })) .ToDictionary(d => d.Assignment, d => d.AssignmentWithWarning); @@ -92,7 +92,7 @@ private static IEnumerable<TargetMemberData> EnumerateTargetMemberDatas(ObjectMa var dataSources = targetMemberAndDataSource .Value - .Filter(dataSource => IsValidOtherEnumType(dataSource, targetEnumType)) + .Filter(targetEnumType, IsValidOtherEnumType) .ToArray(); if (dataSources.Any()) @@ -111,7 +111,7 @@ private static IEnumerable<TargetMemberData> EnumerateTargetMemberDatas(ObjectMa } } - private static bool IsValidOtherEnumType(IDataSource dataSource, Type targetEnumType) + private static bool IsValidOtherEnumType(Type targetEnumType, IDataSource dataSource) { return dataSource.IsValid && IsEnum(dataSource.SourceMember.Type, out var sourceEnumType) && @@ -163,7 +163,7 @@ private bool TryGetMatch(Expression targetMemberAccess, out TargetMemberData tar var memberName = targetMemberAccess.GetMemberName(); targetMemberData = _targetMemberDatas - .FirstOrDefault(dss => dss.TargetMember.Name == memberName); + .FirstOrDefault(memberName, (mn, dss) => dss.TargetMember.Name == mn); return targetMemberData != null; } diff --git a/AgileMapper/Validation/EnumMappingMismatchSet.cs b/AgileMapper/Validation/EnumMappingMismatchSet.cs index fce6fc54b..c353aeb90 100644 --- a/AgileMapper/Validation/EnumMappingMismatchSet.cs +++ b/AgileMapper/Validation/EnumMappingMismatchSet.cs @@ -55,7 +55,7 @@ public static EnumMappingMismatchSet For( var targetEnumNames = Enum.GetNames(targetEnumType); var mappingMismatches = sourceEnumData - .Filter(d => d.EnumType != targetEnumType) + .Filter(targetEnumType, (tet, d) => d.EnumType != tet) .Project(d => EnumMappingMismatch.For( d.EnumType, d.SourceMembers, @@ -221,7 +221,7 @@ private static void FilterOutConfiguredPairs( public string TargetMemberPath { get; } public string SourceMemberPaths => - _sourceMembers.Project(sm => sm.GetFriendlySourcePath(_rootMapperData)).Join(" / "); + _sourceMembers.Project(_rootMapperData, (rmd, sm) => sm.GetFriendlySourcePath(rmd)).Join(" / "); public string Warning => _warning ?? (_warning = CreateWarning()); diff --git a/AgileMapper/Validation/MappingValidator.cs b/AgileMapper/Validation/MappingValidator.cs index 2929d09e3..27643cf53 100644 --- a/AgileMapper/Validation/MappingValidator.cs +++ b/AgileMapper/Validation/MappingValidator.cs @@ -8,6 +8,7 @@ using Extensions; using Extensions.Internal; using Members; + using NetStandardPolyfills; using ObjectPopulation; using ReadableExpressions.Extensions; @@ -64,7 +65,7 @@ private static void VerifyMappingPlanIsComplete(IEnumerable<ObjectMapperData> ma failureMessage .Append(" Rule set: ").AppendLine(rootData.RuleSet.Name).AppendLine(); - AddUnmappableTargetTypesInfo(mappingData.UnmappableTargetTypes, failureMessage); + AddUnconstructableTargetTypesInfo(mappingData.UnconstructableTargetTypes, failureMessage); AddUnmappedTargetMembersInfo(mappingData.UnmappedMembers, failureMessage, rootData); AddUnpairedEnumsInfo(mappingData.UnpairedEnums, failureMessage); } @@ -79,26 +80,22 @@ private static ICollection<IncompleteMappingData> GetIncompleteMappingPlanData( .Project(md => new { MapperData = md, - IsUnmappable = - !md.IsRoot && - md.TargetMember.IsComplex && - md.DataSourcesByTargetMember.None(), + IsUnconstructable = TargetIsUnconstructable(md), UnmappedMembers = md .DataSourcesByTargetMember .Filter(pair => !pair.Value.HasValue) - .Project(pair => pair) .ToArray(), UnpairedEnums = EnumMappingMismatchFinder.FindMismatches(md) }) - .Filter(d => d.IsUnmappable || d.UnmappedMembers.Any() || d.UnpairedEnums.Any()) + .Filter(d => d.IsUnconstructable || d.UnmappedMembers.Any() || d.UnpairedEnums.Any()) .GroupBy(d => d.MapperData.GetRootMapperData()) .Project(g => new IncompleteMappingData { RootMapperData = g.Key, - UnmappableTargetTypes = g - .Filter(d => d.IsUnmappable) + UnconstructableTargetTypes = g + .Filter(d => d.IsUnconstructable) .Project(d => d.MapperData) - .ToList(), + .ToArray(), UnmappedMembers = g .SelectMany(d => d.UnmappedMembers) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value), @@ -109,6 +106,32 @@ private static ICollection<IncompleteMappingData> GetIncompleteMappingPlanData( .ToArray(); } + private static bool TargetIsUnconstructable(ObjectMapperData mapperData) + { + if (!mapperData.TargetMember.IsComplex || + mapperData.TargetIsDefinitelyPopulated()) + { + return false; + } + + if (mapperData.TargetType.GetPublicInstanceConstructor(Constants.EmptyTypeArray) != null) + { + return false; + } + + if (mapperData.DataSourcesByTargetMember.Any(ds => ds.Key.IsConstructorParameter() && ds.Value.HasValue)) + { + return false; + } + + var configuredFactories = mapperData + .MapperContext + .UserConfigurations + .GetObjectFactories(mapperData); + + return configuredFactories.None(); + } + private static void AddMappingTypeHeaderIfRequired( StringBuilder failureMessage, IMemberMapperData rootData, @@ -135,7 +158,7 @@ private static void AddMappingTypeHeaderIfRequired( previousRootMapperData = rootData; } - private static void AddUnmappableTargetTypesInfo( + private static void AddUnconstructableTargetTypesInfo( ICollection<ObjectMapperData> unmappableTargetTypeData, StringBuilder failureMessage) { @@ -160,7 +183,7 @@ private static void AddUnmappableTargetTypesInfo( } private static void AddUnmappedTargetMembersInfo( - Dictionary<QualifiedMember, DataSourceSet> unmappedMembers, + Dictionary<QualifiedMember, IDataSourceSet> unmappedMembers, StringBuilder failureMessage, IMemberMapperData rootData) { @@ -218,9 +241,9 @@ private class IncompleteMappingData { public IMemberMapperData RootMapperData { get; set; } - public ICollection<ObjectMapperData> UnmappableTargetTypes { get; set; } + public ICollection<ObjectMapperData> UnconstructableTargetTypes { get; set; } - public Dictionary<QualifiedMember, DataSourceSet> UnmappedMembers { get; set; } + public Dictionary<QualifiedMember, IDataSourceSet> UnmappedMembers { get; set; } public ICollection<EnumMappingMismatchSet> UnpairedEnums { get; set; } } diff --git a/NuGet/AgileObjects.AgileMapper.1.5.0-preview.nupkg b/NuGet/AgileObjects.AgileMapper.1.5.0-preview.nupkg new file mode 100644 index 000000000..d5bc8874f Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.5.0-preview.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.5.0-preview2.nupkg b/NuGet/AgileObjects.AgileMapper.1.5.0-preview2.nupkg new file mode 100644 index 000000000..cfe67b5c9 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.5.0-preview2.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.5.0-preview3.nupkg b/NuGet/AgileObjects.AgileMapper.1.5.0-preview3.nupkg new file mode 100644 index 000000000..42f8edda1 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.5.0-preview3.nupkg differ diff --git a/NuGet/AgileObjects.AgileMapper.1.5.0.nupkg b/NuGet/AgileObjects.AgileMapper.1.5.0.nupkg new file mode 100644 index 000000000..c9c9bd84e Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.5.0.nupkg differ diff --git a/VersionInfo.cs b/VersionInfo.cs index 46d1940cf..be6d1a51e 100644 --- a/VersionInfo.cs +++ b/VersionInfo.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("1.4.0")] -[assembly: AssemblyFileVersion("1.4.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.5.0")] +[assembly: AssemblyFileVersion("1.5.0")] \ No newline at end of file diff --git a/common.props b/common.props index 24c678855..bb52d5607 100644 --- a/common.props +++ b/common.props @@ -9,9 +9,9 @@ <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign> <RepositoryType>git</RepositoryType> <RepositoryUrl>https://github.com/AgileObjects/AgileMapper</RepositoryUrl> - <Version>1.4.0</Version> - <AssemblyVersion>1.4.0.0</AssemblyVersion> - <FileVersion>1.4.0.0</FileVersion> + <Version>1.5.0</Version> + <AssemblyVersion>1.5.0.0</AssemblyVersion> + <FileVersion>1.5.0.0</FileVersion> </PropertyGroup> </Project> \ No newline at end of file diff --git a/docs/src/configuration/Filtering-Source-Values.md b/docs/src/configuration/Filtering-Source-Values.md new file mode 100644 index 000000000..4bd4b4b68 --- /dev/null +++ b/docs/src/configuration/Filtering-Source-Values.md @@ -0,0 +1,43 @@ +[Compatible](/Type-Conversion) source members are automatically [matched](/Member-Matching) to target members, but you can tell a mapper to ignore source values if they match a given condition. For example: + +```cs +public class OrderDto +{ + public string Id { get; set; } +} + +public class Order +{ + public string Id { get; set; } +} +``` + +When updating an `Order` from an `OrderDto`, `Order.Id` is overwritten with the value of `OrderDto.Id` - including if `OrderDto.Id` is null or an empty string. You can stop this using: + +```cs +Mapper.WhenMapping + .Over<Order>() // Apply the filter to Order updates + .IgnoreSources(c => c.If(string.IsNullOrEmpty)); // Ignore null or empty source strings +``` + +Multiple value types can be filtered with a single configuration, and filters can be made conditional. Here's an [inline configuration](/configuration/Inline) example: + +```cs +// Source, target and mapping types are implicit from the mapping: +Mapper.Map(orderDto).Over(order, cfg => cfg + .If((dto, o) => dto.Id == "0") // Apply the filters only if OrderDto.Id is 0 + .IgnoreSources(c => c + .If(o => o == null) || // Ignore null source values of any type + .If<string>(str => str == string.Empty) || // Ignore empty source strings + .If<int>(i => i == int.MinValue)); // Ignore int.MinValue source ints +``` + +Source value filters can also be configured globally: + +```cs +// Ignore null or whitespace source strings in mappings +// for all rule sets (create new, update, merge, etc) and +// for all source and target types: +Mapper.WhenMapping + .IgnoreSources(c => c.If(string.IsNullOrWhiteSpace)) +``` \ No newline at end of file diff --git a/docs/src/configuration/Ignoring-Source-Members.md b/docs/src/configuration/Ignoring-Source-Members.md new file mode 100644 index 000000000..1c3b9d7b6 --- /dev/null +++ b/docs/src/configuration/Ignoring-Source-Members.md @@ -0,0 +1,121 @@ +[Compatible](/Type-Conversion) source members are automatically [matched](/Member-Matching) to target members, but you can tell a mapper to ignore source members which would usually be matched. For example: + +```cs +public class OrderDto +{ + public int Id { get; set; } + public string OrderNumber { get; set; } +} + +public class Order +{ + public int? Id { get; set; } + public string OrderNumber { get; set; } +} +``` + +`OrderDto.Id` member will be used to populate `Order.Id`, and `OrderDto.OrderNumber` will be used to populate `Order.OrderNumber`. To stop the `Id` mapping, use: + +```cs +Mapper.WhenMapping + .From<OrderDto>() // Apply when mapping from OrderDto + .Over<Order>() // Apply the ignore to Order updates (optional) + .IgnoreSource(dto => dto.Id); // Ignore the Order.Id property +``` + +Multiple members can be ignored with a single configuration, and ignores can be made conditional. Here's an [inline configuration](/configuration/Inline) example: + +```cs +// Source, target and mapping types are implicit from the mapping: +Mapper.Map(orderDto).Over(order, cfg => cfg + .If((dto, o) => dto.Id == 0) // Apply the ignores if OrderDto.Id is 0 + .IgnoreSource( + dto => dto.Id, // Ignore Order.Id... + dto => dto.OrderNumber); // ...and Order.OrderNumber +``` + +Source members can be ignored in several other ways, either globally (for all source and target types), or for specific source and target types. + +## Source Member Filtering + +Source members can be ignored by Type: + +```cs +Mapper.WhenMapping + .IgnoreSourceMembersOfType<IDontMapMe>(); // Global ignore + +Mapper.WhenMapping + .From<MySource>() // Apply when mapping from MySource + .OnTo<MyTarget>() // Apply the ignore to MyTarget merges (optional) + .IgnoreSourceMembersOfType<IDontMapMe>(); // Ignore all IDontMapMe members +``` + +...by member type: + +```cs +Mapper.WhenMapping + .IgnoreSourceMembersWhere(m => m.IsGetMethod); // Global ignore + +Mapper.WhenMapping + .ToANew<MyTarget>() // Apply the ignore to MyTarget creations + .IgnoreSourceMembersWhere(m => m.IsField); // Ignore all fields + +Mapper.WhenMapping + .From<MySource>() // Apply when mapping from MySource (optional) + .Over<MyTarget>() // Apply the ignore to MyTarget updates + .IgnoreSourceMembersWhere(m => m.IsProperty); // Ignore all properties +``` + +...by member name: + +```cs +Mapper.WhenMapping + .IgnoreSourceMembersWhere(m => m.Name.Contains("NOPE")); // Global ignore + +Mapper.WhenMapping + .ToANew<MyTarget>() // Apply the ignore to MyTarget creations + .IgnoreSourceMembersWhere(m => m.Name.Contains("NOPE")); // Ignore +``` + +...by Attribute: + +```cs +Mapper.WhenMapping + .IgnoreSourceMembersWhere(m => + m.HasAttribute<IgnoreDataMember>()); // Global ignore + +Mapper.WhenMapping + .OnTo<MyTarget>() // Apply the ignore to MyTarget merges + .IgnoreSourceMembersWhere(m => + m.HasAttribute<IgnoreDataMember>()); // Ignore +``` + +...by member path: + +```cs +Mapper.WhenMapping + .IgnoreSourceMembersWhere(m => + m.Path.Contains("Customer.Address")); // Global ignore + +Mapper.WhenMapping + .Over<MyTarget>() // Apply the ignore to MyTarget updates + .IgnoreSourceMembersWhere(m => + m.Path == "Customer.Address"); // Ignore +``` + +...or by `MemberInfo` matcher: + +```cs +Mapper.WhenMapping + .IgnoreSourceMembersWhere(m => + m.IsFieldMatching(f => f.IsAssembly)); // Global ignore + +Mapper.WhenMapping + .Over<MyTarget>() // Apply the ignore to MyTarget updates + .IgnoreSourceMembersWhere(m => + m.IsPropertyMatching(p => p.IsAssembly)); // Ignore +``` + +Again, all ignores can alternatively be configured [inline](/configuration/Inline). + +You can also ignore [target members](Ignoring-Target-Members) \ No newline at end of file diff --git a/docs/src/configuration/Ignoring-Target-Members.md b/docs/src/configuration/Ignoring-Target-Members.md index fab073dd2..7b81144fe 100644 --- a/docs/src/configuration/Ignoring-Target-Members.md +++ b/docs/src/configuration/Ignoring-Target-Members.md @@ -13,7 +13,7 @@ public class Order } ``` -`Order.DateCreated` will be ignored because `OrderDto` has no matching member, but out of the box the `Id` property will be updated. You can stop this using: +`Order.DateCreated` will be ignored because `OrderDto` has no matching member, but out of the box the `Id` property will be updated. To stop this, use: ```cs Mapper.WhenMapping @@ -22,23 +22,22 @@ Mapper.WhenMapping .Ignore(o => o.Id); // Ignore the Order.Id property ``` -Multiple fields can be ignored with a single configuration, and ignores can be made conditional. Here's an [inline configuration](/configuration/Inline) example: +Multiple members can be ignored with a single configuration, and ignores can be made conditional. Here's an [inline configuration](/configuration/Inline) example: ```cs // Source, target and mapping types are implicit from the mapping: -Mapper - .Map(orderDto).Over(order, cfg => cfg - .If((dto, o) => dto.Id == 0) // Apply the ignores if OrderDto.Id is 0 - .Ignore( - o => o.Id, - o => o.DateCreated); // Ignore Order.Id and Order.DateCreated +Mapper.Map(orderDto).Over(order, cfg => cfg + .If((dto, o) => dto.Id == 0) // Apply the ignores if OrderDto.Id is 0 + .Ignore( + o => o.Id, // Ignore Order.Id... + o => o.DateCreated); // ...and Order.DateCreated ``` Target members can be ignored in several other ways, either globally (for all source and target types), or for specific source and target types. -## Member Filtering +## Target Member Filtering -You can ignore members by Type: +Target members can be ignored by Type: ```cs Mapper.WhenMapping @@ -116,4 +115,6 @@ Mapper.WhenMapping m.IsPropertyMatching(p => p.IsAssembly)); // Ignore ``` -Again, all ignores can alternatively be configured [inline](/configuration/Inline). \ No newline at end of file +Again, all ignores can alternatively be configured [inline](/configuration/Inline). + +You can also ignore [source members](Ignoring-Source-Members) \ No newline at end of file