Дима_89 Ответов: 0

Проблема с переназначением внутренней лямбды в текущем поиске LINQ


I'm using Automapper and three-layered architecture in my application. I have a method which takes Linq query (LambdaExpression) as a parameter. To pass this parameter in another layer I need to remap it. So, I created an ExpressionExtension class, which is responsible for returning remapped Expression:

/// <summary>
/// A class which contains extension methods for <see cref="Expression"/> and <see cref="Expression{TDelegate}"/> instances.
/// </summary>
public static class ExpressionExtensions
{
    /// <summary>
    /// Remaps all property access from type <typeparamref name="TSource"/> to <typeparamref name="TDestination"/> in <paramref name="expression"/>.
    /// </summary>
    /// <typeparam name="TSource">The type of the source element.</typeparam>
    /// <typeparam name="TDestination">The type of the destination element.</typeparam>
    /// <typeparam name="TResult">The type of the result from the lambda expression.</typeparam>
    /// <param name="expression">The <see cref="Expression{TDelegate}"/> to remap the property access in.</param>
    /// <returns>An <see cref="Expression{TDelegate}"/> equivalent to <paramref name="expression"/>, but applying to elements of type <typeparamref name="TDestination"/> instead of <typeparamref name="TSource"/>.</returns>
    public static Expression<Func<TDestination, TResult>> RemapForType<TSource, TDestination, TResult>(
        this Expression<Func<TSource, TResult>> expression)
    {
        Contract.Requires(expression != null);
        Contract.Ensures(Contract.Result<Expression<Func<TDestination, TResult>>>() != null);

        var newParameter = Expression.Parameter(typeof(TDestination));
        Contract.Assume(newParameter != null);

        var visitor = new AutoMapVisitor<TSource, TDestination>(newParameter);
        var remappedBody = visitor.Visit(expression.Body);
        if (remappedBody == null)
        {
            throw new InvalidOperationException("Unable to remap expression");
        }
        return Expression.Lambda<Func<TDestination, TResult>>(remappedBody, newParameter);
    }
}

I understand that I need to override base Lambda Visitor and to call ExpressionExtension.RemapForType method once again to remap inner lambda. But how can I pass the right parameters to it? So, I decided to create generic method but nothing works right.

Could you help me to get the right solution. E.g. something like that:

Expression<Func<TSource,TResult>> a = val => val.FullName == "ABC" && val.SomeEnumerableProperty.Any(inner => inner.City == "Moscow");
// Here we will get remapped lambda with TDestination class as a parameter and TDestination class properties remapped by AutoMapper as you saw in my posted code
Expression<Func<TDestination, TResult>> b = a.RemapForType<TSource,TDestination,TResult>();

And in I don't know which classes will be used as Source and Destination in inner Lambda because it is runtime. So the example is simply for you to understand what I want to do. And code for AutoMapVisitor:

/// <summary>
/// An <see cref="ExpressionVisitor"/> implementation which uses <see href="http://automapper.org">AutoMapper</see> to remap property access from elements of type <typeparamref name="TSource"/> to elements of type <typeparamref name="TDestination"/>.
/// </summary>
/// <typeparam name="TSource">The type of the source element.</typeparam>
/// <typeparam name="TDestination">The type of the destination element.</typeparam>
public class AutoMapVisitor<TSource, TDestination> : ExpressionVisitor
{
    private readonly TypeMap _typeMap;
    private readonly List<TypeMap> _typeMaps = new List<TypeMap>();
    private readonly Dictionary<Type, ParameterExpression> parameterMap = new Dictionary<Type, ParameterExpression>();
    private Dictionary<MemberInfo, Expression> memberMap = new Dictionary<MemberInfo, Expression>();

    /// <summary>
    /// Initialises a new instance of the <see cref="AutoMapVisitor{TSource, TDestination}"/> class.
    /// </summary>
    /// <param name="newParameter">The new <see cref="ParameterExpression"/> to access.</param>
    public AutoMapVisitor(ParameterExpression newParameter)
    {
        _typeMap = Mapper.FindTypeMapFor<TSource, TDestination>();
        Contract.Assume(_typeMap != null);
        _typeMaps.Add(_typeMap);
        _typeMaps.AddRange(FillNestedTypeMaps(_typeMap));
        parameterMap.Add(newParameter.Type, newParameter); // main parameter which we don't need to recreate
        foreach (TypeMap map in _typeMaps)
        {
            if (parameterMap.ContainsKey(map.DestinationType)) continue;
            parameterMap.Add(map.DestinationType, Expression.Parameter(map.DestinationType, map.DestinationType.Name.ToLower()));
        }
    }

    /// <summary>
    /// находим все возможные маппинги текущей конфигурации вниз по иерархии свойств класса источника
    /// </summary>
    /// <param name="tMap">Объект конфигурации, содержащий информацию о типе источника и типе назначения</param>
    private IEnumerable<TypeMap> FillNestedTypeMaps(TypeMap tMap)
    {
        List<TypeMap> result = new List<TypeMap>();
        // 1 where: маппинг только между классами
        // 2 where: маппинг не равен входящему и ReverseMap значению, например на входе: A -> B, тогда отпадут значения A -> B и B -> A из проверки
        // 3 where: берем те свойства, тип которых совпадает с входящим типом источника или, если это обобщенный тип, с его аргументом и тоже самое для типа назначения
        IEnumerable<PropertyMap> pMaps = tMap.GetPropertyMaps();
        var list = Mapper.GetAllTypeMaps()
                         .Where(
                             map => map.SourceType.IsClass && map.DestinationType.IsClass)
                         .Where(map =>
                                !(map.Equals(tMap) ||
                                  (map.SourceType == tMap.DestinationType && map.DestinationType == tMap.SourceType)))
                         .Where(
                             map =>
                             pMaps
                                 .Any(
                                     pi =>
                                         {
                                             var pis = pi.SourceMember as PropertyInfo;
                                             if (pis == null) return false;
                                             bool forSource = pis.PropertyType == map.SourceType ||
                                                              (pis.PropertyType.IsGenericType &&
                                                               pis.PropertyType.GetGenericArguments()[0] == map.SourceType);
                                             bool forDestination = pi.DestinationPropertyType == map.DestinationType ||
                                                                   (pi.DestinationPropertyType.IsGenericType &&
                                                                    pi.DestinationPropertyType.GetGenericArguments()[0] == map.DestinationType);
                                             return forSource && forDestination;
                                         }))
                         .ToList();
        if (list.Count > 0)
        {
            result.AddRange(list);
            foreach (TypeMap typeMap in list)
            {
                result.AddRange(FillNestedTypeMaps(typeMap));
            }
        }
        return result;
    }

    private Type Map(Type type)
    {
        var tMap = _typeMaps.FirstOrDefault(map => map.SourceType == type);
        Contract.Assume(tMap != null);
        return tMap.DestinationType;
    }

    private ParameterExpression Map(ParameterExpression parameter)
    {
        var mappedType = Map(parameter.Type);
        ParameterExpression mappedParameter;
        if (!parameterMap.TryGetValue(mappedType, out mappedParameter))
            parameterMap.Add(mappedType, mappedParameter = Expression.Parameter(mappedType, parameter.Name));
        return mappedParameter;
    }

    private Expression Map(MemberInfo mi, Expression exp)
    {
        Expression val;
        if (!memberMap.TryGetValue(mi, out val))
        {
            foreach (PropertyMap propertyMap in
                         _typeMaps.Select(map => map.GetPropertyMaps().SingleOrDefault(m => m.SourceMember == mi))
                                  .Where(propertyMap => propertyMap != null))
            {
                memberMap.Add(mi, val = Expression.PropertyOrField(exp, propertyMap.DestinationProperty.MemberInfo.Name));
                break;
            }
        }
        return val;
    }

    /// <summary>
    /// Visits the children of the <see cref="T:System.Linq.Expressions.MemberExpression"/>.
    /// </summary>
    /// <returns>
    /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
    /// </returns>
    /// <param name="node">The expression to visit.</param>
    protected override Expression VisitMember(MemberExpression node)
    {
        var expression = Visit(node.Expression);
        if (expression == node.Expression)
            return node;
        return Map(node.Member, expression);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        return Expression.Lambda(Visit(node.Body), node.Parameters.Select(Map));
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return Map(node);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        // if static object and generic method
        if (node.Object == null && node.Method.IsGenericMethod)
        {
            // Static generic method
            var args = Visit(node.Arguments);
            var genericArgs = node.Method.GetGenericArguments().Select(Map).ToArray();
            var method = node.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs);
            return Expression.Call(method, args);
        }
        return base.VisitMethodCall(node);
    }
}

And now after remapping it throws an Exception:

variable 'person' of type 'Reestr.DAL.Entities.Person' referenced from scope '', but it is not defined

It is interesting that this Exception is thrown only when we have an inner Lambda (e.g. method Any). But if we didn't include it, using only simple search conditions, everything works fine! Seems like there is missing reference on Enumerable property of mapped class or there is newly created parameter somewhere and I can't understand where it can be. Could you give me any suggestions, please, how can I solve this issue! Thanks!


Что я уже пробовал:

Я искал Stackoverflow и Google, но не нашел ничего полезного. Мой верхний код основан на некоторых решениях, которые я нашел в Stackoverflow и правильно скомбинировал.

0 Ответов