Skip to content

Commit 2bed007

Browse files
authored
Merge pull request #3411 from lbargaoanu/EFCore_constructors
EF Core; generalize constructor mapping for ProjectTo
2 parents 39f0549 + b250482 commit 2bed007

File tree

4 files changed

+80
-76
lines changed

4 files changed

+80
-76
lines changed

src/AutoMapper/ConstructorMap.cs

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Linq;
4-
using System.Linq.Expressions;
53
using System.Reflection;
6-
using AutoMapper.Execution;
7-
using AutoMapper.QueryableExtensions;
8-
using AutoMapper.QueryableExtensions.Impl;
94

105
namespace AutoMapper
116
{
12-
using static Expression;
13-
147
public class ConstructorMap
158
{
169
private readonly IList<ConstructorParameterMap> _ctorParams = new List<ConstructorParameterMap>();
@@ -25,34 +18,9 @@ public ConstructorMap(ConstructorInfo ctor, TypeMap typeMap)
2518
TypeMap = typeMap;
2619
}
2720

28-
private static readonly IExpressionResultConverter[] ExpressionResultConverters =
29-
{
30-
new MemberResolverExpressionResultConverter(),
31-
new MemberGetterExpressionResultConverter()
32-
};
33-
3421
public bool CanResolve => CtorParams.All(param => param.CanResolveValue);
3522

36-
public Expression NewExpression(Expression instanceParameter, LetPropertyMaps letPropertyMaps)
37-
{
38-
var parameters = CtorParams.Select(map =>
39-
{
40-
var result = new ExpressionResolutionResult(instanceParameter, Ctor.DeclaringType);
41-
42-
var matchingExpressionConverter =
43-
ExpressionResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, map));
44-
45-
result = matchingExpressionConverter?.GetExpressionResolutionResult(result, map, letPropertyMaps)
46-
?? throw new AutoMapperMappingException($"Unable to generate the instantiation expression for the constructor {Ctor}: no expression could be mapped for constructor parameter '{map.Parameter}'.", null, TypeMap.Types);
47-
48-
return result;
49-
});
50-
return New(Ctor, parameters.Select(p => p.ResolutionExpression));
51-
}
52-
53-
public void AddParameter(ParameterInfo parameter, MemberInfo[] resolvers, bool canResolve)
54-
{
23+
public void AddParameter(ParameterInfo parameter, MemberInfo[] resolvers, bool canResolve) =>
5524
_ctorParams.Add(new ConstructorParameterMap(TypeMap, parameter, resolvers, canResolve));
56-
}
5725
}
58-
}
26+
}

src/AutoMapper/QueryableExtensions/ExpressionBuilder.cs

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,16 @@ public Expression CreateMapExpression(ExpressionRequest request, Expression inst
127127

128128
private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps, out TypeMap typeMap)
129129
{
130-
typeMap = _configurationProvider.ResolveTypeMap(request.SourceType, request.DestinationType);
131-
132-
if(typeMap == null)
133-
{
134-
throw QueryMapperHelper.MissingMapException(request.SourceType, request.DestinationType);
135-
}
136-
137-
if(typeMap.CustomMapExpression != null)
138-
{
139-
return typeMap.CustomMapExpression.ReplaceParameters(instanceParameter);
140-
}
130+
typeMap = _configurationProvider.ResolveTypeMap(request.SourceType, request.DestinationType) ?? throw QueryMapperHelper.MissingMapException(request.SourceType, request.DestinationType);
141131
return CreateMapExpressionCore(request, instanceParameter, typePairCount, typeMap, letPropertyMaps);
142132
}
143133

