【问题标题】:Entity Framework Core Dynamic GroupBy实体框架核心动态 GroupBy
【发布时间】:2020-06-08 15:13:30
【问题描述】:

我希望能够动态地将 GroupBy 子句提供给 EF 查询,而不是硬编码 GroupBy 子句。

希望这段代码能说明我想要做什么:

以下是简单图书馆图书借阅系统的一些简单实体:

public class Book
{
   public int Id { get; set; }
   public string Isbn { get; set; }
   public string Title { get; set; }

   public IList<Loan> Loans { get; set; }
}


public class Member
{
   public int Id { get; set; }
   public string FirstName { get; set; }
   public string Surname { get; set; }
   public int Age { get; set; }

   public IList<Loan> Loans { get; set; }
}


public class Loan
{
   public int Id { get; set; }
   public int BookId { get; set; }
   public int MemberId { get; set; }
   public DateTime StartDate { get; set; }
   public DateTime EndDate { get; set; }

   public Book Book { get; set; }
   public Member Member { get; set; }
}

我有这两个视图模型来帮助我的 EF 查询:

public class DoubleGroupByClause
{
   public string GroupByFieldValue1 { get; set; }
   public string GroupByFieldValue2 { get; set; }
}

public class QueryRow
{
   public string GroupBy1 { get; set; }
   public string GroupBy2 { get; set; }
   public int Value { get; set; }
}

我可以运行以下代码,它会返回一个 QueryRow 对象列表,它等于 Loans 的总数,按书籍和成员年龄分组:

var rows = _db.Loans
   // note that the GroupBy method params are hard-coded
   .GroupBy( x => new DoubleGroupByClause{GroupByFieldValue1 = x.BookId, GroupByFieldValue2 = x.Member.Age} )
   .Select(x => new QueryRow
   {
      RowId = x.Key.GroupByFieldValue1.ToString(),
      ColId = x.Key.GroupByFieldValue2.ToString(),
      Value = x.Count()
   })
   .ToList();

这很好用,并返回一个类似于以下内容的列表:

GroupBy1   GroupBy2   Value
(BookId)   (Age)      (Loans)
========   ========   =====
      45         14      23
      45         15      37
      45         16      55
      72         14      34
      72         15      66
      72         16       9

现在解决问题: 我希望能够从查询本身之外指定我的 GroupBy 子句。这个想法是我希望分组的两个字段应该保存在类似于变量的东西中,然后在运行时应用于查询。我非常接近做到这一点。这是我的工作:

// holding the two group by clauses here
Func<Loan, string> groupBy1 = g => g.BookId.ToString();
Func<Loan, string> groupBy2 = g => g.Member.Age.ToString();

// applying the clauses into an expression
Expression<Func<Loan, DoubleGroupByClause>> groupBy = g => new DoubleGroupByClause {GroupByFieldValue1 = groupBy1.Invoke(g), GroupByFieldValue2 = groupBy2.Invoke(g)};

var rows = _db.Loans
   .GroupBy(groupBy)        // applying the expression into the GroupBy clause  
   .Select(x => new QueryRow
   {
      RowId = x.Key.GroupByFieldValue1.ToString(),
      ColId = x.Key.GroupByFieldValue2.ToString(),
      Value = x.Count()
   })
   .ToList();

这编译得很好,但我在运行时收到以下错误:

System.InvalidOperationException: The LINQ expression 'DbSet<Loans>
    .GroupBy(
        source: m => new DoubleGroupByClause{
            GroupByFieldValue1 = __groupBy1_0.Invoke(m),
            GroupByFieldValue2 = __groupBy2_1.Invoke(m)
        }
        ,
        keySelector: m => m)' 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().

我想我已经很接近了,但是任何人都可以帮我解决这个问题吗?

可能有一种更简单的方法可以做到这一点,所以我愿意接受任何建议。希望代码说明了我想要实现的目标。

