tools/AutoMapper/QueryableExtensions/ExpressionBuilder.cs (461 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using AutoMapper.Configuration;
using AutoMapper.Internal;
using AutoMapper.Execution;
using AutoMapper.QueryableExtensions.Impl;
using static System.Linq.Expressions.Expression;
namespace AutoMapper.QueryableExtensions
{
using static ExpressionFactory;
using TypePairCount = IDictionary<ExpressionRequest, int>;
using ParameterBag = IDictionary<string, object>;
public interface IExpressionBuilder
{
LambdaExpression[] GetMapExpression(Type sourceType, Type destinationType, ParameterBag parameters, MemberInfo[] membersToExpand);
LambdaExpression[] CreateMapExpression(ExpressionRequest request, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps);
Expression CreateMapExpression(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps);
}
public class ExpressionBuilder : IExpressionBuilder
{
private static readonly IExpressionResultConverter[] ExpressionResultConverters =
{
new MemberResolverExpressionResultConverter(),
new MemberGetterExpressionResultConverter()
};
private static readonly IExpressionBinder[] Binders =
{
new CustomProjectionExpressionBinder(),
new NullableDestinationExpressionBinder(),
new NullableSourceExpressionBinder(),
new AssignableExpressionBinder(),
new EnumerableExpressionBinder(),
new MappedTypeExpressionBinder(),
new StringExpressionBinder()
};
private readonly LockingConcurrentDictionary<ExpressionRequest, LambdaExpression[]> _expressionCache;
private readonly IConfigurationProvider _configurationProvider;
public ExpressionBuilder(IConfigurationProvider configurationProvider)
{
_configurationProvider = configurationProvider;
_expressionCache = new LockingConcurrentDictionary<ExpressionRequest, LambdaExpression[]>(CreateMapExpression);
}
public LambdaExpression[] GetMapExpression(Type sourceType, Type destinationType, ParameterBag parameters,
MemberInfo[] membersToExpand)
{
if (sourceType == null)
{
throw new ArgumentNullException(nameof(sourceType));
}
if (destinationType == null)
{
throw new ArgumentNullException(nameof(destinationType));
}
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
if (membersToExpand == null)
{
throw new ArgumentNullException(nameof(membersToExpand));
}
var cachedExpressions = _expressionCache.GetOrAdd(new ExpressionRequest(sourceType, destinationType, membersToExpand, null));
return cachedExpressions.Select(e => Prepare(e, parameters)).Cast<LambdaExpression>().ToArray();
}
private Expression Prepare(Expression cachedExpression, ParameterBag parameters)
{
Expression result;
if(parameters.Any())
{
var visitor = new ConstantExpressionReplacementVisitor(parameters);
result = visitor.Visit(cachedExpression);
}
else
{
result = cachedExpression;
}
// perform null-propagation if this feature is enabled.
if(_configurationProvider.EnableNullPropagationForQueryMapping)
{
var nullVisitor = new NullsafeQueryRewriter();
return nullVisitor.Visit(result);
}
return result;
}
private LambdaExpression[] CreateMapExpression(ExpressionRequest request) => CreateMapExpression(request, new Dictionary<ExpressionRequest, int>(), new FirstPassLetPropertyMaps(_configurationProvider));
public LambdaExpression[] CreateMapExpression(ExpressionRequest request, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps)
{
// this is the input parameter of this expression with name <variableName>
var instanceParameter = Parameter(request.SourceType, "dto");
var expressions = new QueryExpressions(CreateMapExpressionCore(request, instanceParameter, typePairCount, letPropertyMaps, out var typeMap));
if(letPropertyMaps.Count > 0)
{
expressions = letPropertyMaps.GetSubQueryExpression(this, expressions.First, typeMap, request, instanceParameter, typePairCount);
}
if(expressions.First == null)
{
return null;
}
var firstLambda = Lambda(expressions.First, instanceParameter);
if(expressions.Second == null)
{
return new[] { firstLambda };
}
return new[] { firstLambda, Lambda(expressions.Second, expressions.SecondParameter) };
}
public Expression CreateMapExpression(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps)
{
return CreateMapExpressionCore(request, instanceParameter, typePairCount, letPropertyMaps, out var _);
}
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.CustomProjection != null)
{
return typeMap.CustomProjection.ReplaceParameters(instanceParameter);
}
return CreateMapExpressionCore(request, instanceParameter, typePairCount, typeMap, letPropertyMaps);
}
private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, TypeMap typeMap, LetPropertyMaps letPropertyMaps)
{
var bindings = new List<MemberBinding>();
var depth = GetDepth(request, typePairCount);
if(typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth)
{
if(typeMap.Profile.AllowNullDestinationValues)
{
return null;
}
}
else
{
bindings = CreateMemberBindings(request, typeMap, instanceParameter, typePairCount, letPropertyMaps);
}
Expression constructorExpression = DestinationConstructorExpression(typeMap, instanceParameter);
if(instanceParameter is ParameterExpression)
constructorExpression = ((LambdaExpression)constructorExpression).ReplaceParameters(instanceParameter);
var visitor = new NewFinderVisitor();
visitor.Visit(constructorExpression);
var expression = MemberInit(
visitor.NewExpression,
bindings.ToArray()
);
return expression;
}
private static int GetDepth(ExpressionRequest request, TypePairCount typePairCount)
{
if (typePairCount.TryGetValue(request, out int visitCount))
{
visitCount = visitCount + 1;
}
typePairCount[request] = visitCount;
return visitCount;
}
private LambdaExpression DestinationConstructorExpression(TypeMap typeMap, Expression instanceParameter)
{
var ctorExpr = typeMap.ConstructExpression;
if (ctorExpr != null)
{
return ctorExpr;
}
var newExpression = typeMap.ConstructorMap?.CanResolve == true
? typeMap.ConstructorMap.NewExpression(instanceParameter)
: New(typeMap.DestinationTypeToUse);
return Lambda(newExpression);
}
private class NewFinderVisitor : ExpressionVisitor
{
public NewExpression NewExpression { get; private set; }
protected override Expression VisitNew(NewExpression node)
{
NewExpression = node;
return base.VisitNew(node);
}
}
private List<MemberBinding> CreateMemberBindings(ExpressionRequest request,
TypeMap typeMap,
Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps)
{
var bindings = new List<MemberBinding>();
foreach (var propertyMap in typeMap.GetPropertyMaps().Where(pm => pm.CanResolveValue() && ReflectionHelper.CanBeSet(pm.DestinationProperty)))
{
letPropertyMaps.Push(propertyMap);
CreateMemberBinding(propertyMap);
letPropertyMaps.Pop();
}
return bindings;
void CreateMemberBinding(PropertyMap propertyMap)
{
var result = ResolveExpression(propertyMap, request.SourceType, instanceParameter, letPropertyMaps);
if(propertyMap.ExplicitExpansion && !request.MembersToExpand.Contains(propertyMap.DestinationProperty))
{
return;
}
var propertyTypeMap = _configurationProvider.ResolveTypeMap(result.Type,
propertyMap.DestinationPropertyType);
var propertyRequest = new ExpressionRequest(result.Type, propertyMap.DestinationPropertyType, request.MembersToExpand, request);
if(!propertyRequest.AlreadyExists)
{
var binder = 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.DestinationProperty.DeclaringType?.Name}.{propertyMap.DestinationProperty.Name} ({propertyMap.DestinationPropertyType})";
throw new AutoMapperMappingException(message, null, typeMap.Types, typeMap, propertyMap);
}
var bindExpression = binder.Build(_configurationProvider, propertyMap, propertyTypeMap,
propertyRequest, result, typePairCount, letPropertyMaps);
if (bindExpression != null)
{
var rhs = propertyMap.ValueTransformers
.Concat(typeMap.ValueTransformers)
.Concat(typeMap.Profile.ValueTransformers)
.Where(vt => vt.IsMatch(propertyMap))
.Aggregate(bindExpression.Expression, (current, vtConfig) => ToType(ReplaceParameters(vtConfig.TransformerExpression, ToType(current, vtConfig.ValueType)), propertyMap.DestinationPropertyType));
bindExpression = bindExpression.Update(rhs);
bindings.Add(bindExpression);
}
}
}
}
private static ExpressionResolutionResult ResolveExpression(PropertyMap propertyMap, Type currentType,
Expression instanceParameter, LetPropertyMaps letPropertyMaps)
{
var result = new ExpressionResolutionResult(instanceParameter, currentType);
var matchingExpressionConverter =
ExpressionResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, propertyMap));
result = matchingExpressionConverter?.GetExpressionResolutionResult(result, propertyMap, letPropertyMaps)
?? throw new Exception("Can't resolve this to Queryable Expression");
if(propertyMap.NullSubstitute != null && result.Type.IsNullableType())
{
var currentChild = result.ResolutionExpression;
var currentChildType = result.Type;
var nullSubstitute = propertyMap.NullSubstitute;
var newParameter = result.ResolutionExpression;
var converter = new NullSubstitutionConversionVisitor(newParameter, nullSubstitute);
currentChild = converter.Visit(currentChild);
currentChildType = currentChildType.GetTypeOfNullable();
return new ExpressionResolutionResult(currentChild, currentChildType);
}
return result;
}
private class NullSubstitutionConversionVisitor : ExpressionVisitor
{
private readonly Expression _newParameter;
private readonly object _nullSubstitute;
public NullSubstitutionConversionVisitor(Expression newParameter, object nullSubstitute)
{
_newParameter = newParameter;
_nullSubstitute = nullSubstitute;
}
protected override Expression VisitMember(MemberExpression node) => node == _newParameter ? NullCheck(node) : node;
private Expression NullCheck(Expression input)
{
var underlyingType = input.Type.GetTypeOfNullable();
var nullSubstitute = ToType(Constant(_nullSubstitute), underlyingType);
return Condition(Property(input, "HasValue"), Property(input, "Value"), nullSubstitute, underlyingType);
}
}
internal class ConstantExpressionReplacementVisitor : ExpressionVisitor
{
private readonly ParameterBag _paramValues;
public ConstantExpressionReplacementVisitor(
ParameterBag paramValues) => _paramValues = paramValues;
protected override Expression VisitMember(MemberExpression node)
{
if (!node.Member.DeclaringType.Has<CompilerGeneratedAttribute>())
{
return base.VisitMember(node);
}
var parameterName = node.Member.Name;
if (!_paramValues.TryGetValue(parameterName, out object parameterValue))
{
const string vbPrefix = "$VB$Local_";
if (!parameterName.StartsWith(vbPrefix, StringComparison.Ordinal) || !_paramValues.TryGetValue(parameterName.Substring(vbPrefix.Length), out parameterValue))
{
return base.VisitMember(node);
}
}
return Convert(Constant(parameterValue), node.Member.GetMemberType());
}
}
/// <summary>
/// Expression visitor for making member access null-safe.
/// </summary>
/// <remarks>
/// Use <see cref="NullsafeQueryRewriter" /> to make a query null-safe.
/// copied from NeinLinq (MIT License): https://github.com/axelheer/nein-linq/blob/master/src/NeinLinq/NullsafeQueryRewriter.cs
/// </remarks>
internal class NullsafeQueryRewriter : ExpressionVisitor
{
static readonly LockingConcurrentDictionary<Type, Expression> Cache = new LockingConcurrentDictionary<Type, Expression>(NodeFallback);
/// <inheritdoc />
protected override Expression VisitMember(MemberExpression node) =>
node?.Expression != null
? MakeNullsafe(node, node.Expression)
: base.VisitMember(node);
/// <inheritdoc />
protected override Expression VisitMethodCall(MethodCallExpression node) =>
node?.Object != null
? MakeNullsafe(node, node.Object)
: base.VisitMethodCall(node);
private Expression MakeNullsafe(Expression node, Expression value)
{
// cache "fallback expression" for performance reasons
var fallback = Cache.GetOrAdd(node.Type);
// check value and insert additional coalesce, if fallback is not default
return Condition(
NotEqual(Visit(value), Default(value.Type)),
fallback.NodeType != ExpressionType.Default ? Coalesce(node, fallback) : node,
fallback);
}
private static Expression NodeFallback(Type type)
{
// default values for generic collections
if (type.GetIsConstructedGenericType() && type.GetTypeInfo().GenericTypeArguments.Length == 1)
{
return GenericCollectionFallback(typeof(List<>), type)
?? GenericCollectionFallback(typeof(HashSet<>), type)
?? Default(type);
}
// default value for arrays
if (type.IsArray)
{
return NewArrayInit(type.GetElementType());
}
// default value
return Default(type);
}
private static Expression GenericCollectionFallback(Type collectionDefinition, Type type)
{
var collectionType = collectionDefinition.MakeGenericType(type.GetTypeInfo().GenericTypeArguments);
// try if an instance of this collection would suffice
return type.GetTypeInfo().IsAssignableFrom(collectionType.GetTypeInfo()) ? Convert(New(collectionType), type) : null;
}
}
public class FirstPassLetPropertyMaps : LetPropertyMaps
{
Stack<PropertyMap> _currentPath = new Stack<PropertyMap>();
List<PropertyPath> _savedPaths = new List<PropertyPath>();
IConfigurationProvider _configurationProvider;
public FirstPassLetPropertyMaps(IConfigurationProvider configurationProvider) =>
_configurationProvider = configurationProvider;
public override Expression GetSubQueryMarker()
{
var propertyMap = _currentPath.Peek();
var mapFrom = propertyMap.CustomExpression;
if(!IsSubQuery() || _configurationProvider.ResolveTypeMap(propertyMap.SourceType, propertyMap.DestinationPropertyType) == null)
{
return null;
}
var type = mapFrom.Body.Type;
var marker = Parameter(type, "marker" + propertyMap.DestinationProperty.Name);
_savedPaths.Add(new PropertyPath(_currentPath.Reverse().ToArray(), marker));
return marker;
bool IsSubQuery()
{
if(!(mapFrom.Body is MethodCallExpression methodCall))
{
return false;
}
var method = methodCall.Method;
return method.IsStatic && method.DeclaringType == typeof(Enumerable);
}
}
public override void Push(PropertyMap propertyMap) => _currentPath.Push(propertyMap);
public override void Pop() => _currentPath.Pop();
public override int Count => _savedPaths.Count;
public override LetPropertyMaps New() => new FirstPassLetPropertyMaps(_configurationProvider);
public override QueryExpressions GetSubQueryExpression(ExpressionBuilder builder, Expression projection, TypeMap typeMap, ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount)
{
var letMapInfos = _savedPaths.Select(path => new
{
MapFrom = path.Last.CustomExpression,
MapFromSource = path.PropertyMaps.Take(path.PropertyMaps.Length - 1).Select(pm=>pm.SourceMember).MemberAccesses(instanceParameter),
Property = new PropertyDescription
(
string.Join("_", path.PropertyMaps.Select(pm => pm.DestinationProperty.Name)),
path.Last.SourceType
),
Marker = path.Marker
}).ToArray();
var letProperties = letMapInfos.Select(m => m.Property);
var letType = ProxyGenerator.GetSimilarType(request.SourceType, letProperties);
var typeMapFactory = new TypeMapFactory();
TypeMap firstTypeMap;
lock(_configurationProvider)
{
firstTypeMap = typeMapFactory.CreateTypeMap(request.SourceType, letType, typeMap.Profile);
}
var secondParameter = Parameter(letType, "dto");
ReplaceSubQueries();
var firstExpression = builder.CreateMapExpressionCore(request, instanceParameter, typePairCount, firstTypeMap, LetPropertyMaps.Default);
return new QueryExpressions(firstExpression, projection, secondParameter);
void ReplaceSubQueries()
{
foreach(var letMapInfo in letMapInfos)
{
var letProperty = letType.GetDeclaredProperty(letMapInfo.Property.Name);
var letPropertyMap = firstTypeMap.FindOrCreatePropertyMapFor(letProperty);
letPropertyMap.CustomExpression =
Lambda(letMapInfo.MapFrom.ReplaceParameters(letMapInfo.MapFromSource), (ParameterExpression)instanceParameter);
projection = projection.Replace(letMapInfo.Marker, MakeMemberAccess(secondParameter, letProperty));
}
projection = new ReplaceMemberAccessesVisitor(instanceParameter, secondParameter).Visit(projection);
}
}
class ReplaceMemberAccessesVisitor : ExpressionVisitor
{
Expression _oldObject, _newObject;
public ReplaceMemberAccessesVisitor(Expression oldObject, Expression newObject)
{
_oldObject = oldObject;
_newObject = newObject;
}
protected override Expression VisitMember(MemberExpression node)
{
if(node.Expression != _oldObject)
{
return base.VisitMember(node);
}
return MakeMemberAccess(_newObject, _newObject.Type.GetFieldOrProperty(node.Member.Name));
}
}
}
}
public class LetPropertyMaps
{
public static readonly LetPropertyMaps Default = new LetPropertyMaps();
protected LetPropertyMaps() { }
public virtual Expression GetSubQueryMarker() => null;
public virtual void Push(PropertyMap propertyMap) {}
public virtual void Pop() {}
public virtual int Count => 0;
public virtual LetPropertyMaps New() => Default;
public virtual QueryExpressions GetSubQueryExpression(ExpressionBuilder builder, Expression projection, TypeMap typeMap, ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount)
=> throw new NotImplementedException();
public struct PropertyPath
{
public PropertyPath(PropertyMap[] propertyMaps, Expression marker)
{
PropertyMaps = propertyMaps;
Marker = marker;
}
public PropertyMap[] PropertyMaps { get; }
public Expression Marker { get; }
public PropertyMap Last => PropertyMaps[PropertyMaps.Length - 1];
}
}
public struct QueryExpressions
{
public QueryExpressions(Expression first, Expression second = null, ParameterExpression secondParameter = null)
{
First = first;
Second = second;
SecondParameter = secondParameter;
}
public Expression First { get; }
public Expression Second { get; }
public ParameterExpression SecondParameter { get; }
}
public static class ExpressionBuilderExtensions
{
public static Expression<Func<TSource, TDestination>> GetMapExpression<TSource, TDestination>(
this IExpressionBuilder expressionBuilder)
{
return (Expression<Func<TSource, TDestination>>) expressionBuilder.GetMapExpression(typeof(TSource),
typeof(TDestination), new Dictionary<string, object>(), new MemberInfo[0])[0];
}
}
}