Использование 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);
}
}