Заменить тип параметра в лямбда-выражении

Я пытаюсь заменить тип параметра в лямбда-выражении от одного типа к другому.

Я нашел другие ответы на stackru, т.е. этот, но мне не повезло с ними.

Представьте на секунду, что у вас есть объект домена и хранилище, из которого вы можете получить объект домена.

однако хранилище должно иметь дело с собственными объектами передачи данных, а затем отображать и возвращать объекты домена:

ColourDto.cs

public class DtoColour {

    public DtoColour(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

DomainColour.cs

public class DomainColour {

    public DomainColour(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

Repository.cs

public class ColourRepository {
    ...
    public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
    {
        // Context.Colours is of type ColourDto
        return Context.Colours.Where(predicate).Map().ToList();
    }
}

Как вы можете видеть, это не сработает, так как предикат предназначен для модели предметной области, а коллекция внутри репозитория является коллекцией объектов передачи данных.

Я пытался использовать ExpressionVisitor чтобы сделать это, но не могу понять, как просто изменить тип ParameterExpression например, без исключения

Тестовый сценарий

public class ColourRepository {
    ...
    public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
    {
        var visitor = new MyExpressionVisitor();
        var newPredicate = visitor.Visit(predicate) as Expression<Func<ColourDto, bool>>;
        return Context.Colours.Where(newPredicate.Complie()).Map().ToList();
    }
}


public class MyExpressionVisitor : ExpressionVisitor
{
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return Expression.Parameter(typeof(ColourDto), node.Name);
    }
}

наконец, вот исключение:

System.ArgumentException: свойство 'System.String Name' не определено для типа 'ColourDto'

Надеюсь, кто-то может помочь.

РЕДАКТИРОВАТЬ: Вот это dotnetfiddle

все еще не работает.

Редактировать: вот рабочий dotnetfiddle

Спасибо Eli Arbel

3 ответа

Решение

Вам нужно сделать несколько вещей для этого:

  • Замените экземпляр параметра как на Expression.Lambda и везде, где они появляются в теле - и используют один и тот же экземпляр для обоих.
  • Измените тип делегата лямбды.
  • Замените выражения свойств.

Вот код с добавленными обобщениями:

public static Func<TTarget, bool> Convert<TSource, TTarget>(Expression<Func<TSource, bool>> root)
{
    var visitor = new ParameterTypeVisitor<TSource, TTarget>();
    var expression = (Expression<Func<TTarget, bool>>)visitor.Visit(root);
    return expression.Compile();
}

public class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> _parameters;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return _parameters?.FirstOrDefault(p => p.Name == node.Name) ?? 
            (node.Type == typeof(TSource) ? Expression.Parameter(typeof(TTarget), node.Name) : node);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _parameters = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
        return Expression.Lambda(Visit(node.Body), _parameters);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Member.DeclaringType == typeof(TSource))
        {
            return Expression.Property(Visit(node.Expression), node.Member.Name);
        }
        return base.VisitMember(node);
    }
}

Свойства определяются отдельно для каждого типа.

Эта ошибка происходит, потому что вы не можете получить значение свойства, определенного DomainColour от значения типа ColourDto,

Вам нужно посетить каждый MemberExpression который использует параметр и возвращает новый MemberExpression который использует это свойство из нового типа.

Ответ Эли великолепен.

Но в моем случае у меня есть лямбда, внутри которой есть еще одна лямбда. Таким образом, он устанавливает «_parameter» дважды, переопределяя старый.

бывший:

      Expression<Func<IObjectWithCompanyUnits, bool>> expr = e => e.CompanyUnits.Any(cu => cu.ID == GetCurrentCompanyUnitId());

Посетитель нарушает приведенное выше выражение.

Поэтому я изменил исходный ответ на свой случай:

      public class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
    private Dictionary<int, ReadOnlyCollection<ParameterExpression>> _parameters = new();

    private int currentLambdaIndex = -1;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var prms = _parameters.Count > currentLambdaIndex ? _parameters[currentLambdaIndex] : null;

        var p = prms?.FirstOrDefault(p => p.Name == node.Name);
        if (p != null)
        {
            return p;
        }
        else
        {
            if (node.Type == typeof(TSource))
            {
                return Expression.Parameter(typeof(TTarget), node.Name);
            }
            else
            {
                return node;
            }
        }
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        currentLambdaIndex++;
        try
        {
            _parameters[currentLambdaIndex] = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
            return Expression.Lambda(Visit(node.Body), _parameters[currentLambdaIndex]);
        }
        finally
        {
            currentLambdaIndex--;
        }
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Member.DeclaringType == typeof(TSource))
        {
            return Expression.Property(Visit(node.Expression), node.Member.Name);
        }
        return base.VisitMember(node);
    }
}
Другие вопросы по тегам