EF Core 的 ConstantExpression 中的正确集合
Correct Collection in ConstantExpression for EF Core
我尝试实现我自己的表达式 serializator/deserializator 以通过服务传递它(我想为 EF Core 服务实现我自己的端点)。
所以,现在我对 LambdaExpressions 中的 Collections 有疑问。例如,
var dataQuery = testDb.Users.Include(e => e.EmployeeInfo).Include(f => f.Notifications).Where(s => tstList.Contains(s.Id)).Select(e => e.FullName);
var tstEspressionBase = dataQuery.Expression;
var tstEspression = new ReflectionLocalValculationVisitor().Visit(tstEspressionBase);
这里
public class ReflectionLocalValculationVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression memberExpression)
{
var expression = Visit(memberExpression.Expression);
if (expression is ConstantExpression)
{
object container = ((ConstantExpression)expression).Value;
var member = memberExpression.Member;
if (member is FieldInfo)
{
object value = ((FieldInfo)member).GetValue(container);
return Expression.Constant(value);
}
if (member is PropertyInfo)
{
object value = ((PropertyInfo)member).GetValue(container, null);
return Expression.Constant(value);
}
}
return base.VisitMember(memberExpression);
}
}
var tstList = new List<Guid>()
{
new Guid("D45E1A1A-F546-48DB-77BA-08D7775C6A93"),
new Guid("5B21C782-9B95-48F2-77BD-08D7775C6A93")
};
正在使用此代码执行
var providerAsync = testDb.GetService<IAsyncQueryProvider>();
var toListAsyncMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync)).MakeGenericMethod(typeof(string));
var s3 = await toListAsyncMethodInfo.InvokeAsync(null, new object[] { providerAsync.CreateQuery(tstEspression), default(CancellationToken) }).ConfigureAwait(false);
给我正确的结果。
因此,在 serializing/deserializing 使用 Newtonsoft Json 之后,我在 Lambda
中从 Where
方法收集时遇到问题:
Error Message: The LINQ expression 'DbSet
.Where(u => List { d45e1a1a-f546-48db-77ba-08d7775c6a93, 5b21c782-9b95-48f2-77bd-08d7775c6a93, }.Contains(s.Id))' 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.
我试图实现这个"recommendation",但没有效果(见下面的代码):
var asEnumerableMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.AsEnumerable)).MakeGenericMethod(GenericTypes.Select(e => e.FromNode()).ToArray());
var asEnumerabled = asEnumerableMethod.Invoke(null, new object[] { Value });
这里的Value
对象是JSON.NET反序列化后生成的List<Guid>
。
因此,我比较了 ConstantExpression
中 Value
的已实现接口,它表示序列化之前和反序列化之后的 List<guid>
- 都实现了 8 个接口。
所以,也许有人遇到了同样的问题。
谢谢。
P.S。我不知道为什么 EF Core 给我 Where(u => ...
而不是 Where(s => ...
,因为在这个表达式的 DebugView 模式下我看到正确的 Where(s => ...
表示。
让我们看看序列化/(反序列化和恢复)表达式(来自 DebugView 的数据):
.Call System.Linq.Queryable.Select(
.Call System.Linq.Queryable.Where(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]),
'(.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>))
,
'(.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>))
,
'(.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>)),
'(.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>))
.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.EmployeeInfo
}
.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>(EFCoreDataModel.DataClasses.Users.Base.User $f)
{
$f.Notifications
}
.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>(EFCoreDataModel.DataClasses.Users.Base.User $s)
{
.Call .Constant<System.Collections.Generic.List`1[System.Guid]>(System.Collections.Generic.List`1[System.Guid]).Contains($s.Id)
}
.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.FullName
}
原始表达式(来自 DebugView):
.Call System.Linq.Queryable.Select(
.Call System.Linq.Queryable.Where(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]),
'(.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>))
,
'(.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>))
,
'(.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>)),
'(.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>))
.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.EmployeeInfo
}
.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>(EFCoreDataModel.DataClasses.Users.Base.User $f)
{
$f.Notifications
}
.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>(EFCoreDataModel.DataClasses.Users.Base.User $s)
{
.Call .Constant<System.Collections.Generic.List`1[System.Guid]>(System.Collections.Generic.List`1[System.Guid]).Contains($s.Id)
}
.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.FullName
}
所以,他们是平等的。并且 serialized/deserialized 在 Lambda 中没有 u
参数,只是 's' 因为它可能是。
问题很可能是由反序列化后未绑定的 lambda 表达式参数引起的
s => tstList.Contains(s.Id)
条件并不重要。并且调试显示具有误导性。 s =>
中的 s
与 s.Id
中的 s
不是 相同的 ParameterExpression
实例。这不会发生在 C# 编译时表达式中,但可以使用 Expression
class 方法轻松完成。请注意,从表达式树的角度来看,参数的名称并不重要,重要的是实例。
例如下面的代码片段
var param1 = Expression.Parameter(typeof(User), "s");
var param2 = Expression.Parameter(typeof(User), "s");
var body = Expression.Equal(
Expression.Property(param2, "Id"),
Expression.Constant(new Guid("D45E1A1A-F546-48DB-77BA-08D7775C6A93"))
);
var predicate = Expression.Lambda<Func<Blog, bool>>(body, param1);
创建一个有效的 (!?) lambda 表达式,用于 LINQ 查询
var test = testDb.Set<User>().Where(predicate).ToList();
将生成与所讨论的异常类似的异常。
话虽如此,忘掉常量表达式中的集合,专注于 lambda 表达式,找到并修复导致上述参数表达式差异的代码。
是的,当您在 where 表达式中使用您自己的自定义集合作为常量时,EF Core 表现不佳。它们必须在表达式中完全可评估,即使这样 EF Core 也很难正确翻译它们。也许您想压平您的清单?我的意思是,您为每个用户创建一个表达式,将每个项目与其进行比较。
我已经为集合实现了一个小的谓词生成器,它可能对你有帮助吗?它负责所有 ParameterExpression 映射。
var whereExpression = CollectionConstantPredicateBuilder<UserEntity>
// Here begins the scope of ALL users.
.CreateFromCollection(tstList)
// Each expression of one user is combined with OrElse (||)
.DefinePredicatePerItem(consecutiveItemBinaryExpressionFactory: Expression.OrElse,
// Pre-Conditions, you may check for null or empty list or you simply configures
// comparisonValuesBehaviourFlags. Do NOT use method call on 'yourTstList'.
sourceAndItemPredicate: (user, yourTstList) => true)
// Here begins the scope of ALL items of 'yourTstList' in ONE user.
// The resulted expression of the hole collection that belongs to one user
// is combined with the previous expression from ONE user by AndAlso (&&).
.ThenCreateFromCollection(parentBinaryExpressionFactory: Expression.AndAlso,
comparisonValuesFactory: yourTstList => yourTstList,
// If the collection of the comparison values is null or empty,
// the boolean expression branch for each each item won't be
// created. Instead an expression is used that leads to false.
// Ergo, if 'yourTstList' does not contain the user id, the user
// won't be queried.
comparisonValuesBehaviourFlags: ComparisonValuesBehaviourFlags.NullOrEmptyLeadsToFalse)
// Each expression of one item is combined with OrElse (||).
// So one user's ID can be the one or the other 'oneTstItem'.
.DefinePredicatePerItem(Expression.OrElse,
sourceAndItemPredicate: (user, oneTstItem) => user.Id == oneTstItem)
.BuildLambdaExpression();
dbContext.Users.AsQueryable().Where(whereExpression).
您可以在我的 pre-release package (0.1.7-alpha.68) on NuGet.
中找到 CollectionConstantPredicateBuilder
我尝试实现我自己的表达式 serializator/deserializator 以通过服务传递它(我想为 EF Core 服务实现我自己的端点)。 所以,现在我对 LambdaExpressions 中的 Collections 有疑问。例如,
var dataQuery = testDb.Users.Include(e => e.EmployeeInfo).Include(f => f.Notifications).Where(s => tstList.Contains(s.Id)).Select(e => e.FullName);
var tstEspressionBase = dataQuery.Expression;
var tstEspression = new ReflectionLocalValculationVisitor().Visit(tstEspressionBase);
这里
public class ReflectionLocalValculationVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression memberExpression)
{
var expression = Visit(memberExpression.Expression);
if (expression is ConstantExpression)
{
object container = ((ConstantExpression)expression).Value;
var member = memberExpression.Member;
if (member is FieldInfo)
{
object value = ((FieldInfo)member).GetValue(container);
return Expression.Constant(value);
}
if (member is PropertyInfo)
{
object value = ((PropertyInfo)member).GetValue(container, null);
return Expression.Constant(value);
}
}
return base.VisitMember(memberExpression);
}
}
var tstList = new List<Guid>()
{
new Guid("D45E1A1A-F546-48DB-77BA-08D7775C6A93"),
new Guid("5B21C782-9B95-48F2-77BD-08D7775C6A93")
};
正在使用此代码执行
var providerAsync = testDb.GetService<IAsyncQueryProvider>();
var toListAsyncMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync)).MakeGenericMethod(typeof(string));
var s3 = await toListAsyncMethodInfo.InvokeAsync(null, new object[] { providerAsync.CreateQuery(tstEspression), default(CancellationToken) }).ConfigureAwait(false);
给我正确的结果。
因此,在 serializing/deserializing 使用 Newtonsoft Json 之后,我在 Lambda
中从 Where
方法收集时遇到问题:
Error Message: The LINQ expression 'DbSet .Where(u => List { d45e1a1a-f546-48db-77ba-08d7775c6a93, 5b21c782-9b95-48f2-77bd-08d7775c6a93, }.Contains(s.Id))' 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.
我试图实现这个"recommendation",但没有效果(见下面的代码):
var asEnumerableMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.AsEnumerable)).MakeGenericMethod(GenericTypes.Select(e => e.FromNode()).ToArray());
var asEnumerabled = asEnumerableMethod.Invoke(null, new object[] { Value });
这里的Value
对象是JSON.NET反序列化后生成的List<Guid>
。
因此,我比较了 ConstantExpression
中 Value
的已实现接口,它表示序列化之前和反序列化之后的 List<guid>
- 都实现了 8 个接口。
所以,也许有人遇到了同样的问题。
谢谢。
P.S。我不知道为什么 EF Core 给我 Where(u => ...
而不是 Where(s => ...
,因为在这个表达式的 DebugView 模式下我看到正确的 Where(s => ...
表示。
让我们看看序列化/(反序列化和恢复)表达式(来自 DebugView 的数据):
.Call System.Linq.Queryable.Select(
.Call System.Linq.Queryable.Where(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]),
'(.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>))
,
'(.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>))
,
'(.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>)),
'(.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>))
.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.EmployeeInfo
}
.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>(EFCoreDataModel.DataClasses.Users.Base.User $f)
{
$f.Notifications
}
.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>(EFCoreDataModel.DataClasses.Users.Base.User $s)
{
.Call .Constant<System.Collections.Generic.List`1[System.Guid]>(System.Collections.Generic.List`1[System.Guid]).Contains($s.Id)
}
.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.FullName
}
原始表达式(来自 DebugView):
.Call System.Linq.Queryable.Select(
.Call System.Linq.Queryable.Where(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
.Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]),
'(.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>))
,
'(.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>))
,
'(.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>)),
'(.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>))
.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.EmployeeInfo
}
.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>(EFCoreDataModel.DataClasses.Users.Base.User $f)
{
$f.Notifications
}
.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>(EFCoreDataModel.DataClasses.Users.Base.User $s)
{
.Call .Constant<System.Collections.Generic.List`1[System.Guid]>(System.Collections.Generic.List`1[System.Guid]).Contains($s.Id)
}
.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
$e.FullName
}
所以,他们是平等的。并且 serialized/deserialized 在 Lambda 中没有 u
参数,只是 's' 因为它可能是。
问题很可能是由反序列化后未绑定的 lambda 表达式参数引起的
s => tstList.Contains(s.Id)
条件并不重要。并且调试显示具有误导性。 s =>
中的 s
与 s.Id
中的 s
不是 相同的 ParameterExpression
实例。这不会发生在 C# 编译时表达式中,但可以使用 Expression
class 方法轻松完成。请注意,从表达式树的角度来看,参数的名称并不重要,重要的是实例。
例如下面的代码片段
var param1 = Expression.Parameter(typeof(User), "s");
var param2 = Expression.Parameter(typeof(User), "s");
var body = Expression.Equal(
Expression.Property(param2, "Id"),
Expression.Constant(new Guid("D45E1A1A-F546-48DB-77BA-08D7775C6A93"))
);
var predicate = Expression.Lambda<Func<Blog, bool>>(body, param1);
创建一个有效的 (!?) lambda 表达式,用于 LINQ 查询
var test = testDb.Set<User>().Where(predicate).ToList();
将生成与所讨论的异常类似的异常。
话虽如此,忘掉常量表达式中的集合,专注于 lambda 表达式,找到并修复导致上述参数表达式差异的代码。
是的,当您在 where 表达式中使用您自己的自定义集合作为常量时,EF Core 表现不佳。它们必须在表达式中完全可评估,即使这样 EF Core 也很难正确翻译它们。也许您想压平您的清单?我的意思是,您为每个用户创建一个表达式,将每个项目与其进行比较。
我已经为集合实现了一个小的谓词生成器,它可能对你有帮助吗?它负责所有 ParameterExpression 映射。
var whereExpression = CollectionConstantPredicateBuilder<UserEntity>
// Here begins the scope of ALL users.
.CreateFromCollection(tstList)
// Each expression of one user is combined with OrElse (||)
.DefinePredicatePerItem(consecutiveItemBinaryExpressionFactory: Expression.OrElse,
// Pre-Conditions, you may check for null or empty list or you simply configures
// comparisonValuesBehaviourFlags. Do NOT use method call on 'yourTstList'.
sourceAndItemPredicate: (user, yourTstList) => true)
// Here begins the scope of ALL items of 'yourTstList' in ONE user.
// The resulted expression of the hole collection that belongs to one user
// is combined with the previous expression from ONE user by AndAlso (&&).
.ThenCreateFromCollection(parentBinaryExpressionFactory: Expression.AndAlso,
comparisonValuesFactory: yourTstList => yourTstList,
// If the collection of the comparison values is null or empty,
// the boolean expression branch for each each item won't be
// created. Instead an expression is used that leads to false.
// Ergo, if 'yourTstList' does not contain the user id, the user
// won't be queried.
comparisonValuesBehaviourFlags: ComparisonValuesBehaviourFlags.NullOrEmptyLeadsToFalse)
// Each expression of one item is combined with OrElse (||).
// So one user's ID can be the one or the other 'oneTstItem'.
.DefinePredicatePerItem(Expression.OrElse,
sourceAndItemPredicate: (user, oneTstItem) => user.Id == oneTstItem)
.BuildLambdaExpression();
dbContext.Users.AsQueryable().Where(whereExpression).
您可以在我的 pre-release package (0.1.7-alpha.68) on NuGet.
中找到 CollectionConstantPredicateBuilder