C# 实体框架核心动态GroupBy

C# 实体框架核心动态GroupBy,c#,entity-framework,linq,entity-framework-core,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;

我希望能够动态地向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; }
}
我可以运行以下代码,它将返回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().
我想我已经很接近了,但是有谁能帮我解决这个问题吗


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

参数应该是表达式而不是委托函数,例如

现在是所需表达式的实现:

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

参数应该是表达式而不是委托函数,例如

现在是所需表达式的实现:

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

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

缺少的只是一个对.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();

不过还是要感谢伊万·斯托夫的初步建议。这让我走上了正确的道路,帮助我最终解决了这个问题。

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

缺少的只是一个对.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();

不过还是要感谢伊万·斯托夫的初步建议。这使我走上了正确的道路,帮助我最终解决了这个问题。

非常感谢您的回复。我已经实现了您的建议,但在执行查询时,我得到以下错误:System.InvalidOperationException:从“VisitLambda”调用时,重写“System.Linq.Expressions.ParameterExpression”类型的节点必须返回同一类型的非空值。或者,重写“VisitLambda”并将其更改为不访问此类型的子级。抱歉,现在应该没有问题了。我喜欢您在扩展方法类中嵌套ExpressionVisitor子类。谢谢-您的解决方案确实有效,它有一些关键点让我走上了正确的道路。最后,我能够更优雅地解决它,但我想说一声非常感谢,因为你的解决方案帮助我实现了目标。非常感谢你的回复。我已经实现了您的建议,但在执行查询时,我得到以下错误:System.InvalidOperationException:从“VisitLambda”调用时,重写“System.Linq.Expressions.ParameterExpression”类型的节点必须返回同一类型的非空值。或者,重写“VisitLambda”并将其更改为不访问此类型的子级。抱歉,现在应该没有问题了。我喜欢您在扩展方法类中嵌套ExpressionVisitor子类。谢谢-您的解决方案确实有效,它有一些关键点让我走上了正确的道路。最后,我能够更优雅地解决它,但我想说一声非常感谢,因为您的解决方案帮助我实现了这一点。AsExpandable和Invoke是LinqKit包中的自定义扩展方法。它们允许调用类似于委托的表达式,但仍然需要生成groupBy1和groupBy2表达式。LinqKit和类似软件包的问题是AsExpandable替换了查询提供程序,这对于EF Core来说通常并不好,因为它会阻止EF Core特定的扩展,如Include、AsNoTracking等。在这种特定情况下,这并不重要,但总的来说,我会避免使用它,或者在编写表达式时只使用Expand。这一切都是有意义的,并且为我提供了很多好的阅读材料。谢谢:AsExpandable和Invoke是LinqKit包中的自定义扩展方法。它们允许调用类似于委托的表达式,但仍然需要生成groupBy1和groupBy2表达式。LinqKit和类似软件包的问题是,AsExpandable替换了查询提供程序,这对于EF Core来说通常并不好,因为它会阻止EF Core特定的扩展,如Include、AsNoTracking等。在这种特定情况下,这并不重要,但通常我会避免使用它,或者在编写expre时只使用Expand
这一切都是有道理的,并为我提供了很多好的阅读材料。谢谢:
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();