簡體   English   中英

實體框架核心動態 GroupBy

[英]Entity Framework Core Dynamic GroupBy

我希望能夠動態地將 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().

我想我已經很接近了,但是任何人都可以幫我解決這個問題嗎?

可能有一種更簡單的方法可以做到這一點,所以我願意接受任何建議。 希望代碼說明了我想要實現的目標。

arguments 應該是表達式( 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 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;
    }
}

現在執行所需的表達式:

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

不過,感謝 Ivan Stoev 提出的初步建議。 這使我走上了正確的道路,以幫助我最終解決它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM