【问题标题】:Correct Collection in ConstantExpression for EF CoreEF Core 的 ConstantExpression 中的正确集合
【发布时间】:2020-06-20 17:54:32
【问题描述】:

我尝试实现我自己的表达式序列化器/反序列化器以通过服务传递它(我想实现我自己的 EF Core 服务端点)。 所以,现在我对 LambdaExpressions 中的集合有疑问。例如,

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);

给我正确的结果。

因此,在使用 Newtonsoft Json 进行序列化/反序列化后,我在 Lambda from Where 方法中的收集出现问题:

错误消息:LINQ 表达式 'DbSet .Where(u => List { d45e1a1a-f546-48db-77ba-08d7775c6a93, 5b21c782-9b95-48f2-77bd-08d7775c6a93, }.Contains(s.Id))' 不能 翻译。要么以可翻译的形式重写查询, 或通过插入调用显式切换到客户端评估 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync()。见https://go.microsoft.com/fwlink/?linkid=2101038 更多信息。

我试图实现这个“建议”,但没有效果(见下面的代码):

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&lt;Guid&gt;。 所以,我比较了ConstantExpressionValue 实现的接口,它代表List&lt;guid&gt; 在序列化之前和反序列化之后——都实现了8 个接口。

所以,也许有人遇到了同样的问题。

谢谢。

附:我不知道为什么 EF Core 给我Where(u =&gt; ... 而不是Where(s =&gt; ...,因为在此表达式的 DebugView 模式下,我看到了正确的Where(s =&gt; ... 表示。

让我们看看序列化/(反序列化和恢复的)表达式(来自 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
}

所以,他们是平等的。并且序列化/反序列化在 Lambda 中没有 u 参数,只是 's'。

【问题讨论】:

    标签: c# linq .net-core entity-framework-core expression-trees


    【解决方案1】:

    问题很可能是反序列化后此处未绑定的 lambda 表达式参数引起的

    s => tstList.Contains(s.Id)
    

    条件并不重要。并且调试显示具有误导性。 s =&gt; 中的 s s.Id 中的 ParameterExpression 实例相同。这在 C# 编译时表达式中不会发生,但可以使用 Expression 类方法轻松完成。请注意,从表达式树的角度来看,参数的名称无关紧要,只是实例。

    比如下面的代码sn-p

    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 表达式来查找和修复导致上述参数表达式差异的代码。

    【讨论】:

    • 异常消息中的u =&gt; 仍然表示参数无效。这是你需要战斗的。我无能为力,因为我没有序列化/反序列化代码。如果你提供完整的复制品,我可以看看。
    • 您的示例用法中的result 是什么以及如何获取它?
    • 没关系,我自己想通了。该问题是由FromNode 方法的LambdaExpressionNodeParameterExpressionNode 实现引起的。在处理 lambda 表达式节点的主体时,您正在创建新的 ParameterExpression 对象,这些对象与创建的对象不同,存储在 parameterExpressions 中并用于创建 LambdaExpression。您需要某种方法将正文中使用的参数节点映射到为 lambda 创建的参数表达式。类似于反序列化程序处理共享引用的方式。
    • 不确定如何使用您的 FromNode 签名来实现,可能需要一些额外的参数,或者您可以将其用作服务定位器以获得某种“参数解析上下文” " - 基本上将(字典)从ParameterExpressionNode 映射到ParameterExpression
    • 好吧,很高兴您解决了这个问题。祝你接下来好运:-)
    【解决方案2】:

    是的,当您在 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) NuGet 中找到 CollectionConstantPredicateBuilder。

    【讨论】:

    • Ох,谢谢!我想,我会在返回 DatabaseService 实现后不久使用它,作为所有使用数据库上下文的服务的一个点!
    • 如果您遇到任何问题,如果您提供反馈,那就太好了,这样我可以改进我的实施。您可以在 GitHub 或此处进行操作。 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-05
    • 2017-01-19
    • 1970-01-01
    • 2019-04-27
    • 2017-04-14
    • 2020-01-25
    相关资源
    最近更新 更多