Skip to content

Commit b362fb5

Browse files
authored
Merge pull request #4 from BlaiseD/master
Fixes bug mapping expressions involving members of the generic type definition.
2 parents 61cd442 + ed802e0 commit b362fb5

File tree

9 files changed

+112
-14
lines changed

9 files changed

+112
-14
lines changed

AutoMapper.Extensions.ExpressionMapping.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26124.0
4+
VisualStudioVersion = 15.0.27130.2020
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.Extensions.ExpressionMapping", "src\AutoMapper.Extensions.ExpressionMapping\AutoMapper.Extensions.ExpressionMapping.csproj", "{24DF305C-EE59-460A-BA97-4B7CD5505434}"
77
EndProject

src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</PropertyGroup>
1818

1919
<ItemGroup>
20-
<PackageReference Include="AutoMapper" Version="7.0.0" />
20+
<PackageReference Include="AutoMapper" Version="7.0.1" />
2121
</ItemGroup>
2222

2323
</Project>

src/AutoMapper.Extensions.ExpressionMapping/ExpressionExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace AutoMapper
99
{
10+
using AutoMapper.Extensions.ExpressionMapping;
1011
using static Expression;
1112

1213
internal static class ExpressionExtensions
@@ -56,4 +57,23 @@ public static Expression NullCheck(this Expression expression, Type destinationT
5657
public static Expression IfNullElse(this Expression expression, Expression then, Expression @else = null)
5758
=> ExpressionFactory.IfNullElse(expression, then, @else);
5859
}
60+
61+
internal static class ExpressionHelpers
62+
{
63+
public static MemberExpression MemberAccesses(string members, Expression obj) =>
64+
(MemberExpression)GetMemberPath(obj.Type, members).MemberAccesses(obj);
65+
66+
private static IEnumerable<MemberInfo> GetMemberPath(Type type, string fullMemberName)
67+
{
68+
MemberInfo property = null;
69+
foreach (var memberName in fullMemberName.Split('.'))
70+
{
71+
var currentType = GetCurrentType(property, type);
72+
yield return property = currentType.GetFieldOrProperty(memberName);
73+
}
74+
}
75+
76+
private static Type GetCurrentType(MemberInfo member, Type type)
77+
=> member?.GetMemberType() ?? type;
78+
}
5979
}

src/AutoMapper.Extensions.ExpressionMapping/FindMemberExpressionsVisitor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public MemberExpression Result
3737
return result;
3838
});
3939

40-
return ExpressionFactory.MemberAccesses(member, _newParameter);
40+
return ExpressionHelpers.MemberAccesses(member, _newParameter);
4141
}
4242
}
4343

src/AutoMapper.Extensions.ExpressionMapping/MapIncludesVisitor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ protected override Expression VisitMember(MemberExpression node)
9696
return v.Result;
9797
}
9898
fullName = BuildFullName(propertyMapInfoList);
99-
var me = ExpressionFactory.MemberAccesses(fullName, InfoDictionary[parameterExpression].NewParameter);
99+
var me = ExpressionHelpers.MemberAccesses(fullName, InfoDictionary[parameterExpression].NewParameter);
100100
if (me.Expression.NodeType == ExpressionType.MemberAccess && (me.Type == typeof(string) || me.Type.GetTypeInfo().IsValueType || (me.Type.GetTypeInfo().IsGenericType
101101
&& me.Type.GetGenericTypeDefinition() == typeof(Nullable<>)
102102
&& Nullable.GetUnderlyingType(me.Type).GetTypeInfo().IsValueType)))

src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected override Expression VisitMember(MemberExpression node)
4040
? sourcePath
4141
: string.Concat(ParentFullName, ".", sourcePath);
4242

43-
var me = ExpressionFactory.MemberAccesses(fullName, NewParameter);
43+
var me = ExpressionHelpers.MemberAccesses(fullName, NewParameter);
4444

4545
return me;
4646
}

src/AutoMapper.Extensions.ExpressionMapping/TypeExtensions.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ internal static class TypeExtensions
2929
public static MethodInfo GetRemoveMethod(this EventInfo eventInfo) => eventInfo.RemoveMethod;
3030
#endif
3131

