Entity Framework 核心嵌套表达式
Entity Framework Core nested expressions
最近发布的 Entity Framework Core 3.0,默认为 LINQ queries are no longer evaluated on the client。我非常喜欢这个变化,因为它揭示了我项目中一些潜在危险的客户端评估,我认为这些评估被翻译成了 SQL;但是,它也使我用来避免疯狂的三元链无法使用的一些辅助方法。
有没有人设法嵌套 LINQ 表达式以用于 Entity Framework Core 3.0?这是我希望实现的示例:
[Fact]
public async Task Can_use_custom_expression()
{
var dbContext = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseInMemoryDatabase("Test").Options);
dbContext.Users.Add(new ApplicationUser { FirstName = "Foo", LastName = "Bar" });
dbContext.SaveChanges();
string query = "Foo";
Expression<Func<string, string, bool>> valueCheck = (value, expected) => !string.IsNullOrEmpty(value) && value.Contains(expected);
var valueCheckFunc = valueCheck.Compile();
Expression<Func<ApplicationUser, bool>> whereExpression = (u) => valueCheckFunc(u.FirstName, query);
var user = await dbContext.Users
.Where(whereExpression)
.FirstOrDefaultAsync();
Assert.NotNull(user);
}
当我运行这个例子时,我得到以下异常:
Message:
System.InvalidOperationException : The LINQ expression 'Where<ApplicationUser>(
source: DbSet<ApplicationUser>,
predicate: (a) => Invoke(__valueCheckFunc_0, a.FirstName, __query_1)
)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
就像我说的,我不想在客户端计算这个表达式,但我想避免在一个表达式中链接十几个 !string.IsNullOrEmpty(x) && x.Contains(y)
。我想要一些关于如何实现这一目标的提示。
如果您希望 EF 将您的表达式翻译成 Sql,您需要避免调用委托或方法(当然有一些例外)。但是你想要实现的是通过用它的定义表达式替换委托调用来实现的。为此,您需要专门的 ExpressionVisitor。
以下访问者将遍历表达式,用 lambda 表达式主体替换其包装调用中的委托引用:
public class DelegateByLambda: ExpressionVisitor
{
LambdaExpression delegateReferenceExpression;
LambdaExpression lambdaExpression;
Stack<InvocationExpression> invocations;
public DelegateByLambda(LambdaExpression delegateReferenceExpression, LambdaExpression lambdaExpression)
{
this.delegateReferenceExpression = delegateReferenceExpression;
this.lambdaExpression = lambdaExpression;
this.invocations = new Stack<InvocationExpression>();
}
protected override Expression VisitParameter(ParameterExpression node)
{
var paramIndex = lambdaExpression.Parameters.IndexOf(node);
if (paramIndex >= 0)
{
InvocationExpression call = invocations.Peek();
return base.Visit(call.Arguments[paramIndex]);
}
return base.VisitParameter(node);
}
protected override Expression VisitInvocation(InvocationExpression node)
{
if (node.Expression.ToString() == delegateReferenceExpression.Body.ToString())
{
invocations.Push(node);
var result = base.Visit(lambdaExpression.Body);
invocations.Pop();
return result;
}
return base.VisitInvocation(node);
}
}
此 class 无法防止尝试用不匹配的参数(数字和类型)替换 lambda 的委托调用,但是,以下扩展方法可以解决问题:
public static class DelegateByLambdaExtensions
{
public static Expression<T> Replace<T, X>(this Expression<T> source, Expression<Func<X>> delegateReference, Expression<X> lambdaReference)
{
return new DelegateByLambda(delegateReference, lambdaReference).Visit(source) as Expression<T>;
}
}
因此,您需要在代码中做的就是对要翻译的表达式调用 replace 扩展方法,传递一个表达式返回委托和所需的 lambda 表达式以进行扩展。您的示例应如下所示:
Expression<Func<string, string, bool>> valueCheck = (value, expected) => !string.IsNullOrEmpty(value) && value.Contains(expected);
var valueCheckFunc = valueCheck.Compile();
Expression<Func<ApplicationUser, bool>> whereExpression = (u) => valueCheckFunc(u.FirstName, query);
whereExpression = whereExpression.Replace(() => valueCheckFunc, valueCheck);
var user = dbContext.Users
.Where(whereExpression)
.FirstOrDefault();
Console.WriteLine(user != null ? $"Found {user.FirstName} {user.LastName}!" : "User not found!");
可在此处找到工作示例。 https://dotnetfiddle.net/Lun3LA
最近发布的 Entity Framework Core 3.0,默认为 LINQ queries are no longer evaluated on the client。我非常喜欢这个变化,因为它揭示了我项目中一些潜在危险的客户端评估,我认为这些评估被翻译成了 SQL;但是,它也使我用来避免疯狂的三元链无法使用的一些辅助方法。
有没有人设法嵌套 LINQ 表达式以用于 Entity Framework Core 3.0?这是我希望实现的示例:
[Fact]
public async Task Can_use_custom_expression()
{
var dbContext = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseInMemoryDatabase("Test").Options);
dbContext.Users.Add(new ApplicationUser { FirstName = "Foo", LastName = "Bar" });
dbContext.SaveChanges();
string query = "Foo";
Expression<Func<string, string, bool>> valueCheck = (value, expected) => !string.IsNullOrEmpty(value) && value.Contains(expected);
var valueCheckFunc = valueCheck.Compile();
Expression<Func<ApplicationUser, bool>> whereExpression = (u) => valueCheckFunc(u.FirstName, query);
var user = await dbContext.Users
.Where(whereExpression)
.FirstOrDefaultAsync();
Assert.NotNull(user);
}
当我运行这个例子时,我得到以下异常:
Message:
System.InvalidOperationException : The LINQ expression 'Where<ApplicationUser>(
source: DbSet<ApplicationUser>,
predicate: (a) => Invoke(__valueCheckFunc_0, a.FirstName, __query_1)
)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
就像我说的,我不想在客户端计算这个表达式,但我想避免在一个表达式中链接十几个 !string.IsNullOrEmpty(x) && x.Contains(y)
。我想要一些关于如何实现这一目标的提示。
如果您希望 EF 将您的表达式翻译成 Sql,您需要避免调用委托或方法(当然有一些例外)。但是你想要实现的是通过用它的定义表达式替换委托调用来实现的。为此,您需要专门的 ExpressionVisitor。
以下访问者将遍历表达式,用 lambda 表达式主体替换其包装调用中的委托引用:
public class DelegateByLambda: ExpressionVisitor
{
LambdaExpression delegateReferenceExpression;
LambdaExpression lambdaExpression;
Stack<InvocationExpression> invocations;
public DelegateByLambda(LambdaExpression delegateReferenceExpression, LambdaExpression lambdaExpression)
{
this.delegateReferenceExpression = delegateReferenceExpression;
this.lambdaExpression = lambdaExpression;
this.invocations = new Stack<InvocationExpression>();
}
protected override Expression VisitParameter(ParameterExpression node)
{
var paramIndex = lambdaExpression.Parameters.IndexOf(node);
if (paramIndex >= 0)
{
InvocationExpression call = invocations.Peek();
return base.Visit(call.Arguments[paramIndex]);
}
return base.VisitParameter(node);
}
protected override Expression VisitInvocation(InvocationExpression node)
{
if (node.Expression.ToString() == delegateReferenceExpression.Body.ToString())
{
invocations.Push(node);
var result = base.Visit(lambdaExpression.Body);
invocations.Pop();
return result;
}
return base.VisitInvocation(node);
}
}
此 class 无法防止尝试用不匹配的参数(数字和类型)替换 lambda 的委托调用,但是,以下扩展方法可以解决问题:
public static class DelegateByLambdaExtensions
{
public static Expression<T> Replace<T, X>(this Expression<T> source, Expression<Func<X>> delegateReference, Expression<X> lambdaReference)
{
return new DelegateByLambda(delegateReference, lambdaReference).Visit(source) as Expression<T>;
}
}
因此,您需要在代码中做的就是对要翻译的表达式调用 replace 扩展方法,传递一个表达式返回委托和所需的 lambda 表达式以进行扩展。您的示例应如下所示:
Expression<Func<string, string, bool>> valueCheck = (value, expected) => !string.IsNullOrEmpty(value) && value.Contains(expected);
var valueCheckFunc = valueCheck.Compile();
Expression<Func<ApplicationUser, bool>> whereExpression = (u) => valueCheckFunc(u.FirstName, query);
whereExpression = whereExpression.Replace(() => valueCheckFunc, valueCheck);
var user = dbContext.Users
.Where(whereExpression)
.FirstOrDefault();
Console.WriteLine(user != null ? $"Found {user.FirstName} {user.LastName}!" : "User not found!");
可在此处找到工作示例。 https://dotnetfiddle.net/Lun3LA