实体框架核心动态分组依据

时间:2020-06-08 15:13:30

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

我希望能够为EF查询动态提供GroupBy子句,而不是对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对象的列表,该列表等于按书名和会员年龄分组的贷款总数:

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().

我认为我已经很接近了,但是有人可以帮助我解决这个问题吗?

执行此操作的方法可能更简单,因此我愿意提出任何建议。希望代码可以说明我正在尝试实现的目标。

2 个答案:

答案 0 :(得分:2)

参数应该是表达式(Expression<Func<>>)而不是委托(Func<>),例如

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

然后,您需要从中编写Expression<Func<Loan, DoubleGroupByClause>>,这与委托相比并不自然,但是在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);

答案 1 :(得分:1)

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

所缺少的只是对.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的初步建议。那引导我走上了正确的道路,可以帮助我最终解决它。