Использование LINQ ExpressionVisitor для замены примитивных параметров ссылками на свойства в выражении лямбда

Я нахожусь в процессе написания слоя данных для части нашей системы, которая регистрирует информацию о автоматизированных заданиях, которые запускаются каждый день - имя задания, как долго оно работает, каков результат и т.д.

Я говорю с базой данных, используя Entity Framework, но я пытаюсь скрыть эти детали от модулей более высокого уровня, и я не хочу, чтобы сами объекты объектов были открыты.

Однако я хотел бы сделать мой интерфейс очень гибким в критериях, которые он использует для поиска информации о работе. Например, пользовательский интерфейс должен позволить пользователю выполнять сложные запросы, такие как "дать мне все задания с именем" hello ", которые выполнялись с 10:00 до 11:00, что не удалось". Очевидно, что это выглядит как работа для динамически построенных деревьев Expression.

Так что я хотел бы, чтобы мой уровень данных (репозиторий) был в состоянии сделать, это принять выражения LINQ типа Expression<Func<string, DateTime, ResultCode, long, bool>> (выражение lambda), а затем за кадром преобразовать этот лямбда в выражение, которое моя Entity Framework ObjectContext может использоваться как фильтр внутри предложения Where().

Вкратце, я пытаюсь преобразовать лямбда-выражение типа Expression<Func<string, DateTime, ResultCode, long, bool>> в Expression<Func<svc_JobAudit, bool>>, где svc_JobAudit - объект данных Entity Framework, который соответствует таблице, в которой хранится информация о задании. (Четыре параметра в первом делетете соответствуют имени задания, при его запуске, результату и длительности в MS соответственно)

Я делал очень хороший прогресс, используя класс ExpressionVisitor, пока не ударил кирпичную стену и получил сообщение InvalidOperationException с этим сообщением об ошибке:

При вызове из "VisitLambda" переписывание типа node'System.Linq.Expressions.ParameterExpression' должен возвращать ненулевое значение значение того же типа. Альтернативно, переопределить 'VisitLambda' и измените его, чтобы не посещать детей этого типа.

Я полностью сбит с толку. Почему heck не позволяет мне преобразовать узлы выражения, которые ссылаются на узлы, которые ссылаются на свойства? Есть ли еще один способ сделать это?

Вот пример кода:

namespace ExpressionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Expression<Func<string, DateTime, ResultCode, long, bool>> expression = (myString, myDateTime, myResultCode, myTimeSpan) => myResultCode == ResultCode.Failed && myString == "hello";
            var result = ConvertExpression(expression);
        }

        private static Expression<Func<svc_JobAudit, bool>> ConvertExpression(Expression<Func<string, DateTime, ResultCode, long, bool>> expression)
        {
            var newExpression = Expression.Lambda<Func<svc_JobAudit, bool>>(new ReplaceVisitor().Modify(expression), Expression.Parameter(typeof(svc_JobAudit)));
            return newExpression;
        }
    }

    class ReplaceVisitor : ExpressionVisitor
    {
        public Expression Modify(Expression expression)
        {
            return Visit(expression);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (node.Type == typeof(string))
            {
                return Expression.Property(Expression.Parameter(typeof(svc_JobAudit)), "JobName");
            }
            return node;
        }
    }
}

Ответ 1

Проблема была в два раза:

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

  • Мне нужно было провести ссылку на новый экземпляр ParameterExpression, который я не делал.

Новый код выглядит так (обратите внимание, что теперь посетитель принимает ссылку на ParameterExpression, соответствующий объекту Data Entity Framework):

class Program
{
    const string conString = @"myDB";

    static void Main(string[] args)
    {
        Expression<Func<string, DateTime, byte, long, bool>> expression = (jobName, ranAt, resultCode, elapsed) => jobName == "Email Notifications" && resultCode == (byte)ResultCode.Failed;
        var criteria = ConvertExpression(expression);

        using (MyDataContext dataContext = new MyDataContext(conString))
        {
            List<svc_JobAudit> jobs = dataContext.svc_JobAudit.Where(criteria).ToList();
        }
    }

    private static Expression<Func<svc_JobAudit, bool>> ConvertExpression(Expression<Func<string, DateTime, byte, long, bool>> expression)
    {
        var jobAuditParameter = Expression.Parameter(typeof(svc_JobAudit), "jobAudit");
        var newExpression = Expression.Lambda<Func<svc_JobAudit, bool>>(new ReplaceVisitor().Modify(expression.Body, jobAuditParameter), jobAuditParameter);
        return newExpression;
    }
}

class ReplaceVisitor : ExpressionVisitor
{
    private ParameterExpression parameter;

    public Expression Modify(Expression expression, ParameterExpression parameter)
    {
        this.parameter = parameter;
        return Visit(expression);
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        return Expression.Lambda<Func<svc_JobAudit, bool>>(Visit(node.Body), Expression.Parameter(typeof(svc_JobAudit)));
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node.Type == typeof(string))
        {
            return Expression.Property(parameter, "JobName");
        }
        else if (node.Type == typeof(DateTime))
        {
            return Expression.Property(parameter, "RanAt");
        }
        else if (node.Type == typeof(byte))
        {
            return Expression.Property(parameter, "Result");
        }
        else if (node.Type == typeof(long))
        {
            return Expression.Property(parameter, "Elapsed");
        }
        throw new InvalidOperationException();
    }
}

Ответ 2

Принятый ответ "жестко закодирован" для некоторых конкретных типов. Здесь более общий редиректор выражения, чем может заменить параметр для любого другого выражения (лямбда, константа,...). В случае лямбда-выражения подпись выражения должна изменяться для включения параметров, необходимых для замещенного значения.

public class ExpressionParameterSubstitute : System.Linq.Expressions.ExpressionVisitor
{
    private readonly ParameterExpression from;
    private readonly Expression to;
    public ExpressionParameterSubstitute(ParameterExpression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        if (node.Parameters.All(p => p != this.from))
            return node;

        // We need to replace the `from` parameter, but in its place we need the `to` parameter(s)
        // e.g. F<DateTime,Bool> subst F<Source,DateTime> => F<Source,bool>
        // e.g. F<DateTime,Bool> subst F<Source1,Source2,DateTime> => F<Source1,Source2,bool>

        var toLambda = to as LambdaExpression;
        var substituteParameters = toLambda?.Parameters ?? Enumerable.Empty<ParameterExpression>();

        ReadOnlyCollection<ParameterExpression> substitutedParameters
            = new ReadOnlyCollection<ParameterExpression>(node.Parameters
                .SelectMany(p => p == this.from ? substituteParameters : Enumerable.Repeat(p, 1) )
                .ToList());

        var updatedBody = this.Visit(node.Body);        // which will convert parameters to 'to'
        return Expression.Lambda(updatedBody, substitutedParameters);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var toLambda = to as LambdaExpression;
        if (node == from) return toLambda?.Body ?? to;
        return base.VisitParameter(node);
    }
}