From 51ce626a28fc116f4a64127c81aab44fa4336ef4 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 10 Jan 2018 19:54:10 +0000 Subject: [PATCH 1/7] Start of flags enum support --- .../AgileMapper.UnitTests.csproj | 2 ++ .../WhenConvertingToFlagsEnums.cs | 17 +++++++++++++++++ AgileMapper.UnitTests/TestClasses/Status.cs | 15 +++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs create mode 100644 AgileMapper.UnitTests/TestClasses/Status.cs diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index c8106b6e2..32f08d2a8 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -143,6 +143,7 @@ + @@ -207,6 +208,7 @@ + diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs new file mode 100644 index 000000000..ba04bad96 --- /dev/null +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs @@ -0,0 +1,17 @@ +namespace AgileObjects.AgileMapper.UnitTests.SimpleTypeConversion +{ + using TestClasses; + using Xunit; + + public class WhenConvertingToFlagsEnums + { + [Fact] + public void ShouldMapASingleValueByteToAFlagsEnum() + { + var source = new PublicField { Value = (byte)Status.InProgress }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldBe(Status.InProgress); + } + } +} diff --git a/AgileMapper.UnitTests/TestClasses/Status.cs b/AgileMapper.UnitTests/TestClasses/Status.cs new file mode 100644 index 000000000..c59432398 --- /dev/null +++ b/AgileMapper.UnitTests/TestClasses/Status.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.UnitTests.TestClasses +{ + using System; + + [Flags] + public enum Status + { + New = 1, + Assigned = 2, + InProgress = 4, + Complete = 8, + Cancelled = 16, + Removed = 32 + } +} \ No newline at end of file From d3b6338b95269700e1c6f3b984cd7336d513d555 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 10 Jan 2018 20:24:41 +0000 Subject: [PATCH 2/7] Mapping numeric values to enums with IsDefined and a cast, instead of TryParse with the string value --- .../WhenConvertingToEnums.cs | 49 ++++---- .../WhenConvertingToFlagsEnums.cs | 17 ++- AgileMapper/TypeConversion/ToEnumConverter.cs | 106 +++++++++++++++--- 3 files changed, 133 insertions(+), 39 deletions(-) diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs index babaa6d3a..0cbb26415 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -2,40 +2,41 @@ { using TestClasses; using Xunit; + using static TestClasses.Title; public class WhenConvertingToEnums { [Fact] public void ShouldMapAByteToAnEnum() { - var source = new PublicField { Value = (byte)Title.Dr }; + var source = new PublicField { Value = (byte)Dr }; var result = Mapper.Map(source).ToANew>(); - result.Value.ShouldBe(Title.Dr); + result.Value.ShouldBe(Dr); } [Fact] public void ShouldMapAShortToAnEnum() { - var source = new PublicField { Value = (short)Title.Miss }; + var source = new PublicField { Value = (short)Miss }; var result = Mapper.Map(source).ToANew>(); - result.Value.ShouldBe(Title.Miss); + result.Value.ShouldBe(Miss); } [Fact] public void ShouldMapANullableIntToAnEnum() { - var source = new PublicProperty { Value = (int)Title.Lady }; + var source = new PublicProperty { Value = (int)Lady }; var result = Mapper.Map(source).ToANew>(); - result.Value.ShouldBe(Title.Lady); + result.Value.ShouldBe(Lady); } [Fact] public void ShouldMapAnIntToAnEnum() { - var source = new PublicProperty { Value = (int)Title.Dr }; + var source = new PublicProperty { Value = (int)Dr }; var result = Mapper.Map(source).ToANew>(); result.Value.ShouldBe((Title)source.Value); @@ -53,7 +54,7 @@ public void ShouldMapANullNullableIntToAnEnum() [Fact] public void ShouldMapALongToAnEnum() { - var source = new PublicProperty { Value = (long)Title.Miss }; + var source = new PublicProperty { Value = (long)Miss }; var result = Mapper.Map(source).ToANew>(); result.Value.ShouldBe((Title)source.Value); @@ -62,7 +63,7 @@ public void ShouldMapALongToAnEnum() [Fact] public void ShouldMapANonMatchingNullableLongToANullableEnum() { - var source = new PublicProperty { Value = (long)Title.Earl }; + var source = new PublicProperty { Value = (long)Earl }; var result = Mapper.Map(source).ToANew>(); result.Value.ShouldBeNull(); @@ -98,28 +99,28 @@ public void ShouldMapAMatchingNullableCharacterOnToANullableEnum() [Fact] public void ShouldMapAMatchingStringOnToAnEnum() { - var source = new PublicField { Value = Title.Mrs.ToString() }; + var source = new PublicField { Value = Mrs.ToString() }; var result = Mapper.Map(source).OnTo(new PublicProperty()); - result.Value.ShouldBe(Title.Mrs); + result.Value.ShouldBe(Mrs); } [Fact] public void ShouldMapAMatchingStringOnToAnEnumCaseInsensitively() { - var source = new PublicField<string> { Value = Title.Miss.ToString().ToLowerInvariant() }; + var source = new PublicField<string> { Value = Miss.ToString().ToLowerInvariant() }; var result = Mapper.Map(source).OnTo(new PublicProperty<Title>()); - result.Value.ShouldBe(Title.Miss); + result.Value.ShouldBe(Miss); } [Fact] public void ShouldMapAMatchingNumericStringOverAnEnum() { - var source = new PublicField<string> { Value = ((int)Title.Dr).ToString() }; + var source = new PublicField<string> { Value = ((int)Dr).ToString() }; var result = Mapper.Map(source).Over(new PublicProperty<Title>()); - result.Value.ShouldBe(Title.Dr); + result.Value.ShouldBe(Dr); } [Fact] @@ -146,13 +147,13 @@ public void ShouldMapAnEnumToAnEnum() var source = new PublicProperty<TitleShortlist> { Value = TitleShortlist.Mrs }; var result = Mapper.Map(source).ToANew<PublicProperty<Title>>(); - result.Value.ShouldBe(Title.Mrs); + result.Value.ShouldBe(Mrs); } [Fact] public void ShouldMapANonMatchingEnumToANullableEnum() { - var source = new PublicProperty<Title> { Value = Title.Lord }; + var source = new PublicProperty<Title> { Value = Lord }; var result = Mapper.Map(source).ToANew<PublicProperty<TitleShortlist?>>(); result.Value.ShouldBeNull(); @@ -161,10 +162,10 @@ public void ShouldMapANonMatchingEnumToANullableEnum() [Fact] public void ShouldMapANullableEnumToAnEnum() { - var source = new PublicProperty<Title?> { Value = Title.Dr }; + var source = new PublicProperty<Title?> { Value = Dr }; var result = Mapper.Map(source).ToANew<PublicProperty<Title>>(); - result.Value.ShouldBe(Title.Dr); + result.Value.ShouldBe(Dr); } [Fact] @@ -206,7 +207,7 @@ public void ShouldMapAnObjectEnumMemberValueToAnNullableEnum() [Fact] public void ShouldMapAnObjectNullableEnumMemberValueToAnNullableEnum() { - var source = new PublicProperty<object> { Value = (Title?)Title.Mr }; + var source = new PublicProperty<object> { Value = (Title?)Mr }; var result = Mapper.Map(source).ToANew<PublicProperty<TitleShortlist>>(); result.Value.ShouldBe(TitleShortlist.Mr); @@ -225,20 +226,20 @@ public void ShouldMapEnumsConditionally() .If((ptf, pf) => ptf.Value1 == null) .Map((ptf, pf) => ptf.Value2).To(pf => pf.Value) .And - .If((ptf, pf) => Title.Duke == ptf.Value1) + .If((ptf, pf) => Duke == ptf.Value1) .Map(TitleShortlist.Other).To(pf => pf.Value); - var nonNullSource = new PublicTwoFields<Title?, Title> { Value1 = Title.Dr, Value2 = Title.Count }; + var nonNullSource = new PublicTwoFields<Title?, Title> { Value1 = Dr, Value2 = Count }; var nonNullResult = mapper.Map(nonNullSource).ToANew<PublicField<TitleShortlist>>(); nonNullResult.Value.ShouldBe(TitleShortlist.Dr); - var nullSource = new PublicTwoFields<Title?, Title> { Value1 = null, Value2 = Title.Mrs }; + var nullSource = new PublicTwoFields<Title?, Title> { Value1 = null, Value2 = Mrs }; var nullResult = mapper.Map(nullSource).ToANew<PublicField<TitleShortlist>>(); nullResult.Value.ShouldBe(TitleShortlist.Mrs); - var dukeSource = new PublicTwoFields<Title?, Title> { Value1 = Title.Duke }; + var dukeSource = new PublicTwoFields<Title?, Title> { Value1 = Duke }; var dukeResult = mapper.Map(dukeSource).ToANew<PublicField<TitleShortlist>>(); dukeResult.Value.ShouldBe(TitleShortlist.Other); diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs index ba04bad96..f5ee43842 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs @@ -2,16 +2,29 @@ { using TestClasses; using Xunit; + using static TestClasses.Status; public class WhenConvertingToFlagsEnums { [Fact] public void ShouldMapASingleValueByteToAFlagsEnum() { - var source = new PublicField<byte> { Value = (byte)Status.InProgress }; + var source = new PublicField<byte> { Value = (byte)InProgress }; var result = Mapper.Map(source).ToANew<PublicField<Status>>(); - result.Value.ShouldBe(Status.InProgress); + result.Value.ShouldBe(InProgress); + } + + [Fact] + public void ShouldMapAMultiValueShortToAnEnum() + { + var source = new PublicField<short> { Value = (short)(InProgress | Assigned) }; + var result = Mapper.Map(source).ToANew<PublicField<Status>>(); + + result.Value.HasFlag(InProgress).ShouldBeTrue(); + result.Value.HasFlag(Assigned).ShouldBeTrue(); + result.Value.HasFlag(Cancelled).ShouldBeFalse(); + result.Value.ShouldBe(InProgress | Assigned); } } } diff --git a/AgileMapper/TypeConversion/ToEnumConverter.cs b/AgileMapper/TypeConversion/ToEnumConverter.cs index 2976fc397..f01bc42b8 100644 --- a/AgileMapper/TypeConversion/ToEnumConverter.cs +++ b/AgileMapper/TypeConversion/ToEnumConverter.cs @@ -22,19 +22,91 @@ public override bool CanConvert(Type nonNullableSourceType, Type nonNullableTarg } return nonNullableSourceType.IsEnum() || - (nonNullableSourceType == typeof(string)) || - (nonNullableSourceType == typeof(object)) || - (nonNullableSourceType == typeof(char)) || - nonNullableSourceType.IsNumeric(); + (nonNullableSourceType == typeof(string)) || + (nonNullableSourceType == typeof(object)) || + (nonNullableSourceType == typeof(char)) || + nonNullableSourceType.IsNumeric(); } - public override Expression GetConversion(Expression sourceValue, Type targetType) + public override Expression GetConversion(Expression sourceValue, Type targetEnumType) + { + var fallbackValue = targetEnumType.ToDefaultExpression(); + var nonNullableTargetEnumType = targetEnumType.GetNonNullableType(); + + if (nonNullableTargetEnumType.HasAttribute<FlagsAttribute>()) + { + return GetFlagsEnumConversion(sourceValue, fallbackValue, nonNullableTargetEnumType); + } + + var nonNullableSourceType = sourceValue.Type.GetNonNullableType(); + + if (nonNullableSourceType.IsNumeric()) + { + return GetNumericToEnumConversion( + sourceValue, + fallbackValue, + nonNullableSourceType, + nonNullableTargetEnumType); + } + + return GetTryParseConversion( + sourceValue, + fallbackValue, + nonNullableSourceType, + nonNullableTargetEnumType); + } + + private static Expression GetNumericToEnumConversion( + Expression sourceValue, + Expression fallbackValue, + Type nonNullableSourceType, + Type nonNullableTargetEnumType) + { + var convertedSourceValue = sourceValue + .GetConversionTo(Enum.GetUnderlyingType(nonNullableTargetEnumType)) + .GetConversionTo(typeof(object)); + + var numericValueIsDefined = Expression.Call( + typeof(Enum).GetPublicStaticMethod("IsDefined"), + nonNullableTargetEnumType.ToConstantExpression(), + convertedSourceValue); + + Expression convertedNumericValue = Expression.Convert(sourceValue, nonNullableTargetEnumType); + + if (nonNullableTargetEnumType != fallbackValue.Type) + { + convertedNumericValue = convertedNumericValue.GetConversionTo(fallbackValue.Type); + } + + var definedValueOrFallback = Expression.Condition( + numericValueIsDefined, + convertedNumericValue, + fallbackValue); + + if (sourceValue.Type == nonNullableSourceType) + { + return definedValueOrFallback; + } + + var nonNullDefinedValueOrFallback = Expression.Condition( + sourceValue.GetIsNotDefaultComparison(), + definedValueOrFallback, + fallbackValue); + + return nonNullDefinedValueOrFallback; + } + + private Expression GetTryParseConversion( + Expression sourceValue, + Expression fallbackValue, + Type nonNullableSourceType, + Type nonNullableTargetEnumType) { bool sourceIsAnEnum; if (sourceValue.Type != typeof(string)) { - sourceIsAnEnum = sourceValue.Type.GetNonNullableType().IsEnum(); + sourceIsAnEnum = nonNullableSourceType.IsEnum(); sourceValue = _toStringConverter.GetConversion(sourceValue); } else @@ -42,13 +114,13 @@ public override Expression GetConversion(Expression sourceValue, Type targetType sourceIsAnEnum = false; } - var nonNullableEnumType = targetType.GetNonNullableType(); - var tryParseMethod = typeof(Enum) .GetPublicStaticMethod("TryParse", parameterCount: 3) - .MakeGenericMethod(nonNullableEnumType); + .MakeGenericMethod(nonNullableTargetEnumType); - var valueVariable = Expression.Variable(nonNullableEnumType, nonNullableEnumType.GetShortVariableName()); + var valueVariable = Expression.Variable( + nonNullableTargetEnumType, + nonNullableTargetEnumType.GetShortVariableName()); var tryParseCall = Expression.Call( tryParseMethod, @@ -56,10 +128,10 @@ public override Expression GetConversion(Expression sourceValue, Type targetType true.ToConstantExpression(), // <- IgnoreCase valueVariable); - var defaultValue = targetType.ToDefaultExpression(); - var parseSuccessBranch = GetParseSuccessBranch(sourceIsAnEnum, valueVariable, defaultValue); - var parsedValueOrDefault = Expression.Condition(tryParseCall, parseSuccessBranch, defaultValue); + var parseSuccessBranch = GetParseSuccessBranch(sourceIsAnEnum, valueVariable, fallbackValue); + + var parsedValueOrDefault = Expression.Condition(tryParseCall, parseSuccessBranch, fallbackValue); var tryParseBlock = Expression.Block(new[] { valueVariable }, parsedValueOrDefault); return tryParseBlock; @@ -89,5 +161,13 @@ private static Expression GetParseSuccessBranch( return definedValueOrDefault; } + + private Expression GetFlagsEnumConversion( + Expression sourceValue, + Expression fallbackValue, + Type nonNullableTargetEnumType) + { + throw new NotImplementedException(); + } } } \ No newline at end of file From 298771b4a6906434836899b81a4925dcfa31b349 Mon Sep 17 00:00:00 2001 From: Steve Wilkes <steve@agileobjects.co.uk> Date: Wed, 10 Jan 2018 20:34:08 +0000 Subject: [PATCH 3/7] Reusing Enum.IsDefined call creation method --- AgileMapper/TypeConversion/ToEnumConverter.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/AgileMapper/TypeConversion/ToEnumConverter.cs b/AgileMapper/TypeConversion/ToEnumConverter.cs index f01bc42b8..edd865f83 100644 --- a/AgileMapper/TypeConversion/ToEnumConverter.cs +++ b/AgileMapper/TypeConversion/ToEnumConverter.cs @@ -62,15 +62,6 @@ private static Expression GetNumericToEnumConversion( Type nonNullableSourceType, Type nonNullableTargetEnumType) { - var convertedSourceValue = sourceValue - .GetConversionTo(Enum.GetUnderlyingType(nonNullableTargetEnumType)) - .GetConversionTo(typeof(object)); - - var numericValueIsDefined = Expression.Call( - typeof(Enum).GetPublicStaticMethod("IsDefined"), - nonNullableTargetEnumType.ToConstantExpression(), - convertedSourceValue); - Expression convertedNumericValue = Expression.Convert(sourceValue, nonNullableTargetEnumType); if (nonNullableTargetEnumType != fallbackValue.Type) @@ -79,7 +70,7 @@ private static Expression GetNumericToEnumConversion( } var definedValueOrFallback = Expression.Condition( - numericValueIsDefined, + GetEnumIsDefinedCall(nonNullableTargetEnumType, sourceValue), convertedNumericValue, fallbackValue); @@ -96,6 +87,18 @@ private static Expression GetNumericToEnumConversion( return nonNullDefinedValueOrFallback; } + private static Expression GetEnumIsDefinedCall(Type enumType, Expression value) + { + var convertedValue = value + .GetConversionTo(Enum.GetUnderlyingType(enumType)) + .GetConversionTo(typeof(object)); + + return Expression.Call( + typeof(Enum).GetPublicStaticMethod("IsDefined"), + enumType.ToConstantExpression(), + convertedValue); + } + private Expression GetTryParseConversion( Expression sourceValue, Expression fallbackValue, @@ -151,13 +154,10 @@ private static Expression GetParseSuccessBranch( return successfulParseReturnValue; } - var isDefinedCall = Expression.Call( - null, - typeof(Enum).GetPublicStaticMethod("IsDefined"), - valueVariable.Type.ToConstantExpression(), - valueVariable.GetConversionTo(typeof(object))); - - var definedValueOrDefault = Expression.Condition(isDefinedCall, successfulParseReturnValue, defaultValue); + var definedValueOrDefault = Expression.Condition( + GetEnumIsDefinedCall(valueVariable.Type, valueVariable), + successfulParseReturnValue, + defaultValue); return definedValueOrDefault; } From d1913313880b5ac38d06f1a448ced71ab84086f4 Mon Sep 17 00:00:00 2001 From: Steve Wilkes <steve@agileobjects.co.uk> Date: Wed, 10 Jan 2018 21:43:05 +0000 Subject: [PATCH 4/7] Support for mapping numeric values to multi-value flags enums --- .../EnumerableSourcePopulationLoopData.cs | 9 +- AgileMapper/TypeConversion/ToEnumConverter.cs | 91 ++++++++++++++++++- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs index 138f719f6..2c933b3ed 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableSourcePopulationLoopData.cs @@ -10,9 +10,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables internal class EnumerableSourcePopulationLoopData : IPopulationLoopData { - private static readonly MethodInfo _enumeratorMoveNextMethod = typeof(IEnumerator).GetPublicInstanceMethod("MoveNext"); - private static readonly MethodInfo _disposeMethod = typeof(IDisposable).GetPublicInstanceMethod("Dispose"); - private readonly Expression _enumerableSubject; private readonly MethodInfo _getEnumeratorMethod; private readonly ParameterExpression _enumerator; @@ -34,7 +31,7 @@ public EnumerableSourcePopulationLoopData( _enumerator = Expression.Variable(_getEnumeratorMethod.ReturnType, "enumerator"); ContinueLoopTarget = Expression.Label(typeof(void), "Continue"); - LoopExitCheck = Expression.Not(Expression.Call(_enumerator, _enumeratorMoveNextMethod)); + LoopExitCheck = Expression.Not(Expression.Call(_enumerator, typeof(IEnumerator).GetPublicInstanceMethod("MoveNext"))); SourceElement = Expression.Property(_enumerator, "Current"); } @@ -69,7 +66,9 @@ public BlockExpression GetLoopBlock( var enumeratorAssignment = _enumerator.AssignTo(enumeratorValue); - Expression finallyClause = Expression.Call(_enumerator, _disposeMethod); + Expression finallyClause = Expression.Call( + _enumerator, + typeof(IDisposable).GetPublicInstanceMethod("Dispose")); if (finallyClauseFactory != null) { diff --git a/AgileMapper/TypeConversion/ToEnumConverter.cs b/AgileMapper/TypeConversion/ToEnumConverter.cs index edd865f83..045d14acc 100644 --- a/AgileMapper/TypeConversion/ToEnumConverter.cs +++ b/AgileMapper/TypeConversion/ToEnumConverter.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper.TypeConversion { using System; + using System.Collections; using System.Linq.Expressions; using Extensions.Internal; using NetStandardPolyfills; @@ -31,14 +32,18 @@ public override bool CanConvert(Type nonNullableSourceType, Type nonNullableTarg public override Expression GetConversion(Expression sourceValue, Type targetEnumType) { var fallbackValue = targetEnumType.ToDefaultExpression(); + var nonNullableSourceType = sourceValue.Type.GetNonNullableType(); var nonNullableTargetEnumType = targetEnumType.GetNonNullableType(); if (nonNullableTargetEnumType.HasAttribute<FlagsAttribute>()) { - return GetFlagsEnumConversion(sourceValue, fallbackValue, nonNullableTargetEnumType); + return GetFlagsEnumConversion( + sourceValue, + fallbackValue, + nonNullableSourceType, + nonNullableTargetEnumType); } - var nonNullableSourceType = sourceValue.Type.GetNonNullableType(); if (nonNullableSourceType.IsNumeric()) { @@ -165,9 +170,91 @@ private static Expression GetParseSuccessBranch( private Expression GetFlagsEnumConversion( Expression sourceValue, Expression fallbackValue, + Type nonNullableSourceType, Type nonNullableTargetEnumType) { + if (nonNullableSourceType.IsNumeric()) + { + return GetNumericToFlagsEnumConversion( + sourceValue, + fallbackValue, + nonNullableSourceType, + nonNullableTargetEnumType); + } + throw new NotImplementedException(); } + + private static Expression GetNumericToFlagsEnumConversion( + Expression sourceValue, + Expression fallbackValue, + Type nonNullableSourceType, + Type nonNullableTargetEnumType) + { + var enumTypeName = nonNullableTargetEnumType.GetVariableNameInCamelCase(); + var underlyingEnumType = Enum.GetUnderlyingType(nonNullableTargetEnumType); + + var enumValueVariable = Expression.Variable(underlyingEnumType, enumTypeName + "Value"); + var assignEnumValue = Expression.Assign(enumValueVariable, underlyingEnumType.ToDefaultExpression()); + + var enumValuesVariable = Expression.Variable(typeof(IEnumerator), enumTypeName + "Values"); + + var enumGetValuesCall = Expression.Call( + typeof(Enum).GetPublicStaticMethod("GetValues"), + nonNullableTargetEnumType.ToConstantExpression()); + + var getValuesEnumeratorCall = Expression.Call( + enumGetValuesCall, + enumGetValuesCall.Type.GetPublicInstanceMethod("GetEnumerator")); + + var assignEnumValues = Expression.Assign(enumValuesVariable, getValuesEnumeratorCall); + + var enumeratorMoveNext = Expression.Call( + enumValuesVariable, + enumValuesVariable.Type.GetPublicInstanceMethod("MoveNext")); + + var loopBreakTarget = Expression.Label(); + + var ifNotMoveNextBreak = Expression.IfThen( + Expression.Not(enumeratorMoveNext), + Expression.Break(loopBreakTarget)); + + var localEnumValueVariable = Expression.Variable(underlyingEnumType, enumTypeName); + var enumeratorCurrent = Expression.Property(enumValuesVariable, "Current"); + var currentAsEnumType = Expression.Convert(enumeratorCurrent, underlyingEnumType); + var assignLocalVariable = Expression.Assign(localEnumValueVariable, currentAsEnumType); + + var sourceValueVariable = Expression.Variable(underlyingEnumType, enumTypeName + "Source"); + + if (sourceValue.Type != underlyingEnumType) + { + sourceValue = Expression.Convert(sourceValue, underlyingEnumType); + } + + var assignSourceVariable = Expression.Assign(sourceValueVariable, sourceValue); + + var localVariableAndSourceValue = Expression.And(localEnumValueVariable, sourceValueVariable); + var andResultEqualsEnumValue = Expression.Equal(localVariableAndSourceValue, localEnumValueVariable); + + var ifAndResultMatchesAssign = Expression.IfThen( + andResultEqualsEnumValue, + Expression.OrAssign(enumValueVariable, localEnumValueVariable)); + + var loopBody = Expression.Block( + new[] { localEnumValueVariable }, + ifNotMoveNextBreak, + assignLocalVariable, + ifAndResultMatchesAssign); + + var populationBlock = Expression.Block( + new[] { sourceValueVariable, enumValueVariable, enumValuesVariable }, + assignSourceVariable, + assignEnumValue, + assignEnumValues, + Expression.Loop(loopBody, loopBreakTarget), + enumValueVariable.GetConversionTo(fallbackValue.Type)); + + return populationBlock; + } } } \ No newline at end of file From 6e118897671f1964e150141e143a057b415dbbe1 Mon Sep 17 00:00:00 2001 From: Steve Wilkes <steve@agileobjects.co.uk> Date: Thu, 11 Jan 2018 07:53:14 +0000 Subject: [PATCH 5/7] Extending numeric to flags enum test coverage --- .../WhenConvertingToFlagsEnums.cs | 29 ++++++++++++++++++- AgileMapper.UnitTests/TestClasses/Status.cs | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs index f5ee43842..b62efa755 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs @@ -16,7 +16,7 @@ public void ShouldMapASingleValueByteToAFlagsEnum() } [Fact] - public void ShouldMapAMultiValueShortToAnEnum() + public void ShouldMapAMultiValueShortToAFlagsEnum() { var source = new PublicField<short> { Value = (short)(InProgress | Assigned) }; var result = Mapper.Map(source).ToANew<PublicField<Status>>(); @@ -26,5 +26,32 @@ public void ShouldMapAMultiValueShortToAnEnum() result.Value.HasFlag(Cancelled).ShouldBeFalse(); result.Value.ShouldBe(InProgress | Assigned); } + + [Fact] + public void ShouldMapAMultiValueNullableIntToAFlagsEnum() + { + var source = new PublicProperty<int?> { Value = (int)(New | Completed | Cancelled) }; + var result = Mapper.Map(source).ToANew<PublicField<Status>>(); + + result.Value.ShouldBe(New | Completed | Cancelled); + } + + [Fact] + public void ShouldMapANullNullableIntToANullableFlagsEnum() + { + var source = new PublicProperty<int?> { Value = default(int?) }; + var result = Mapper.Map(source).ToANew<PublicField<Status?>>(); + + result.Value.ShouldBeNull(); + } + + [Fact] + public void ShouldMapASingleValueLongToANullableFlagsEnum() + { + var source = new PublicProperty<long> { Value = (long)Removed }; + var result = Mapper.Map(source).ToANew<PublicField<Status?>>(); + + result.Value.ShouldBe(Removed); + } } } diff --git a/AgileMapper.UnitTests/TestClasses/Status.cs b/AgileMapper.UnitTests/TestClasses/Status.cs index c59432398..069b778aa 100644 --- a/AgileMapper.UnitTests/TestClasses/Status.cs +++ b/AgileMapper.UnitTests/TestClasses/Status.cs @@ -8,7 +8,7 @@ public enum Status New = 1, Assigned = 2, InProgress = 4, - Complete = 8, + Completed = 8, Cancelled = 16, Removed = 32 } From fd6570095d45b6e50dc153f8b64a53cba796ff1e Mon Sep 17 00:00:00 2001 From: Steve Wilkes <steve@agileobjects.co.uk> Date: Thu, 11 Jan 2018 18:25:36 +0000 Subject: [PATCH 6/7] Support for mapping a numeric string value to a flags enum --- .../WhenConvertingToFlagsEnums.cs | 9 + AgileMapper/TypeConversion/ToEnumConverter.cs | 220 +++++++++++++----- 2 files changed, 177 insertions(+), 52 deletions(-) diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs index b62efa755..533e7e2d2 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs @@ -53,5 +53,14 @@ public void ShouldMapASingleValueLongToANullableFlagsEnum() result.Value.ShouldBe(Removed); } + + [Fact] + public void ShouldMapASingleValueNumericStringToAFlagsEnum() + { + var source = new PublicProperty<string> { Value = "4" }; + var result = Mapper.Map(source).ToANew<PublicField<Status>>(); + + result.Value.ShouldBe((Status)4); + } } } diff --git a/AgileMapper/TypeConversion/ToEnumConverter.cs b/AgileMapper/TypeConversion/ToEnumConverter.cs index 045d14acc..4b0c68c20 100644 --- a/AgileMapper/TypeConversion/ToEnumConverter.cs +++ b/AgileMapper/TypeConversion/ToEnumConverter.cs @@ -44,7 +44,6 @@ public override Expression GetConversion(Expression sourceValue, Type targetEnum nonNullableTargetEnumType); } - if (nonNullableSourceType.IsNumeric()) { return GetNumericToEnumConversion( @@ -122,27 +121,33 @@ private Expression GetTryParseConversion( sourceIsAnEnum = false; } + var tryParseCall = GetTryParseCall(sourceValue, nonNullableTargetEnumType, out var valueVariable); + var parseSuccessBranch = GetParseSuccessBranch(sourceIsAnEnum, valueVariable, fallbackValue); + + var parsedValueOrDefault = Expression.Condition(tryParseCall, parseSuccessBranch, fallbackValue); + var tryParseBlock = Expression.Block(new[] { valueVariable }, parsedValueOrDefault); + + return tryParseBlock; + } + + private static Expression GetTryParseCall( + Expression sourceValue, + Type nonNullableTargetEnumType, + out ParameterExpression valueVariable) + { var tryParseMethod = typeof(Enum) .GetPublicStaticMethod("TryParse", parameterCount: 3) .MakeGenericMethod(nonNullableTargetEnumType); - var valueVariable = Expression.Variable( + valueVariable = Expression.Variable( nonNullableTargetEnumType, nonNullableTargetEnumType.GetShortVariableName()); - var tryParseCall = Expression.Call( + return Expression.Call( tryParseMethod, sourceValue, true.ToConstantExpression(), // <- IgnoreCase valueVariable); - - - var parseSuccessBranch = GetParseSuccessBranch(sourceIsAnEnum, valueVariable, fallbackValue); - - var parsedValueOrDefault = Expression.Condition(tryParseCall, parseSuccessBranch, fallbackValue); - var tryParseBlock = Expression.Block(new[] { valueVariable }, parsedValueOrDefault); - - return tryParseBlock; } private static Expression GetParseSuccessBranch( @@ -173,56 +178,105 @@ private Expression GetFlagsEnumConversion( Type nonNullableSourceType, Type nonNullableTargetEnumType) { + var enumTypeName = nonNullableTargetEnumType.GetVariableNameInCamelCase(); + var underlyingEnumType = Enum.GetUnderlyingType(nonNullableTargetEnumType); + + var enumValueVariable = Expression.Variable(underlyingEnumType, enumTypeName + "Value"); + var assignEnumValue = Expression.Assign(enumValueVariable, underlyingEnumType.ToDefaultExpression()); + if (nonNullableSourceType.IsNumeric()) { return GetNumericToFlagsEnumConversion( sourceValue, fallbackValue, - nonNullableSourceType, - nonNullableTargetEnumType); + nonNullableTargetEnumType, + enumTypeName, + enumValueVariable, + assignEnumValue); } - throw new NotImplementedException(); - } + if (sourceValue.Type != typeof(string)) + { + sourceValue = _toStringConverter.GetConversion(sourceValue); + } - private static Expression GetNumericToFlagsEnumConversion( - Expression sourceValue, - Expression fallbackValue, - Type nonNullableSourceType, - Type nonNullableTargetEnumType) - { - var enumTypeName = nonNullableTargetEnumType.GetVariableNameInCamelCase(); - var underlyingEnumType = Enum.GetUnderlyingType(nonNullableTargetEnumType); + var sourceValuesVariable = GetEnumValuesVariable(enumTypeName); - var enumValueVariable = Expression.Variable(underlyingEnumType, enumTypeName + "Value"); - var assignEnumValue = Expression.Assign(enumValueVariable, underlyingEnumType.ToDefaultExpression()); + var splitSourceValueCall = Expression.Call( + sourceValue, + typeof(string).GetPublicInstanceMethod("Split", parameterCount: 1), + Expression.NewArrayInit(typeof(char), ','.ToConstantExpression())); - var enumValuesVariable = Expression.Variable(typeof(IEnumerator), enumTypeName + "Values"); + var assignSourceValues = GetValuesEnumeratorAssignment(sourceValuesVariable, splitSourceValueCall); - var enumGetValuesCall = Expression.Call( - typeof(Enum).GetPublicStaticMethod("GetValues"), - nonNullableTargetEnumType.ToConstantExpression()); + var ifNotMoveNextBreak = GetLoopExitCheck(sourceValuesVariable, out var loopBreakTarget); - var getValuesEnumeratorCall = Expression.Call( - enumGetValuesCall, - enumGetValuesCall.Type.GetPublicInstanceMethod("GetEnumerator")); + var localSourceValueVariable = Expression.Variable(typeof(string), enumTypeName); + var enumeratorCurrent = Expression.Property(sourceValuesVariable, "Current"); + var currentToString = Expression.Call(enumeratorCurrent, typeof(object).GetPublicInstanceMethod("ToString")); + var stringTrimMethod = typeof(string).GetPublicInstanceMethod("Trim", parameterCount: 0); + var currentTrimmed = Expression.Call(currentToString, stringTrimMethod); + var assignLocalVariable = Expression.Assign(localSourceValueVariable, currentTrimmed); - var assignEnumValues = Expression.Assign(enumValuesVariable, getValuesEnumeratorCall); + var sourceNumericValueVariableName = enumTypeName + underlyingEnumType.Name + "Value"; + var sourceNumericValueVariable = Expression.Parameter(underlyingEnumType, sourceNumericValueVariableName); - var enumeratorMoveNext = Expression.Call( - enumValuesVariable, - enumValuesVariable.Type.GetPublicInstanceMethod("MoveNext")); + var numericTryParseCall = Expression.Call( + underlyingEnumType.GetPublicStaticMethod("TryParse", parameterCount: 2), + localSourceValueVariable, + sourceNumericValueVariable); + + var numericValuePopulationLoop = GetNumericToFlagsEnumPopulationLoop( + nonNullableTargetEnumType, + enumTypeName, + enumValueVariable, + sourceNumericValueVariable, + out var enumValuesVariable, + out var assignEnumValues); + + var numericValuePopulationBlock = Expression.Block( + new[] { enumValuesVariable }, + assignEnumValues, + numericValuePopulationLoop); - var loopBreakTarget = Expression.Label(); + var stringHasValidValueCheck = GetTryParseCall( + localSourceValueVariable, + nonNullableTargetEnumType, + out var sourceEnumValueVariable); - var ifNotMoveNextBreak = Expression.IfThen( - Expression.Not(enumeratorMoveNext), - Expression.Break(loopBreakTarget)); + var convertedEnumValue = sourceEnumValueVariable.GetConversionTo(enumValueVariable.Type); + var assignParsedEnumValue = Expression.OrAssign(enumValueVariable, convertedEnumValue); - var localEnumValueVariable = Expression.Variable(underlyingEnumType, enumTypeName); - var enumeratorCurrent = Expression.Property(enumValuesVariable, "Current"); - var currentAsEnumType = Expression.Convert(enumeratorCurrent, underlyingEnumType); - var assignLocalVariable = Expression.Assign(localEnumValueVariable, currentAsEnumType); + var assignValidValuesIfPossible = Expression.IfThenElse( + numericTryParseCall, + numericValuePopulationBlock, + Expression.IfThen(stringHasValidValueCheck, assignParsedEnumValue)); + + var loopBody = Expression.Block( + new[] { localSourceValueVariable, sourceNumericValueVariable, sourceEnumValueVariable }, + ifNotMoveNextBreak, + assignLocalVariable, + assignValidValuesIfPossible); + + var populationBlock = Expression.Block( + new[] { sourceValuesVariable, enumValueVariable }, + assignEnumValue, + assignSourceValues, + Expression.Loop(loopBody, loopBreakTarget), + enumValueVariable.GetConversionTo(fallbackValue.Type)); + + return populationBlock; + } + + private static Expression GetNumericToFlagsEnumConversion( + Expression sourceValue, + Expression fallbackValue, + Type nonNullableTargetEnumType, + string enumTypeName, + ParameterExpression enumValueVariable, + Expression assignEnumValue) + { + var underlyingEnumType = enumValueVariable.Type; var sourceValueVariable = Expression.Variable(underlyingEnumType, enumTypeName + "Source"); @@ -233,6 +287,49 @@ private static Expression GetNumericToFlagsEnumConversion( var assignSourceVariable = Expression.Assign(sourceValueVariable, sourceValue); + var populationLoop = GetNumericToFlagsEnumPopulationLoop( + nonNullableTargetEnumType, + enumTypeName, + enumValueVariable, + sourceValueVariable, + out var enumValuesVariable, + out var assignEnumValues); + + var populationBlock = Expression.Block( + new[] { sourceValueVariable, enumValueVariable, enumValuesVariable }, + assignSourceVariable, + assignEnumValue, + assignEnumValues, + populationLoop, + enumValueVariable.GetConversionTo(fallbackValue.Type)); + + return populationBlock; + } + + private static Expression GetNumericToFlagsEnumPopulationLoop( + Type nonNullableTargetEnumType, + string enumTypeName, + Expression enumValueVariable, + Expression sourceValueVariable, + out ParameterExpression enumValuesVariable, + out Expression assignEnumValues) + { + var underlyingEnumType = enumValueVariable.Type; + enumValuesVariable = GetEnumValuesVariable(enumTypeName); + + var enumGetValuesCall = Expression.Call( + typeof(Enum).GetPublicStaticMethod("GetValues"), + nonNullableTargetEnumType.ToConstantExpression()); + + assignEnumValues = GetValuesEnumeratorAssignment(enumValuesVariable, enumGetValuesCall); + + var ifNotMoveNextBreak = GetLoopExitCheck(enumValuesVariable, out var loopBreakTarget); + + var localEnumValueVariable = Expression.Variable(underlyingEnumType, enumTypeName); + var enumeratorCurrent = Expression.Property(enumValuesVariable, "Current"); + var currentAsEnumType = Expression.Convert(enumeratorCurrent, underlyingEnumType); + var assignLocalVariable = Expression.Assign(localEnumValueVariable, currentAsEnumType); + var localVariableAndSourceValue = Expression.And(localEnumValueVariable, sourceValueVariable); var andResultEqualsEnumValue = Expression.Equal(localVariableAndSourceValue, localEnumValueVariable); @@ -246,15 +343,34 @@ private static Expression GetNumericToFlagsEnumConversion( assignLocalVariable, ifAndResultMatchesAssign); - var populationBlock = Expression.Block( - new[] { sourceValueVariable, enumValueVariable, enumValuesVariable }, - assignSourceVariable, - assignEnumValue, - assignEnumValues, - Expression.Loop(loopBody, loopBreakTarget), - enumValueVariable.GetConversionTo(fallbackValue.Type)); + return Expression.Loop(loopBody, loopBreakTarget); + } - return populationBlock; + private static ParameterExpression GetEnumValuesVariable(string enumTypeName) + => Expression.Variable(typeof(IEnumerator), enumTypeName + "Values"); + + private static Expression GetValuesEnumeratorAssignment( + Expression enumValuesVariable, + Expression enumeratedValues) + { + var getValuesEnumeratorCall = Expression.Call( + enumeratedValues, + enumeratedValues.Type.GetPublicInstanceMethod("GetEnumerator")); + + return enumValuesVariable.AssignTo(getValuesEnumeratorCall); + } + + private static Expression GetLoopExitCheck( + Expression valuesEnumerator, + out LabelTarget loopBreakTarget) + { + var enumeratorMoveNext = Expression.Call( + valuesEnumerator, + valuesEnumerator.Type.GetPublicInstanceMethod("MoveNext")); + + loopBreakTarget = Expression.Label(); + + return Expression.IfThen(Expression.Not(enumeratorMoveNext), Expression.Break(loopBreakTarget)); } } } \ No newline at end of file From 51afee8702988d4d35df1188bfa28705b71102e9 Mon Sep 17 00:00:00 2001 From: Steve Wilkes <steve@agileobjects.co.uk> Date: Thu, 11 Jan 2018 18:28:41 +0000 Subject: [PATCH 7/7] Expanding string -> flags enum test coverage --- .../WhenConvertingToFlagsEnums.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs index 533e7e2d2..65f2e7582 100644 --- a/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs +++ b/AgileMapper.UnitTests/SimpleTypeConversion/WhenConvertingToFlagsEnums.cs @@ -54,6 +54,15 @@ public void ShouldMapASingleValueLongToANullableFlagsEnum() result.Value.ShouldBe(Removed); } + [Fact] + public void ShouldMapAMultiValueNumericCharacterToAFlagsEnum() + { + var source = new PublicProperty<char> { Value = '9' }; + var result = Mapper.Map(source).ToANew<PublicField<Status>>(); + + result.Value.ShouldBe(New | Completed); + } + [Fact] public void ShouldMapASingleValueNumericStringToAFlagsEnum() { @@ -62,5 +71,14 @@ public void ShouldMapASingleValueNumericStringToAFlagsEnum() result.Value.ShouldBe((Status)4); } + + [Fact] + public void ShouldMapAMultiValueMixedStringToAFlagsEnum() + { + var source = new PublicProperty<string> { Value = "9, InProgress, 4" }; + var result = Mapper.Map(source).ToANew<PublicField<Status>>(); + + result.Value.ShouldBe(New | InProgress | Completed); + } } }