在 Linq to Entities 和 Linq to Objects 之间共享表达式
Share expressions between Linq to Entities and Linq to Objects
我正在尝试 "share" Linq to Entities 调用和其他一些代码之间的一组条件,以减少两个调用之间可能的条件不匹配。
我首先声明了我的条件:
private Func<DateTime, Status, bool> _submissionDateExpiredCondition =
(submissionDate, status) => submissionDate < DateTime.Now && status == Status.OK;
private Func<DateTime, Status, bool> _submissionDateWithinOneWeekCondition =
(submissionDate, status) => DateTime.Now < DbFunctions.AddDays(submissionDate, -7) && status == Status.Pending;
private Func<DateTime?, Status, bool> _bidValidityEndPeriodWithinThirtyDaysCondition =
(bidValidityEndPeriod, status) => bidValidityEndPeriod.HasValue && DateTime.Now < DbFunctions.AddDays(bidValidityEndPeriod.Value, -30) && (status == Status.OK);
然后我想在 Linq to Entities where 调用和 if 语句中的函数(或者可能是 Linq to Objects 查询的 where 调用)中的 where 子句中使用这些条件:
myRepository
.FindAll()
.Where(x => x.Property == "value"
&& x.Data.AnotherProperty == true
&& _submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)
|| _submissionDateWithinOneWeekCondition(x.Data.Timestamp, x.Data.Status)
|| _bidValidityEndPeriodWithinThirtyDaysCondition(x.Data.ValidityStamp, x.Data.Status))
and(请注意 MyCustomObject 与 myRepository.FindAll()
返回的类型不同)
private void ApplyConditions(List<MyCustomObject> items) {
foreach(var x in items){
if(_submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)){
x.Property = "condition 1";
}
else if(_submissionDateWithinOneWeekCondition(x.Data.Timestamp, x.Data.Status))
{
x.Property = "condition 2";
}
else if(_bidValidityEndPeriodWithinThirtyDaysCondition(x.Data.ValidityStamp, x.Data.Status))
{
x.Property = "condition 3";
}
}
}
但我一直遇到像
这样的常规问题
The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.
执行存储库查询时...
我试过使用谓词生成器构建谓词(根据 https://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/)但没有成功。
谁能指出我正确的方向?
派对迟到了,但有人可能会发现我解决问题的方式很有用。
但是,如果不进行一些表达式操作,则无法轻松完成。
主要问题是:在 .Where
的谓词表达式中有 InvocationExpression
个委托(即编译代码)。 EF 无法找出委托中包含的逻辑,因此无法将其转换为 SQL。这就是异常的来源。
目标是获得一个 .Where
谓词 lambda 表达式,它在逻辑上等同于您的表达式,但 EF 可以理解。这意味着我们必须从
Expression<Func<EntityType, bool>> xPredicate = x =>
x.Property == "value" &&
x.Data.AnotherProperty == true &&
_submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)
|| ...;
至
Expression<Func<EntityType, bool>> xPredicate = x =>
x.Property == "value" &&
x.Data.AnotherProperty == true &&
x.Data.Timestamp < DateTime.Now && x.Data.Status == Status.OK
|| ...;
用于
myRepository.FindAll().Where(xPredicate)
,其中 EntityType
是 Find
返回的可查询元素类型 - 不同于 MyCustomObject
.
请注意,委托的调用正在被它的定义表达式(lambda 主体)替换,(lambda) 参数 submissionDate
和 status
被调用的相应参数表达式替换.
如果将条件定义为委托,它们的内部逻辑会在编译代码中丢失,因此我们必须从 lambda 表达式而不是委托开始:
private Expression<Func<DateTime, Status, bool>> _xSubmissionDateExpiredCondition = (submissionDate, status) => submissionDate < DateTime.Now && status == Status.OK;
// getting the delegate as before (to be used in ApplyConditions) is trivial:
private Func<DateTime, Status, bool> _submissionDateExpiredCondition = _xSubmissionDateExpiredCondition.Compile();
// ... other conditions here
使用 lambda 表达式而不是委托,编译器允许您像这样重写原始谓词:
Expression<Func<EntityType, bool>> xPredicate = x =>
x.Property == "value" &&
x.Data.AnotherProperty == true &&
_xSubmissionDateExpiredCondition.Compile()(x.Data.Timestamp, x.Data.Status)
|| ...;
,当然 EF 不会比以前更好地理解这一点。然而,我们实现的是条件的内部逻辑是表达式树的一部分。所以所缺少的只是一些魔法:
xPredicate = MAGIC(xPredicate);
MAGIC
的作用:找到委托的 InvocationExpression
,它是对 lambda 表达式调用 Compile()
方法的结果,并将其替换为 lambda 的主体,但是使务必将正文中的 lambda 参数替换为调用的参数表达式。
这是我的实现。其实这里把MAGIC
叫做Express.Prepare
,稍微不那么具体。
/// <summary>
/// Helps in building expressions.
/// </summary>
public static class Express
{
#region Prepare
/// <summary>
/// Prepares an expression to be used in queryables.
/// </summary>
/// <returns>The modified expression.</returns>
/// <remarks>
/// The method replaces occurrences of <see cref="LambdaExpression"/>.Compile().Invoke(...) with the body of the lambda, with it's parameters replaced by the arguments of the invocation.
/// Values are resolved by evaluating properties and fields only.
/// </remarks>
public static Expression<TDelegate> Prepare<TDelegate>(this Expression<TDelegate> lambda) => (Expression<TDelegate>)new PrepareVisitor().Visit(lambda);
/// <summary>
/// Wrapper for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
/// </summary>
public static Expression<Func<T1, TResult>> Prepare<T1, TResult>(Expression<Func<T1, TResult>> lambda) => lambda.Prepare();
/// <summary>
/// Wrapper for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
/// </summary>
public static Expression<Func<T1, T2, TResult>> Prepare<T1, T2, TResult>(Expression<Func<T1, T2, TResult>> lambda) => lambda.Prepare();
// NOTE: more overloads of Prepare here.
#endregion
/// <summary>
/// Evaluate an expression to a simple value.
/// </summary>
private static object GetValue(Expression x)
{
switch (x.NodeType)
{
case ExpressionType.Constant:
return ((ConstantExpression)x).Value;
case ExpressionType.MemberAccess:
var xMember = (MemberExpression)x;
var instance = xMember.Expression == null ? null : GetValue(xMember.Expression);
switch (xMember.Member.MemberType)
{
case MemberTypes.Field:
return ((FieldInfo)xMember.Member).GetValue(instance);
case MemberTypes.Property:
return ((PropertyInfo)xMember.Member).GetValue(instance);
default:
throw new Exception(xMember.Member.MemberType + "???");
}
default:
// NOTE: it would be easy to compile and invoke the expression, but it's intentionally not done. Callers can always pre-evaluate and pass a captured member.
throw new NotSupportedException("Only constant, field or property supported.");
}
}
/// <summary>
/// <see cref="ExpressionVisitor"/> for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
/// </summary>
private sealed class PrepareVisitor : ExpressionVisitor
{
/// <summary>
/// Replace lambda.Compile().Invoke(...) with lambda's body, where the parameters are replaced with the invocation's arguments.
/// </summary>
protected override Expression VisitInvocation(InvocationExpression node)
{
// is it what we are looking for?
var call = node.Expression as MethodCallExpression;
if (call == null || call.Method.Name != "Compile" || call.Arguments.Count != 0 || call.Object == null || !typeof(LambdaExpression).IsAssignableFrom(call.Object.Type))
return base.VisitInvocation(node);
// get the lambda
var lambda = call.Object as LambdaExpression ?? (LambdaExpression)GetValue(call.Object);
// get the expressions for the lambda's parameters
var replacements = lambda.Parameters.Zip(node.Arguments, (p, x) => new KeyValuePair<ParameterExpression, Expression>(p, x));
// return the body with the parameters replaced
return Visit(new ParameterReplaceVisitor(replacements).Visit(lambda.Body));
}
}
/// <summary>
/// <see cref="ExpressionVisitor"/> to replace parameters with actual expressions.
/// </summary>
private sealed class ParameterReplaceVisitor : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, Expression> _replacements;
/// <summary>
/// Init.
/// </summary>
/// <param name="replacements">Parameters and their respective replacements.</param>
public ParameterReplaceVisitor(IEnumerable<KeyValuePair<ParameterExpression, Expression>> replacements)
{
_replacements = replacements.ToDictionary(kv => kv.Key, kv => kv.Value);
}
protected override Expression VisitParameter(ParameterExpression node)
{
Expression replacement;
return _replacements.TryGetValue(node, out replacement) ? replacement : node;
}
}
}
我正在尝试 "share" Linq to Entities 调用和其他一些代码之间的一组条件,以减少两个调用之间可能的条件不匹配。
我首先声明了我的条件:
private Func<DateTime, Status, bool> _submissionDateExpiredCondition =
(submissionDate, status) => submissionDate < DateTime.Now && status == Status.OK;
private Func<DateTime, Status, bool> _submissionDateWithinOneWeekCondition =
(submissionDate, status) => DateTime.Now < DbFunctions.AddDays(submissionDate, -7) && status == Status.Pending;
private Func<DateTime?, Status, bool> _bidValidityEndPeriodWithinThirtyDaysCondition =
(bidValidityEndPeriod, status) => bidValidityEndPeriod.HasValue && DateTime.Now < DbFunctions.AddDays(bidValidityEndPeriod.Value, -30) && (status == Status.OK);
然后我想在 Linq to Entities where 调用和 if 语句中的函数(或者可能是 Linq to Objects 查询的 where 调用)中的 where 子句中使用这些条件:
myRepository
.FindAll()
.Where(x => x.Property == "value"
&& x.Data.AnotherProperty == true
&& _submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)
|| _submissionDateWithinOneWeekCondition(x.Data.Timestamp, x.Data.Status)
|| _bidValidityEndPeriodWithinThirtyDaysCondition(x.Data.ValidityStamp, x.Data.Status))
and(请注意 MyCustomObject 与 myRepository.FindAll()
返回的类型不同)
private void ApplyConditions(List<MyCustomObject> items) {
foreach(var x in items){
if(_submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)){
x.Property = "condition 1";
}
else if(_submissionDateWithinOneWeekCondition(x.Data.Timestamp, x.Data.Status))
{
x.Property = "condition 2";
}
else if(_bidValidityEndPeriodWithinThirtyDaysCondition(x.Data.ValidityStamp, x.Data.Status))
{
x.Property = "condition 3";
}
}
}
但我一直遇到像
这样的常规问题
The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.
执行存储库查询时...
我试过使用谓词生成器构建谓词(根据 https://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/)但没有成功。
谁能指出我正确的方向?
派对迟到了,但有人可能会发现我解决问题的方式很有用。 但是,如果不进行一些表达式操作,则无法轻松完成。
主要问题是:在 .Where
的谓词表达式中有 InvocationExpression
个委托(即编译代码)。 EF 无法找出委托中包含的逻辑,因此无法将其转换为 SQL。这就是异常的来源。
目标是获得一个 .Where
谓词 lambda 表达式,它在逻辑上等同于您的表达式,但 EF 可以理解。这意味着我们必须从
Expression<Func<EntityType, bool>> xPredicate = x =>
x.Property == "value" &&
x.Data.AnotherProperty == true &&
_submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)
|| ...;
至
Expression<Func<EntityType, bool>> xPredicate = x =>
x.Property == "value" &&
x.Data.AnotherProperty == true &&
x.Data.Timestamp < DateTime.Now && x.Data.Status == Status.OK
|| ...;
用于
myRepository.FindAll().Where(xPredicate)
,其中 EntityType
是 Find
返回的可查询元素类型 - 不同于 MyCustomObject
.
请注意,委托的调用正在被它的定义表达式(lambda 主体)替换,(lambda) 参数 submissionDate
和 status
被调用的相应参数表达式替换.
如果将条件定义为委托,它们的内部逻辑会在编译代码中丢失,因此我们必须从 lambda 表达式而不是委托开始:
private Expression<Func<DateTime, Status, bool>> _xSubmissionDateExpiredCondition = (submissionDate, status) => submissionDate < DateTime.Now && status == Status.OK;
// getting the delegate as before (to be used in ApplyConditions) is trivial:
private Func<DateTime, Status, bool> _submissionDateExpiredCondition = _xSubmissionDateExpiredCondition.Compile();
// ... other conditions here
使用 lambda 表达式而不是委托,编译器允许您像这样重写原始谓词:
Expression<Func<EntityType, bool>> xPredicate = x =>
x.Property == "value" &&
x.Data.AnotherProperty == true &&
_xSubmissionDateExpiredCondition.Compile()(x.Data.Timestamp, x.Data.Status)
|| ...;
,当然 EF 不会比以前更好地理解这一点。然而,我们实现的是条件的内部逻辑是表达式树的一部分。所以所缺少的只是一些魔法:
xPredicate = MAGIC(xPredicate);
MAGIC
的作用:找到委托的 InvocationExpression
,它是对 lambda 表达式调用 Compile()
方法的结果,并将其替换为 lambda 的主体,但是使务必将正文中的 lambda 参数替换为调用的参数表达式。
这是我的实现。其实这里把MAGIC
叫做Express.Prepare
,稍微不那么具体。
/// <summary>
/// Helps in building expressions.
/// </summary>
public static class Express
{
#region Prepare
/// <summary>
/// Prepares an expression to be used in queryables.
/// </summary>
/// <returns>The modified expression.</returns>
/// <remarks>
/// The method replaces occurrences of <see cref="LambdaExpression"/>.Compile().Invoke(...) with the body of the lambda, with it's parameters replaced by the arguments of the invocation.
/// Values are resolved by evaluating properties and fields only.
/// </remarks>
public static Expression<TDelegate> Prepare<TDelegate>(this Expression<TDelegate> lambda) => (Expression<TDelegate>)new PrepareVisitor().Visit(lambda);
/// <summary>
/// Wrapper for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
/// </summary>
public static Expression<Func<T1, TResult>> Prepare<T1, TResult>(Expression<Func<T1, TResult>> lambda) => lambda.Prepare();
/// <summary>
/// Wrapper for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
/// </summary>
public static Expression<Func<T1, T2, TResult>> Prepare<T1, T2, TResult>(Expression<Func<T1, T2, TResult>> lambda) => lambda.Prepare();
// NOTE: more overloads of Prepare here.
#endregion
/// <summary>
/// Evaluate an expression to a simple value.
/// </summary>
private static object GetValue(Expression x)
{
switch (x.NodeType)
{
case ExpressionType.Constant:
return ((ConstantExpression)x).Value;
case ExpressionType.MemberAccess:
var xMember = (MemberExpression)x;
var instance = xMember.Expression == null ? null : GetValue(xMember.Expression);
switch (xMember.Member.MemberType)
{
case MemberTypes.Field:
return ((FieldInfo)xMember.Member).GetValue(instance);
case MemberTypes.Property:
return ((PropertyInfo)xMember.Member).GetValue(instance);
default:
throw new Exception(xMember.Member.MemberType + "???");
}
default:
// NOTE: it would be easy to compile and invoke the expression, but it's intentionally not done. Callers can always pre-evaluate and pass a captured member.
throw new NotSupportedException("Only constant, field or property supported.");
}
}
/// <summary>
/// <see cref="ExpressionVisitor"/> for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
/// </summary>
private sealed class PrepareVisitor : ExpressionVisitor
{
/// <summary>
/// Replace lambda.Compile().Invoke(...) with lambda's body, where the parameters are replaced with the invocation's arguments.
/// </summary>
protected override Expression VisitInvocation(InvocationExpression node)
{
// is it what we are looking for?
var call = node.Expression as MethodCallExpression;
if (call == null || call.Method.Name != "Compile" || call.Arguments.Count != 0 || call.Object == null || !typeof(LambdaExpression).IsAssignableFrom(call.Object.Type))
return base.VisitInvocation(node);
// get the lambda
var lambda = call.Object as LambdaExpression ?? (LambdaExpression)GetValue(call.Object);
// get the expressions for the lambda's parameters
var replacements = lambda.Parameters.Zip(node.Arguments, (p, x) => new KeyValuePair<ParameterExpression, Expression>(p, x));
// return the body with the parameters replaced
return Visit(new ParameterReplaceVisitor(replacements).Visit(lambda.Body));
}
}
/// <summary>
/// <see cref="ExpressionVisitor"/> to replace parameters with actual expressions.
/// </summary>
private sealed class ParameterReplaceVisitor : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, Expression> _replacements;
/// <summary>
/// Init.
/// </summary>
/// <param name="replacements">Parameters and their respective replacements.</param>
public ParameterReplaceVisitor(IEnumerable<KeyValuePair<ParameterExpression, Expression>> replacements)
{
_replacements = replacements.ToDictionary(kv => kv.Key, kv => kv.Value);
}
protected override Expression VisitParameter(ParameterExpression node)
{
Expression replacement;
return _replacements.TryGetValue(node, out replacement) ? replacement : node;
}
}
}