32-
public static Type CreateType(this TypeBuilder type)
33-
{
34-
return type.CreateTypeInfo().AsType();
35-
}
36-
3732
public static IEnumerable<MemberInfo> GetDeclaredMembers(this Type type) => type.GetTypeInfo().DeclaredMembers;
3833

3934
public static IEnumerable<Type> GetTypeInheritance(this Type type)

src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,21 @@ protected override Expression VisitMember(MemberExpression node)
9090
return ex;
9191
}
9292
fullName = BuildFullName(propertyMapInfoList);
93-
var me = ExpressionFactory.MemberAccesses(fullName, InfoDictionary[parameterExpression].NewParameter);
93+
var me = ExpressionHelpers.MemberAccesses(fullName, InfoDictionary[parameterExpression].NewParameter);
9494

9595
this.TypeMappings.AddTypeMapping(ConfigurationProvider, node.Type, me.Type);
9696
return me;
9797
}
9898

99+
protected override Expression VisitLambda<T>(Expression<T> node)
100+
{
101+
var ex = this.Visit(node.Body);
102+
103+
var mapped = Expression.Lambda(ex, node.GetDestinationParameterExpressions(this.InfoDictionary, this.TypeMappings));
104+
this.TypeMappings.AddTypeMapping(ConfigurationProvider, node.Type, mapped.Type);
105+
return mapped;
106+
}
107+
99108
protected override Expression VisitUnary(UnaryExpression node)
100109
{
101110
switch (node.NodeType)
@@ -109,6 +118,13 @@ protected override Expression VisitUnary(UnaryExpression node)
109118
default:
110119
return base.VisitUnary(node);
111120
}
121+
case ExpressionType.Lambda:
122+
var lambdaExpression = (LambdaExpression)node.Operand;
123+
var ex = this.Visit(lambdaExpression.Body);
124+
125+
var mapped = Expression.Quote(Expression.Lambda(ex, lambdaExpression.GetDestinationParameterExpressions(this.InfoDictionary, this.TypeMappings)));
126+
this.TypeMappings.AddTypeMapping(ConfigurationProvider, node.Type, mapped.Type);
127+
return mapped;
112128
default:
113129
return base.VisitUnary(node);
114130
}
@@ -137,7 +153,9 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
137153