144134
private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, TypeMap typeMap, LetPropertyMaps letPropertyMaps)
145135
{
136+
if (typeMap.CustomMapExpression != null)
137+
{
138+
return typeMap.CustomMapExpression.ReplaceParameters(instanceParameter);
139+
}
146140
var memberBindings = new List<MemberBinding>();
147141
var depth = GetDepth(request, typePairCount);
148142
if(typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth)
@@ -156,16 +150,9 @@ private Expression CreateMapExpressionCore(ExpressionRequest request, Expression
156150
{
157151
memberBindings = CreateMemberBindings();
158152
}
159-
Expression constructorExpression = DestinationConstructorExpression(typeMap, instanceParameter, letPropertyMaps);
160-
if(instanceParameter is ParameterExpression)
161-
constructorExpression = ((LambdaExpression)constructorExpression).ReplaceParameters(instanceParameter);
162-
var visitor = new NewFinderVisitor();
163-
visitor.Visit(constructorExpression);
164-
165-
var expression = MemberInit(
166-
visitor.NewExpression,
167-
memberBindings.ToArray()
168-
);
153+
var constructorExpressionLambda = DestinationConstructorExpression(request, instanceParameter, typePairCount, typeMap, letPropertyMaps);
154+
var constructorExpression = instanceParameter is ParameterExpression ? constructorExpressionLambda.ReplaceParameters(instanceParameter) : constructorExpressionLambda.Body;
155+
var expression = MemberInit((NewExpression)constructorExpression, memberBindings);
169156
return expression;
170157
List<MemberBinding> CreateMemberBindings()
171158
{
@@ -197,9 +184,8 @@ void CreateMemberBinding(PropertyExpression propertyExpression)
197184
var binder = _configurationProvider.Binders.FirstOrDefault(b => b.IsMatch(propertyMap, propertyTypeMap, result));
198185
if (binder == null)
199186
{
200-
var message =
201-
$"Unable to create a map expression from {propertyMap.SourceMember?.DeclaringType?.Name}.{propertyMap.SourceMember?.Name} ({result.Type}) to {propertyMap.DestinationMember.DeclaringType?.Name}.{propertyMap.DestinationName} ({propertyMap.DestinationType})";
202-
throw new AutoMapperMappingException(message, null, typeMap.Types, typeMap, propertyMap);
187+
ThrowCannotMap(propertyMap, result);
188+
return;
203189
}
204190
var bindExpression = binder.Build(_configurationProvider, propertyMap, propertyTypeMap, propertyRequest, result, typePairCount, letPropertyMaps);
205191
if (bindExpression == null)
@@ -219,6 +205,13 @@ void CreateMemberBinding(PropertyExpression propertyExpression)
219205
}
220206
}
221207

208+
private static void ThrowCannotMap(IMemberMap propertyMap, ExpressionResolutionResult result)
209+
{
210+
var message =
211+
$"Unable to create a map expression from {propertyMap.SourceMember?.DeclaringType?.Name}.{propertyMap.SourceMember?.Name} ({result.Type}) to {propertyMap.DestinationType.Name}.{propertyMap.DestinationName} ({propertyMap.DestinationType})";
212+
throw new AutoMapperMappingException(message, null, propertyMap.TypeMap.Types, propertyMap.TypeMap, propertyMap);
213+
}
214+
222215
private static int GetDepth(ExpressionRequest request, TypePairCount typePairCount)
223216
{
224217
if (typePairCount.TryGetValue(request, out int visitCount))
@@ -229,39 +222,51 @@ private static int GetDepth(ExpressionRequest request, TypePairCount typePairCou
229222
return visitCount;
230223
}
231224

232-
private LambdaExpression DestinationConstructorExpression(TypeMap typeMap, Expression instanceParameter, LetPropertyMaps letPropertyMaps)
225+
private LambdaExpression DestinationConstructorExpression(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, TypeMap typeMap, LetPropertyMaps letPropertyMaps)
233226
{
234227
var ctorExpr = typeMap.CustomCtorExpression;
235228
if (ctorExpr != null)
236229
{
237230
return ctorExpr;
238231
}
239232
var newExpression = typeMap.ConstructorMap?.CanResolve == true
240-
? typeMap.ConstructorMap.NewExpression(instanceParameter, letPropertyMaps)
233+
? NewExpression(typeMap.ConstructorMap, request, instanceParameter, typePairCount, letPropertyMaps)
241234
: New(typeMap.DestinationTypeToUse);
242-
243235
return Lambda(newExpression);
244236
}
245237

246-
private class NewFinderVisitor : ExpressionVisitor
247-
{
248-
public NewExpression NewExpression { get; private set; }
249238

250-
protected override Expression VisitNew(NewExpression node)
239+
public Expression NewExpression(ConstructorMap constructorMap, ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps)
240+
{
241+
var parameters = constructorMap.CtorParams.Select(map =>
251242
{
252-
NewExpression = node;
253-
return base.VisitNew(node);
254-
}
243+
var resolvedSource = ResolveExpression(map, request.SourceType, instanceParameter, letPropertyMaps);
244+
var types = new TypePair(resolvedSource.Type, map.DestinationType);
245+
var typeMap = _configurationProvider.ResolveTypeMap(types);
246+
if (typeMap == null)
247+
{
248+
if (types.DestinationType.IsAssignableFrom(types.SourceType))
249+
{
250+
return resolvedSource.ResolutionExpression;
251+
}
252+
ThrowCannotMap(map, resolvedSource);
253+
}
254+
var newRequest = new ExpressionRequest(types.SourceType, types.DestinationType, request.MembersToExpand, request);
255+
return CreateMapExpressionCore(newRequest, resolvedSource.ResolutionExpression, typePairCount, typeMap, letPropertyMaps);
256+
});
257+
return New(constructorMap.Ctor, parameters);
255258
}
256259

257-
private ExpressionResolutionResult ResolveExpression(PropertyMap propertyMap, Type currentType, Expression instanceParameter, LetPropertyMaps letPropertyMaps)
260+
private ExpressionResolutionResult ResolveExpression(IMemberMap propertyMap, Type currentType, Expression instanceParameter, LetPropertyMaps letPropertyMaps)
258261
{
259262
var result = new ExpressionResolutionResult(instanceParameter, currentType);
260-
261263
var matchingExpressionConverter = _configurationProvider.ResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, propertyMap));
262-
result = matchingExpressionConverter?.GetExpressionResolutionResult(result, propertyMap, letPropertyMaps)
263-
?? throw new Exception("Can't resolve this to Queryable Expression");
264-
264+
if (matchingExpressionConverter == null)
265+
{
266+
ThrowCannotMap(propertyMap, result);
267+
return null;
268+
}
269+
result = matchingExpressionConverter.GetExpressionResolutionResult(result, propertyMap, letPropertyMaps);
265270
if(propertyMap.NullSubstitute != null && result.Type.IsNullableType())
266271
{
267272
var currentChild = result.ResolutionExpression;

src/UnitTests/Projection/ConstructorTests.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
namespace AutoMapper.UnitTests.Projection
22
{
33
using System.Linq;
4-
using QueryableExtensions;
4+
using AutoMapper;
5+
using AutoMapper.QueryableExtensions;
56
using Shouldly;
67
using Xunit;
78

@@ -55,4 +56,34 @@ public void Should_construct_correctly()
5556
_dest[0].Other.ShouldBe(15);
5657
}
5758
}
59+
public class NestedConstructors : AutoMapperSpecBase
60+
{
61+
public class A
62+
{
63+
public int Id { get; set; }
64+
public B B { get; set; }
65+
}
66+
public class B
67+
{
68+
public int Id { get; set; }
69+
}
70+
public class DtoA
71+
{
72+
public DtoB B { get; }
73+
public DtoA(DtoB b) => B = b;
74+
}
75+
public class DtoB
76+
{
77+
public int Id { get; }
78+
public DtoB(int id) => Id = id;
79+
}
80+
protected override MapperConfiguration Configuration => new MapperConfiguration(cfg =>
81+
{
82+
cfg.CreateMap<A, DtoA>();
83+
cfg.CreateMap<B, DtoB>();
84+
});
85+
[Fact]
86+
public void Should_project_ok() =>
87+
ProjectTo<DtoA>(new[] { new A { B = new B { Id = 3 } } }.AsQueryable()).FirstOrDefault().B.Id.ShouldBe(3);
88+
}
5889
}

src/UnitTests/Projection/MoreExplanatoryExceptionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public void ConstructorWithUnknownParameterTypeThrowsExplicitException()
2222
new EntitySource[0].AsQueryable().ProjectTo<EntityDestination>(config));
2323

2424
// Assert
25-
Assert.Contains("object notSupported", exception.Message, StringComparison.OrdinalIgnoreCase);
25+
Assert.Contains("parameter notSupported", exception.Message, StringComparison.OrdinalIgnoreCase);
2626
}
2727

2828
class EntitySource

0 commit comments

Comments
 (0)