Skip to content

Commit 6572dd4

Browse files
committed
Using Type's to-string operators to convert values to strings where available, fixes issue #51 / Simplifying type converter hierarchy
1 parent 29db660 commit 6572dd4

File tree

7 files changed

+143
-140
lines changed

7 files changed

+143
-140
lines changed

AgileMapper.UnitTests/Dictionaries/WhenMappingFromDictionariesToNewEnumerableMembers.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,32 +118,45 @@ public void ShouldMapToAComplexTypeArrayFromUntypedDottedEntries()
118118
#if !NET_STANDARD
119119

120120
// See https://github.com/agileobjects/AgileMapper/issues/50
121+
// See https://github.com/agileobjects/AgileMapper/issues/51
121122
[Fact]
122123
public void ShouldMapStringValuesToAStringArray()
123124
{
124125
var source = new Dictionary<string, StringValues>
125126
{
126-
["WidgetId"] = new StringValues("123"),
127-
["ClientId"] = new StringValues("456"),
128-
["TestPayload"] = new StringValues(new[] { "a", "b", "c" })
127+
["StringValue"] = new StringValues("123"),
128+
["StringArray"] = new StringValues(new[] { "a", "b", "c" }),
129+
["IntValue"] = new StringValues("456"),
130+
["IntArray"] = new StringValues(new[] { "5", "4", "3" }),
131+
["DoubleValue"] = new StringValues("35.4354"),
132+
["DoubleArray"] = new StringValues(new[] { "1.23", "2.23", "3.23", "4.23" })
129133
};
130134

131135
var result = Mapper.Map(source).ToANew<StringValuesTestDto>();
132136

133-
result.WidgetId.ShouldBe("123");
134-
result.ClientId.ShouldBe("456");
135-
result.TestPayload.ShouldBe("a", "b", "c");
137+
result.StringValue.ShouldBe("123");
138+
result.StringArray.ShouldBe("a", "b", "c");
139+
result.IntValue.ShouldBe(456);
140+
result.IntArray.ShouldBe(5, 4, 3);
141+
result.DoubleValue.ShouldBe(35.4354);
142+
result.DoubleArray.ShouldBe(1.23, 2.23, 3.23, 4.23);
136143
}
137144

138145
#region Helper Class
139146

140147
public class StringValuesTestDto
141148
{
142-
public string WidgetId { get; set; }
149+
public string StringValue { get; set; }
143150

144-
public string ClientId { get; set; }
151+
public string[] StringArray { get; set; }
145152

146-
public string[] TestPayload { get; set; }
153+
public int IntValue { get; set; }
154+
155+
public int[] IntArray { get; set; }
156+
157+
public double DoubleValue { get; set; }
158+
159+
public double[] DoubleArray { get; set; }
147160
}
148161

149162
#endregion

AgileMapper/TypeConversion/ConverterSet.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public ConverterSet()
2323
new ToNumericConverter<int>(toStringConverter),
2424
new ToBoolConverter(toStringConverter),
2525
new ToEnumConverter(toStringConverter),
26-
new DefaultTryParseConverter<DateTime>(toStringConverter),
27-
new DefaultTryParseConverter<Guid>(toStringConverter),
26+
new TryParseConverter<DateTime>(toStringConverter),
27+
new TryParseConverter<Guid>(toStringConverter),
2828
new ToNumericConverter<decimal>(toStringConverter),
2929
new ToNumericConverter<double>(toStringConverter),
3030
new ToNumericConverter<long>(toStringConverter),

AgileMapper/TypeConversion/DefaultTryParseConverter.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.