【问题讨论】:

    标签: c# entity-framework linq entity-framework-core


    【解决方案1】:

    参数应该是表达式 (Expression&lt;Func&lt;&gt;&gt;) 而不是委托 (Func&lt;&gt;),例如

    Expression<Func<Loan, string>> groupBy1 = g => g.BookId.ToString();
    Expression<Func<Loan, string>> groupBy2 = g => g.Member.Age.ToString();
    

    然后你需要从它们中编写Expression&lt;Func&lt;Loan, DoubleGroupByClause&gt;&gt;,这不像委托那样自然,但在Expression 类和以下用于替换 lambda 表达式参数的小助手实用程序类的帮助下仍然可以:

    public static partial class ExpressionUtils
    {
        public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
            => new ParameterReplacer { source = source, target = target }.Visit(expression);
    
        class ParameterReplacer : ExpressionVisitor
        {
            public ParameterExpression source;
            public Expression target;
            protected override Expression VisitParameter(ParameterExpression node)
                => node == source ? target : node;
        }
    }
    

    现在执行所需的表达式:

    Expression<Func<string, string, DoubleGroupByClause>> groupByPrototype = (v1, v2) =>
        new DoubleGroupByClause { GroupByFieldValue1 = v1, GroupByFieldValue2 = v2 };
    var parameter = groupBy1.Parameters[0];
    var v1 = groupBy1.Body;
    var v2 = groupBy2.Body.ReplaceParameter(groupBy2.Parameters[0], parameter);
    var body = groupByPrototype.Body
        .ReplaceParameter(groupByPrototype.Parameters[0], v1)
        .ReplaceParameter(groupByPrototype.Parameters[1], v2);
    var groupBy = Expression.Lambda<Func<Loan, DoubleGroupByClause>>(body, parameter);
    

    【讨论】:

    • 非常感谢您的回复。我已经实现了您的建议,但在执行查询时,我收到以下错误:“System.InvalidOperationException:从'VisitLambda'调用时,重写'System.Linq.Expressions.ParameterExpression'类型的节点必须返回非空相同类型的值。或者,覆盖 'VisitLambda' 并将其更改为不访问此类型的子级。"
    • 抱歉,有小bug 现在应该没问题了。
    • 我喜欢你在扩展方法类中嵌套ExpressionVisitor 子类。
    • 谢谢 - 您的解决方案确实有效,它有一些关键点让我走上了正确的道路。最后,我能够稍微优雅地解决它,但我想非常感谢您,因为您的解决方案帮助我实现了目标。
    【解决方案2】:

    实际的解决方案非常接近我的尝试,所以我在这里发帖以防它帮助其他人。

    缺少的只是对 .AsExpandable() 的调用。这似乎预先评估了表达式,然后一切都按预期工作。

    所以在我上面的例子中,它只需要看起来像这样:

    var rows = _db.Loans
       .AsExpandable()
       .GroupBy(groupBy)        // applying the expression into the GroupBy clause  
       .Select(x => new QueryRow
       {
          RowId = x.Key.GroupByFieldValue1.ToString(),
          ColId = x.Key.GroupByFieldValue2.ToString(),
          Value = x.Count()
       })
       .ToList();
    

    感谢 Ivan Stoev 提出的初步建议。这让我走上了正确的道路,最终帮助我解决了这个问题。

    【讨论】:

    • AsExpandableInvoke 是来自 LinqKit 包的自定义扩展方法。它们允许“调用”类似于委托的表达式(但您仍然需要创建 groupBy1groupBy2 表达式)。 LinqKit 和类似包的问题在于 AsExpandable 替换了查询提供程序,这对于 EF Core 来说通常不好,因为它会阻止 EF Core 特定的扩展,如 IncludeAsNoTracking() 等。在这种特定情况下,它不会没关系,但总的来说我会避免它(或者在编写表达式时只使用Expand())。
    • 这一切都说得通,并且为我提供了很多很好的阅读准备。谢谢:)
    猜你喜欢
    • 2020-05-24
    • 2021-07-14
    • 1970-01-01
    • 1970-01-01
    • 2017-04-03
    • 2020-05-10
    • 2020-09-12
    • 2020-06-21
    • 1970-01-01
    相关资源
    最近更新 更多