晚會晚了,但有人可能會發現我的攻擊方式有用。但是,如果不進行某些表達式操作就不容易做到。
主要問題是:在.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 body)替換,其中(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
也:找一個委託,它是一個lambda表達式Compile()
方法調用的結果的InvocationExpression
,並與拉姆達的身體代替它,但一定要將主體中的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;
}
}
}
看一看這個問題,可能你指出正確的方向:http://stackoverflow.com/questions/5284912/the-linq-expression-node-type-invoke-is-not-支持在LINQ到實體 – JanR
@JanR - 我見過這個例子。這裏大多數例子的問題是它們都與Linq to Entities相關,並且它們都具有實體類型作爲表達式/ func的輸入參數。 我不得不縮小到特定的輸入類型(例如DateTime和Status),因爲我想使用我的實體類型和自定義對象類型上的funcs。 –
是否有可能使您的實體框架實體類型和您的MyCustomObject類型實現相同的接口?這可以讓你將你的過濾器作爲表達式寫入該接口。 –