AgileMapper/TypeConversion/ToNumericConverter.cs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,105 @@
22
{
33
using System;
44
using System.Linq;
5+
using System.Linq.Expressions;
56
using Extensions.Internal;
7+
using NetStandardPolyfills;
68

7-
internal class ToNumericConverter<TNumeric> : ToNumericConverterBase
9+
internal class ToNumericConverter<TNumeric> : TryParseConverter<TNumeric>
810
{
911
private static readonly Type[] _coercibleNumericTypes =
1012
typeof(TNumeric)
1113
.GetCoercibleNumericTypes()
1214
.ToArray();
1315

1416
public ToNumericConverter(ToStringConverter toStringConverter)
15-
: base(toStringConverter, typeof(TNumeric))
17+
: base(toStringConverter)
1618
{
1719
}
1820

19-
protected override bool IsCoercible(Type sourceType) => _coercibleNumericTypes.Contains(sourceType);
21+
protected override bool CanConvert(Type nonNullableSourceType)
22+
{
23+
return base.CanConvert(nonNullableSourceType) ||
24+
nonNullableSourceType.IsEnum() ||
25+
(nonNullableSourceType == typeof(char)) ||
26+
Constants.NumericTypes.Contains(nonNullableSourceType);
27+
}
28+
29+
public override Expression GetConversion(Expression sourceValue, Type targetType)
30+
{
31+
var sourceType = GetNonEnumSourceType(sourceValue);
32+
33+
if (IsCoercible(sourceType))
34+
{
35+
if (!targetType.IsWholeNumberNumeric())
36+
{
37+
sourceValue = sourceValue.GetConversionTo(sourceType);
38+
}
39+
40+
return sourceValue.GetConversionTo(targetType);
41+
}
42+
43+
return IsNumericType(sourceType)
44+
? GetCheckedNumericConversion(sourceValue, targetType)
45+
: base.GetConversion(sourceValue, targetType);
46+
}
47+
48+
private static Type GetNonEnumSourceType(Expression sourceValue)
49+
=> sourceValue.Type.IsEnum() ? Enum.GetUnderlyingType(sourceValue.Type) : sourceValue.Type;
50+
51+
private static bool IsNumericType(Type type)
52+
{
53+
if ((type == typeof(string)) ||
54+
(type == typeof(object)) ||
55+
(type == typeof(char)) ||
56+
(type == typeof(char?)))
57+
{
58+
return false;
59+
}
60+
61+
return Constants.NumericTypes.Contains(type);
62+
}
63+
64+
private static bool IsCoercible(Type sourceType) => _coercibleNumericTypes.Contains(sourceType);
65+
66+
private static Expression GetCheckedNumericConversion(Expression sourceValue, Type targetType)
67+
{
68+
var castSourceValue = sourceValue.GetConversionTo(targetType);
69+
70+
if (sourceValue.Type.GetNonNullableType() == targetType.GetNonNullableType())
71+
{
72+
return castSourceValue;
73+
}
74+
75+
var numericValueIsValid = GetNumericValueValidityCheck(sourceValue, targetType);
76+
var defaultTargetType = targetType.ToDefaultExpression();
77+
var inRangeValueOrDefault = Expression.Condition(numericValueIsValid, castSourceValue, defaultTargetType);
78+
79+
return inRangeValueOrDefault;
80+
}
81+
82+
private static Expression GetNumericValueValidityCheck(Expression sourceValue, Type targetType)
83+
{
84+
var nonNullableTargetType = targetType.GetNonNullableType();
85+
var numericValueIsInRange = NumericValueIsInRangeComparison.For(sourceValue, nonNullableTargetType);
86+
87+
if (NonWholeNumberCheckIsNotRequired(sourceValue, nonNullableTargetType))
88+
{
89+
return numericValueIsInRange;
90+
}
91+
92+
var moduloOneEqualsZero = NumericConversions.GetModuloOneIsZeroCheck(sourceValue);
93+
94+
return Expression.AndAlso(numericValueIsInRange, moduloOneEqualsZero);
95+
}
96+
97+
private static bool NonWholeNumberCheckIsNotRequired(Expression sourceValue, Type nonNullableTargetType)
98+
{
99+
var sourceType = sourceValue.Type.GetNonNullableType();
100+
101+
return sourceType.IsEnum() ||
102+
sourceType.IsWholeNumberNumeric() ||
103+
!nonNullableTargetType.IsWholeNumberNumeric();
104+
}
20105
}
21106
}

AgileMapper/TypeConversion/ToNumericConverterBase.cs

Lines changed: 0 additions & 101 deletions
This file was deleted.

AgileMapper/TypeConversion/ToStringConverter.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public Expression GetConversion(Expression sourceValue)
3232
return GetDateTimeToStringConversion(sourceValue, nonNullableSourceType);
3333
}
3434

35+
if (HasToStringOperator(nonNullableSourceType, out var operatorMethod))
36+
{
37+
return Expression.Call(operatorMethod, sourceValue);
38+
}
39+
3540
var toStringMethod = sourceValue.Type
3641
.GetPublicInstanceMethod("ToString", parameterCount: 0);
3742

@@ -40,6 +45,19 @@ public Expression GetConversion(Expression sourceValue)
4045
return toStringCall;
4146
}
4247

48+
public bool HasToStringOperator(Type nonNullableSourceType, out MethodInfo operatorMethod)
49+
{
50+
operatorMethod = nonNullableSourceType
51+
.GetPublicStaticMembers()
52+
.Where(m => (m.Name == "op_Implicit") || (m.Name == "op_Explicit"))
53+
.Cast<MethodInfo>()
54+
.FirstOrDefault(m =>
55+
(m.ReturnType == typeof(string)) &&
56+
(m.GetParameters()[0].ParameterType == nonNullableSourceType));
57+
58+
return operatorMethod != null;
59+
}
60+
4361
#region Byte[] Conversion
4462

4563
private static readonly MethodInfo _toBase64String = typeof(Convert)

AgileMapper/TypeConversion/TryParseConverterBase.cs renamed to AgileMapper/TypeConversion/TryParseConverter.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,38 @@ namespace AgileObjects.AgileMapper.TypeConversion
66
using Extensions.Internal;
77
using NetStandardPolyfills;
88

9-
internal abstract class TryParseConverterBase : ValueConverterBase
9+
internal class TryParseConverter<T> : ValueConverterBase
1010
{
1111
private readonly ToStringConverter _toStringConverter;
1212
private readonly Type _nonNullableTargetType;
1313
private readonly Type _nullableTargetType;
1414
private readonly MethodInfo _tryParseMethod;
1515
private readonly ParameterExpression _valueVariable;
1616

17-
protected TryParseConverterBase(
18-
ToStringConverter toStringConverter,
19-
Type nonNullableTargetType)
17+
public TryParseConverter(ToStringConverter toStringConverter)
2018
{
2119
_toStringConverter = toStringConverter;
22-
_nonNullableTargetType = nonNullableTargetType;
23-
_nullableTargetType = typeof(Nullable<>).MakeGenericType(nonNullableTargetType);
20+
_nonNullableTargetType = typeof(T);
21+
_nullableTargetType = typeof(Nullable<>).MakeGenericType(_nonNullableTargetType);
2422

25-
_tryParseMethod = nonNullableTargetType
23+
_tryParseMethod = _nonNullableTargetType
2624
.GetPublicStaticMethod("TryParse", parameterCount: 2);
2725

2826
_valueVariable = Expression.Variable(
29-
nonNullableTargetType,
30-
nonNullableTargetType.GetVariableNameInCamelCase() + "Value");
27+
_nonNullableTargetType,
28+
_nonNullableTargetType.GetVariableNameInCamelCase() + "Value");
3129
}
3230

3331
public override bool CanConvert(Type nonNullableSourceType, Type nonNullableTargetType)
3432
=> nonNullableTargetType == _nonNullableTargetType && CanConvert(nonNullableSourceType);
3533

3634
protected virtual bool CanConvert(Type nonNullableSourceType)
37-
=> (nonNullableSourceType == _nonNullableTargetType) || (nonNullableSourceType == typeof(object));
35+
{
36+
return (nonNullableSourceType == _nonNullableTargetType) ||
37+
(nonNullableSourceType == typeof(string)) ||
38+
(nonNullableSourceType == typeof(object)) ||
39+
_toStringConverter.HasToStringOperator(nonNullableSourceType, out var _);
40+
}
3841

3942
public override Expression GetConversion(Expression sourceValue, Type targetType)
4043
{

0 commit comments

Comments
 (0)