简体   繁体   English

实体框架核心动态 GroupBy

[英]Entity Framework Core Dynamic GroupBy

Instead of hard-coding a GroupBy clause, I would like to be able to dynamically supply a GroupBy clause to an EF query.我希望能够动态地将 GroupBy 子句提供给 EF 查询,而不是对 GroupBy 子句进行硬编码。

Hopefully this code illustrates what I am trying to do:希望这段代码说明了我正在尝试做的事情:

Here are some simple entities for a simple library book loan system:以下是简单图书馆图书借阅系统的一些简单实体:

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

I have these two view models to help with my EF queries:我有这两个视图模型来帮助我的 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; }
}

I can run the following code, and it will return a list of QueryRow objects, which equates to the total number of Loans, grouped by book and member age:我可以运行以下代码,它将返回一个 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();

This works just fine, and returns me a list similar to the following:这很好用,并返回一个类似于以下内容的列表:

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

Now on to the problem: I want to be able to specify my GroupBy clause from outside of the query itself.现在解决问题:我希望能够从查询本身之外指定我的 GroupBy 子句。 The idea is that the two fields that I wish to group by should be held in something similar to variables, and then applied to the query at run-time.这个想法是我希望分组的两个字段应该保存在类似于变量的东西中,然后在运行时应用于查询。 I am very close to doing it.我非常接近做到这一点。 Here is where I am up to:这是我要做的事情:

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

This compiles just fine, but I get the following error at runtime:这编译得很好,但我在运行时收到以下错误:

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

I think I'm quite close, but can anyone help me with how I can get this working?我想我已经很接近了,但是任何人都可以帮我解决这个问题吗?

There may be a much simpler way of doing this, so I am open to any suggestions.可能有一种更简单的方法可以做到这一点,所以我愿意接受任何建议。 Hopefully the code illustrates what I am trying to achieve though.希望代码说明了我想要实现的目标。

The arguments should be expressions ( Expression<Func<>> ) rather than delegates ( Func<> ), eg arguments 应该是表达式( Expression<Func<>> )而不是代表( Func<> ),例如

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

Then you need to compose Expression<Func<Loan, DoubleGroupByClause>> from them, which is not so natural as with delegates, but still possible with the help of the Expression class and the following little helper utility class for replacing lambda expression parameters:然后你需要从它们中组合Expression<Func<Loan, DoubleGroupByClause>> ,这不像代表那么自然,但仍然可以在Expression class 和以下小助手实用程序 class 的帮助下替换 Z945F3FC4495168DB46B32

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

Now the implementation of the desired expression:现在执行所需的表达式:

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

The actual solution was frightfully close to my attempt, so I'm posting in here in case it helps anyone else.实际的解决方案非常接近我的尝试,所以我在这里发帖以防它帮助其他人。

All that was missing was a call to.AsExpandable().缺少的只是对.AsExpandable() 的调用。 This seems to pre-evaluate the Expressions, and then everything works as expected.这似乎预先评估了表达式,然后一切都按预期工作。

So in my example above, it just needed to look like this:所以在我上面的例子中,它只需要看起来像这样:

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

Thanks to Ivan Stoev for your initial suggestions though.不过,感谢 Ivan Stoev 提出的初步建议。 That led me on the correct path to help me eventually solve it.这使我走上了正确的道路,以帮助我最终解决它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM