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/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/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs index aab418bf8..a11e5a2cf 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs @@ -9,7 +9,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using Extensions.Internal; using Members; using NetStandardPolyfills; - using ReadableExpressions; using ReadableExpressions.Extensions; internal class ComplexTypeMappingExpressionFactory : MappingExpressionFactoryBase @@ -33,31 +32,28 @@ 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)) { - 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; } diff --git a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs index a9726b3ad..19f249bbc 100644 --- a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs @@ -3,6 +3,11 @@ 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; @@ -13,13 +18,7 @@ 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 { @@ -255,27 +254,27 @@ public override bool IsFor(IObjectMappingData mappingData) 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) { if (!context.MapperData.TargetMember.IsDictionary) diff --git a/AgileMapper/ObjectPopulation/EnumMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/EnumMappingExpressionFactory.cs new file mode 100644 index 000000000..85a3497cf --- /dev/null +++ b/AgileMapper/ObjectPopulation/EnumMappingExpressionFactory.cs @@ -0,0 +1,48 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + using System.Collections.Generic; + using System.Globalization; + using Extensions.Internal; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + using Members; + using NetStandardPolyfills; + using ReadableExpressions.Extensions; + + internal class EnumMappingExpressionFactory : MappingExpressionFactoryBase + { + public static readonly EnumMappingExpressionFactory Instance = new EnumMappingExpressionFactory(); + + public override bool IsFor(IObjectMappingData mappingData) + => mappingData.MapperData.TargetType.GetNonNullableType().IsEnum(); + + 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/EnumerableMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs index 4110298b1..e368156ca 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs @@ -3,7 +3,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Collections.Generic; using Extensions.Internal; using Members; - using ReadableExpressions; #if NET35 using Microsoft.Scripting.Ast; #else @@ -17,25 +16,22 @@ internal class EnumerableMappingExpressionFactory : MappingExpressionFactoryBase 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 +39,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)) 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/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 65c24aa42..2d1eb7d18 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -13,6 +13,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Extensions.Internal; using Members; using NetStandardPolyfills; + using ReadableExpressions; using ReadableExpressions.Extensions; #if NET35 using static Microsoft.Scripting.Ast.ExpressionType; @@ -28,9 +29,11 @@ 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( @@ -69,12 +72,15 @@ public Expression Create(IObjectMappingData mappingData) 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; } + protected virtual Expression GetNullMappingFallbackValue(IMemberMapperData mapperData) + => mapperData.TargetType.ToDefaultExpression(); + private bool MappingAlwaysBranchesToDerivedType(IObjectMappingData mappingData, out Expression derivedTypeMappings) { derivedTypeMappings = GetDerivedTypeMappings(mappingData); @@ -225,23 +231,28 @@ private Expression GetMappingBlock(MappingCreationContext context) AdjustForSingleExpressionBlockIfApplicable(context); + var firstExpression = mappingExpressions.First(); + if (context.MapperData.UseSingleMappingExpression()) { - return mappingExpressions.First(); + return firstExpression; } - if (mappingExpressions.HasOne() && (mappingExpressions[0].NodeType == Constant)) + var isSingleExpression = mappingExpressions.HasOne(); + var firstExpressionType = firstExpression.NodeType; + + if (isSingleExpression && (firstExpressionType == Constant)) { goto CreateFullMappingBlock; } Expression returnExpression; - if (mappingExpressions[0].NodeType != Block) + if (firstExpressionType != Block) { - if (mappingExpressions[0].NodeType == MemberAccess) + if (UseFirstExpression(firstExpression, isSingleExpression)) { - return GetReturnExpression(mappingExpressions[0], context); + return GetReturnExpression(firstExpression, context); } if (TryAdjustForUnusedLocalVariableIfApplicable(context, out returnExpression)) @@ -283,6 +294,19 @@ private static void AdjustForSingleExpressionBlockIfApplicable(MappingCreationCo } } + private static bool UseFirstExpression(Expression firstExpression, bool isSingleExpression) + { + if (firstExpression.NodeType == MemberAccess) + { + return true; + } + + return isSingleExpression && + (firstExpression.NodeType == Conditional) && + (firstExpression.Type != typeof(void)); + + } + private static bool TryAdjustForUnusedLocalVariableIfApplicable(MappingCreationContext context, out Expression returnExpression) { if (!context.MapperData.Context.UseLocalVariable) diff --git a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs index 7eebbb5c9..be8a20a6a 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs @@ -25,6 +25,7 @@ public ObjectMapperFactory(CacheSet mapperScopedCacheSet) _mappingExpressionFactories = new[] { QueryProjectionExpressionFactory.Instance, + EnumMappingExpressionFactory.Instance, DictionaryMappingExpressionFactory.Instance, EnumerableMappingExpressionFactory.Instance, ComplexTypeMappingExpressionFactory.Instance