Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 3 additions & 35 deletions src/AutoMapper/ConstructorMap.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Execution;
using AutoMapper.QueryableExtensions;
using AutoMapper.QueryableExtensions.Impl;

namespace AutoMapper
{
using static Expression;

public class ConstructorMap
{
private readonly IList<ConstructorParameterMap> _ctorParams = new List<ConstructorParameterMap>();
Expand All @@ -25,34 +18,9 @@ public ConstructorMap(ConstructorInfo ctor, TypeMap typeMap)
TypeMap = typeMap;
}

private static readonly IExpressionResultConverter[] ExpressionResultConverters =
{
new MemberResolverExpressionResultConverter(),
new MemberGetterExpressionResultConverter()
};

public bool CanResolve => CtorParams.All(param => param.CanResolveValue);

public Expression NewExpression(Expression instanceParameter, LetPropertyMaps letPropertyMaps)
{
var parameters = CtorParams.Select(map =>
{
var result = new ExpressionResolutionResult(instanceParameter, Ctor.DeclaringType);

var matchingExpressionConverter =
ExpressionResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, map));

result = matchingExpressionConverter?.GetExpressionResolutionResult(result, map, letPropertyMaps)
?? 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);

return result;
});
return New(Ctor, parameters.Select(p => p.ResolutionExpression));
}

public void AddParameter(ParameterInfo parameter, MemberInfo[] resolvers, bool canResolve)
{
public void AddParameter(ParameterInfo parameter, MemberInfo[] resolvers, bool canResolve) =>
_ctorParams.Add(new ConstructorParameterMap(TypeMap, parameter, resolvers, canResolve));
}
}
}
}
83 changes: 44 additions & 39 deletions src/AutoMapper/QueryableExtensions/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,16 @@ public Expression CreateMapExpression(ExpressionRequest request, Expression inst

private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps, out TypeMap typeMap)
{
typeMap = _configurationProvider.ResolveTypeMap(request.SourceType, request.DestinationType);

if(typeMap == null)
{
throw QueryMapperHelper.MissingMapException(request.SourceType, request.DestinationType);
}

if(typeMap.CustomMapExpression != null)
{
return typeMap.CustomMapExpression.ReplaceParameters(instanceParameter);
}
typeMap = _configurationProvider.ResolveTypeMap(request.SourceType, request.DestinationType) ?? throw QueryMapperHelper.MissingMapException(request.SourceType, request.DestinationType);
return CreateMapExpressionCore(request, instanceParameter, typePairCount, typeMap, letPropertyMaps);
}

private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, TypeMap typeMap, LetPropertyMaps letPropertyMaps)
{
if (typeMap.CustomMapExpression != null)
{
return typeMap.CustomMapExpression.ReplaceParameters(instanceParameter);
}
var memberBindings = new List<MemberBinding>();
var depth = GetDepth(request, typePairCount);
if(typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth)
Expand All @@ -156,16 +150,9 @@ private Expression CreateMapExpressionCore(ExpressionRequest request, Expression
{
memberBindings = CreateMemberBindings();
}
Expression constructorExpression = DestinationConstructorExpression(typeMap, instanceParameter, letPropertyMaps);
if(instanceParameter is ParameterExpression)
constructorExpression = ((LambdaExpression)constructorExpression).ReplaceParameters(instanceParameter);
var visitor = new NewFinderVisitor();
visitor.Visit(constructorExpression);

var expression = MemberInit(
visitor.NewExpression,
memberBindings.ToArray()
);
var constructorExpressionLambda = DestinationConstructorExpression(request, instanceParameter, typePairCount, typeMap, letPropertyMaps);
var constructorExpression = instanceParameter is ParameterExpression ? constructorExpressionLambda.ReplaceParameters(instanceParameter) : constructorExpressionLambda.Body;
var expression = MemberInit((NewExpression)constructorExpression, memberBindings);
return expression;
List<MemberBinding> CreateMemberBindings()
{
Expand Down Expand Up @@ -197,9 +184,8 @@ void CreateMemberBinding(PropertyExpression propertyExpression)
var binder = _configurationProvider.Binders.FirstOrDefault(b => b.IsMatch(propertyMap, propertyTypeMap, result));
if (binder == null)
{
var message =
$"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})";
throw new AutoMapperMappingException(message, null, typeMap.Types, typeMap, propertyMap);
ThrowCannotMap(propertyMap, result);
return;
}
var bindExpression = binder.Build(_configurationProvider, propertyMap, propertyTypeMap, propertyRequest, result, typePairCount, letPropertyMaps);
if (bindExpression == null)
Expand All @@ -219,6 +205,13 @@ void CreateMemberBinding(PropertyExpression propertyExpression)
}
}

private static void ThrowCannotMap(IMemberMap propertyMap, ExpressionResolutionResult result)
{
var message =
$"Unable to create a map expression from {propertyMap.SourceMember?.DeclaringType?.Name}.{propertyMap.SourceMember?.Name} ({result.Type}) to {propertyMap.DestinationType.Name}.{propertyMap.DestinationName} ({propertyMap.DestinationType})";
throw new AutoMapperMappingException(message, null, propertyMap.TypeMap.Types, propertyMap.TypeMap, propertyMap);
}

private static int GetDepth(ExpressionRequest request, TypePairCount typePairCount)
{
if (typePairCount.TryGetValue(request, out int visitCount))
Expand All @@ -229,39 +222,51 @@ private static int GetDepth(ExpressionRequest request, TypePairCount typePairCou
return visitCount;
}

private LambdaExpression DestinationConstructorExpression(TypeMap typeMap, Expression instanceParameter, LetPropertyMaps letPropertyMaps)
private LambdaExpression DestinationConstructorExpression(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, TypeMap typeMap, LetPropertyMaps letPropertyMaps)
{
var ctorExpr = typeMap.CustomCtorExpression;
if (ctorExpr != null)
{
return ctorExpr;
}
var newExpression = typeMap.ConstructorMap?.CanResolve == true
? typeMap.ConstructorMap.NewExpression(instanceParameter, letPropertyMaps)
? NewExpression(typeMap.ConstructorMap, request, instanceParameter, typePairCount, letPropertyMaps)
: New(typeMap.DestinationTypeToUse);

return Lambda(newExpression);
}

private class NewFinderVisitor : ExpressionVisitor
{
public NewExpression NewExpression { get; private set; }

protected override Expression VisitNew(NewExpression node)
public Expression NewExpression(ConstructorMap constructorMap, ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps)
{
var parameters = constructorMap.CtorParams.Select(map =>
{
NewExpression = node;
return base.VisitNew(node);
}
var resolvedSource = ResolveExpression(map, request.SourceType, instanceParameter, letPropertyMaps);
var types = new TypePair(resolvedSource.Type, map.DestinationType);
var typeMap = _configurationProvider.ResolveTypeMap(types);
if (typeMap == null)
{
if (types.DestinationType.IsAssignableFrom(types.SourceType))
{
return resolvedSource.ResolutionExpression;
}
ThrowCannotMap(map, resolvedSource);
}
var newRequest = new ExpressionRequest(types.SourceType, types.DestinationType, request.MembersToExpand, request);
return CreateMapExpressionCore(newRequest, resolvedSource.ResolutionExpression, typePairCount, typeMap, letPropertyMaps);
});
return New(constructorMap.Ctor, parameters);
}

private ExpressionResolutionResult ResolveExpression(PropertyMap propertyMap, Type currentType, Expression instanceParameter, LetPropertyMaps letPropertyMaps)
private ExpressionResolutionResult ResolveExpression(IMemberMap propertyMap, Type currentType, Expression instanceParameter, LetPropertyMaps letPropertyMaps)
{
var result = new ExpressionResolutionResult(instanceParameter, currentType);

var matchingExpressionConverter = _configurationProvider.ResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, propertyMap));
result = matchingExpressionConverter?.GetExpressionResolutionResult(result, propertyMap, letPropertyMaps)
?? throw new Exception("Can't resolve this to Queryable Expression");

if (matchingExpressionConverter == null)
{
ThrowCannotMap(propertyMap, result);
return null;
}
result = matchingExpressionConverter.GetExpressionResolutionResult(result, propertyMap, letPropertyMaps);
if(propertyMap.NullSubstitute != null && result.Type.IsNullableType())
{
var currentChild = result.ResolutionExpression;
Expand Down
33 changes: 32 additions & 1 deletion src/UnitTests/Projection/ConstructorTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace AutoMapper.UnitTests.Projection
{
using System.Linq;
using QueryableExtensions;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Shouldly;
using Xunit;

Expand Down Expand Up @@ -55,4 +56,34 @@ public void Should_construct_correctly()
_dest[0].Other.ShouldBe(15);
}
}
public class NestedConstructors : AutoMapperSpecBase
{
public class A
{
public int Id { get; set; }
public B B { get; set; }
}
public class B
{
public int Id { get; set; }
}
public class DtoA
{
public DtoB B { get; }
public DtoA(DtoB b) => B = b;
}
public class DtoB
{
public int Id { get; }
public DtoB(int id) => Id = id;
}
protected override MapperConfiguration Configuration => new MapperConfiguration(cfg =>
{
cfg.CreateMap<A, DtoA>();
cfg.CreateMap<B, DtoB>();
});
[Fact]
public void Should_project_ok() =>
ProjectTo<DtoA>(new[] { new A { B = new B { Id = 3 } } }.AsQueryable()).FirstOrDefault().B.Id.ShouldBe(3);
}
}
2 changes: 1 addition & 1 deletion src/UnitTests/Projection/MoreExplanatoryExceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void ConstructorWithUnknownParameterTypeThrowsExplicitException()
new EntitySource[0].AsQueryable().ProjectTo<EntityDestination>(config));

// Assert
Assert.Contains("object notSupported", exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("parameter notSupported", exception.Message, StringComparison.OrdinalIgnoreCase);
}

class EntitySource
Expand Down