簡體   English   中英

我的 C#.NET LINQ 表達式是否針對延遲執行進行了優化(實體框架核心)

[英]Is my C#.NET LINQ expression optimized for deferred execution (Entity Framework Core)

我正在為客戶使用 C#.NET 構建 REST API,它將用於從數據庫中檢索錯誤日志。 該數據庫包含三個表:Fault、Message 和 MessageData。 各表關系如下:

故障 <---* 消息 <---1 消息數據

這意味着一個故障可以有多個來自消息表的消息鏈接到它,而后者又可以有一個來自 MessageData 表的消息數據鏈接到它。

我使用 Entity Framework Core 創建了表示這些表的實體模型。 我還為每個實體 Model 創建了 DTO,僅包含與通過網絡傳輸相關的數據。 在我的存儲庫 class 中,我使用 LINQ 向數據庫寫入查詢,將結果從我的實體模型映射到我的 DTO。

我的問題是這段代碼是否可以重寫得更充分,特別是在延遲執行方面(不想對數據庫進行任何不必要的往返):

public async Task<IEnumerable<FaultDTO>> GetFaultsAsync(string? application, DateTime? fromDate, DateTime? toDate, int? count)
        {
            List<FaultDTO> faults;

            fromDate = (fromDate == null) ? DateTime.Today.AddDays(-30) : fromDate;
            toDate = (toDate == null) ? DateTime.Today : toDate;
            count = (count == null) ? 10 : count;

            faults = await _context.Faults.Where(fault => String.IsNullOrWhiteSpace(application) ? fromDate <= fault.InsertedDate && fault.InsertedDate <= toDate : fault.Application.ToLower() == application.ToLower() && fromDate <= fault.InsertedDate && fault.InsertedDate <= toDate).Select(fault => new FaultDTO()
            {
                FaultId = fault.FaultId,
                InsertedDate = fault.InsertedDate,
                MachineName = fault.MachineName,
                ServiceName = fault.ServiceName,
                Scope = fault.Scope,
                FaultDescription = fault.FaultDescription,
                Messages = _context.Messages.Where(msg => fault.FaultId == msg.FaultId).Select(msg => new MessageDTO()
                {
                    MessageId = msg.MessageId,
                    MessageName = msg.MessageName,
                    MessageData = _context.MessageData.Where(msgData => msg.MessageId == msgData.MessageId).Select(msgData => new MessageDataDTO()
                    {
                        MessageData = msgData.MessageData,
                        MessageId = msgData.MessageId
                    }).SingleOrDefault(),
                    FaultId = fault.FaultId,
                }).ToList()
            }).OrderByDescending(fault => fault.InsertedDate).Take((int)count).ToListAsync<FaultDTO>();

            return faults;
        }

此外,如果有人可以澄清查詢是否在最后對數據庫執行('.ToListAsync();'),還是在此階段部分執行了 3 次:'.ToList()'、'.SingleOrDefault()' 和'.ToListAsync()?

如前所述,主要關注點是延遲執行。 話雖這么說,我很樂意收到任何關於在性能方面優化我的代碼的建議。

GetFaultsAsync將始終獲取所有Faults

首先,您將所有故障都放在一個列表中,然后丟棄這些信息。 如果用戶(= 軟件,而不是操作員)想知道故障的數量,他們必須執行不同的查詢,或者Count()所有元素。 一項改進是返回ICollection<Fault> ,以便人們可以Count 他們甚至可以根據需要添加/刪除故障。

當然你可以返回IList<Fault> ,但恕我直言,索引沒有意義:

IList<Fault> fetchedFaults = await GGetFaultsAsync(...)
Fault fault4 = fetchedFaults[4];

4 號將毫無意義。 因此,我的建議是返回ICollection<Fault> ,或者IReadonlyCollection<Fault> ,如果您不希望人們在獲取的數據中添加/刪除項目。 另一方面,如果這意味着人們會將獲取的數據復制到一個新列表中,為什么不讓他們更改最初獲取的數據呢?

另一個改進:

var fetchedFaults = await GGetFaultsAsync(...)
var faultsToProcess = fetchedFaults.Take(3);

獲取所有 10.000 個故障,然后只使用前 3 個故障,真是太浪費了!

使用 LINQ 時,明智的做法是盡可能長時間地保持返回值 IQueryable / IEnumerable。 讓查詢的用戶決定是否要添加其他 LINQ 語句以及何時實現它們:`ToList() / FirstOrDefault() / Count() / etc。

最簡單的方法是創建一個以IQueryable<Fault>作為輸入並返回IQueryable<FaultDto>的擴展方法。 如果您不熟悉擴展方法,請參閱擴展方法揭秘

public static IQueryable<FaultDto> GetFaults(this IQueryable<Fault> faults,
    string? application, 
    DateTime? fromDate, DateTime? toDate)
{
    return faults.Where(fault => ...)
        .Select(fault => new FaultDto
        {
            FaultId = fault.FaultId,
            InsertedDate = fault.InsertedDate,
            ...
        });
}

用法:

string? application = ...
DateTime? fromDate = ...
DateTime? toDate = ...
using (var dbContext = new MyDbContext())
{
    var result = await dbContext.Faults.GetFaults(application, fromDate, toDate)
        .GroupBy(fault => fault.MachineName)
        .Take(10)
        .ToListAsync();
    this.Process(result);
}

優點:

  • 用戶可以連接其他 LINQ 語句
  • 您不會獲取比實際使用更多的項目。
  • 調用者決定是否要使用 async/await。
  • 調用者決定他想使用哪個故障序列

后者的例子:

var result = await dbContext.Faults
    .Where(fault => fault.MachineName == "SPX100")
    .GetFaults(application, fromDate, toDate);

缺點:調用者必須創建故障源(DbContext)。

存儲庫

如果需要,您可以隱藏錯誤的來源:它可以是使用實體框架的數據庫,但也可以是 CSV 文件或字典(用於單元測試?),可能是互聯網上的 REST 調用?

如果你想要這個,創建一個“存儲庫”class。 用戶所知道的是,您可以將項目放入存儲庫中,然后再次獲取它們,即使在程序重新啟動后也是如此。 存儲庫隱藏使用實體框架訪問項目。

如果您只需要查詢項目,請使用IQueryable<...>屬性創建存儲庫。 如果要使用此存儲庫 class 添加/刪除項目,請使用ICollection<...>IDbSet<...>屬性。 但請注意,后一種解決方案限制了更改內部結構的可能性。

class Repository : IDisposable
{
    private readonly MyDbContext dbContext = new MyDbContext(...);

    public IQueryable<Fault> Faults => dbContext.Faults;
    public IQueryable<Message> Messages => dbContext.Messages;
    ...

    // Dispose() disposes the DbContext
}

用法:

using (Repository repository = new Repository()
{
    var result = await repository.Faults.GetFaults(application, fromDate, toDate)
        .GroupBy(fault => fault.MachineName)
        .Take(10)
        .ToListAsync();
    this.Process(result);
}

使用 Repository 的另一個好處是,可以給不同的用戶不同的 Repositories:有些用戶只想查詢項目,有些用戶需要添加/刪除/更改項目,而只有超級用戶需要創建或更改表。

暫無
暫無

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

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