138154
var listOfArgumentsForNewMethod = node.Arguments.Aggregate(new List<Expression>(), (lst, next) =>
139155
{
140-
var mappedNext = ArgumentMapper.Create(this, next).MappedArgumentExpression;
156+
//var mappedNext = ArgumentMapper.Create(this, next).MappedArgumentExpression;
157+
//Using VisitUnary and VisitLambda instead of ArgumentMappers
158+
var mappedNext = this.Visit(next);
141159
TypeMappings.AddTypeMapping(ConfigurationProvider, next.Type, mappedNext.Type);
142160

143161
lst.Add(mappedNext);
@@ -151,9 +169,14 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
151169

152170
ConvertTypesIfNecessary(node.Method.GetParameters(), listOfArgumentsForNewMethod, node.Method);
153171

172+
/*Using VisitUnary and VisitLambda instead of ArgumentMappers
173+
* return node.Method.IsStatic
174+
? GetStaticExpression()
175+
: GetInstanceExpression(ArgumentMapper.Create(this, node.Object).MappedArgumentExpression);*/
176+
154177
return node.Method.IsStatic
155178
? GetStaticExpression()
156-
: GetInstanceExpression(ArgumentMapper.Create(this, node.Object).MappedArgumentExpression);
179+
: GetInstanceExpression(this.Visit(node.Object));
157180

158181
MethodCallExpression GetInstanceExpression(Expression instance)
159182
=> node.Method.IsGenericMethod
@@ -225,15 +248,18 @@ private static void AddPropertyMapInfo(Type parentType, string name, List<Proper
225248
}
226249
}
227250

251+
private bool GenericTypeDefinitionsAreEquivalent(Type typeSource, Type typeDestination)
252+
=> typeSource.IsGenericType() && typeDestination.IsGenericType() && typeSource.GetGenericTypeDefinition() == typeDestination.GetGenericTypeDefinition();
253+
228254
protected void FindDestinationFullName(Type typeSource, Type typeDestination, string sourceFullName, List<PropertyMapInfo> propertyMapInfoList)
229255
{
230256
const string period = ".";
257+
231258
if (typeSource == typeDestination)
232259
{
233260
var sourceFullNameArray = sourceFullName.Split(new[] { period[0] }, StringSplitOptions.RemoveEmptyEntries);
234261
sourceFullNameArray.Aggregate(propertyMapInfoList, (list, next) =>
235262
{
236-
237263
if (list.Count == 0)
238264
{
239265
AddPropertyMapInfo(typeSource, next, list);
@@ -250,6 +276,36 @@ protected void FindDestinationFullName(Type typeSource, Type typeDestination, st
250276
return;
251277
}
252278

279+
if (GenericTypeDefinitionsAreEquivalent(typeSource, typeDestination))
280+
{
281+
if (sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) < 0)
282+
{
283+
//sourceFullName is a member of the generic type definition so just add the members PropertyMapInfo
284+
AddPropertyMapInfo(typeDestination, sourceFullName, propertyMapInfoList);
285+
var sourceType = typeSource.GetFieldOrProperty(sourceFullName).GetMemberType();
286+
var destType = typeDestination.GetFieldOrProperty(sourceFullName).GetMemberType();
287+
288+
TypeMappings.AddTypeMapping(ConfigurationProvider, sourceType, destType);
289+
290+
return;
291+
}
292+
else
293+
{
294+
//propertyName is a member of the generic type definition so just add the members PropertyMapInfo
295+
var propertyName = sourceFullName.Substring(0, sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase));
296+
AddPropertyMapInfo(typeDestination, propertyName, propertyMapInfoList);
297+
298+
var sourceType = typeSource.GetFieldOrProperty(propertyName).GetMemberType();
299+
var destType = typeDestination.GetFieldOrProperty(propertyName).GetMemberType();
300+
301+
TypeMappings.AddTypeMapping(ConfigurationProvider, sourceType, destType);
302+
303+
var childFullName = sourceFullName.Substring(sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) + 1);
304+
FindDestinationFullName(sourceType, destType, childFullName, propertyMapInfoList);
305+
return;
306+
}
307+
}
308+
253309
var typeMap = ConfigurationProvider.CheckIfMapExists(sourceType: typeDestination, destinationType: typeSource);//The destination becomes the source because to map a source expression to a destination expression,
254310
//we need the expressions used to create the source from the destination
255311

tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq.Expressions;
55
using Shouldly;
66
using Xunit;
7+
using AutoMapper;
78

89
namespace AutoMapper.Extensions.ExpressionMapping.UnitTests
910
{
@@ -411,6 +412,32 @@ public void Map_orderBy_thenBy_GroupBy_expression()
411412
Assert.True(users[0].Count() == 2);
412413
}
413414

415+
[Fact]
416+
public void Map_orderBy_thenBy_GroupBy_Select_expression()
417+
{
418+
//Arrange
419+
Expression<Func<IQueryable<UserModel>, IQueryable<object>>> grouped = q => q.OrderBy(s => s.Id).ThenBy(s => s.FullName).GroupBy(s => s.AgeInYears).Select(grp => new { Id = grp.Key, Count = grp.Count() });
420+
421+
//Act
422+
Expression<Func<IQueryable<User>, IQueryable<object>>> expMapped = mapper.MapExpression<Expression<Func<IQueryable<User>, IQueryable<object>>>>(grouped);
423+
List<dynamic> users = expMapped.Compile().Invoke(Users).ToList();
424+
425+
Assert.True(users[0].Count == 2);
426+
}
427+
428+
[Fact]
429+
public void Map_orderBy_thenBy_To_Dictionary_Select_expression()
430+
{
431+
//Arrange
432+
Expression<Func<IQueryable<UserModel>, IEnumerable<object>>> grouped = q => q.OrderBy(s => s.Id).ThenBy(s => s.FullName).ToDictionary(kvp => kvp.Id).Select(grp => new { Id = grp.Key, Name = grp.Value.FullName });
433+
434+
//Act
435+
Expression<Func<IQueryable<User>, IEnumerable<object>>> expMapped = mapper.MapExpression<Expression<Func<IQueryable<User>, IEnumerable<object>>>>(grouped);
436+
List<dynamic> users = expMapped.Compile().Invoke(Users).ToList();
437+
438+
Assert.True(users[0].Id == 11);
439+
}
440+
414441
[Fact]
415442
public void Map_dynamic_return_type()
416443
{

0 commit comments

Comments
 (0)