我的 C#.NET LINQ 表达式是否针对延迟执行进行了优化(Entity Framework 核心)
Is my C#.NET LINQ expression optimized for deferred execution (Entity Framework Core)
我正在为客户使用 C#.NET 构建 REST API,它将用于从数据库中检索错误日志。数据库有3个table:Fault、Message和MessageData。 table的关系如下:
Fault <---* Message <---1 MessageData
意味着一个故障可以从消息 table 链接到多个消息,而消息数据 table.
又可以从一个消息数据链接到它
我使用 Entity Framework 核心创建了代表这些 table 的实体模型。我还为每个实体模型创建了 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();'),还是在这个阶段部分执行了三次:'.ToList()', '. SingleOrDefault()' 和 '.ToListAsync()?
如前所述,主要关注点是延迟执行。话虽如此,我很乐意收到任何关于优化我的代码的总体性能建议。
GetFaultsAsync
将始终获得所有 Faults
...
首先,您将所有故障放在一个列表中,然后丢弃这些信息。如果用户(= 软件,而不是操作员)想要知道故障的数量,他们必须进行不同的查询,或者 Count()
所有元素。一项改进是 return 和 ICollection<Fault>
,这样人们就可以 Count
。他们甚至可以根据需要添加/删除故障。
当然你可以 return IList<Fault>
,但恕我直言,索引没有意义:
IList<Fault> fetchedFaults = await GGetFaultsAsync(...)
Fault fault4 = fetchedFaults[4];
数字 4 将毫无意义。因此,我的建议是 return ICollection<Fault>
,或者 IReadonlyCollection<Fault>
,如果您不希望人们向获取的数据添加/删除项目。另一方面,如果这意味着人们会将获取的数据复制到新列表,为什么不允许他们更改最初获取的数据?
另一个改进:
var fetchedFaults = await GGetFaultsAsync(...)
var faultsToProcess = fetchedFaults.Take(3);
获取所有 10.000 个错误,然后只使用其中的前 3 个,真是浪费!
When using LINQ it is wise to keep the return value IQueryable / IEnumerable as long as possible. Let the user of the query decide whether he want to add other LINQ statements or not and when he wants to materialize them: `ToList() / FirstOrDefault() / Count() / etc.
最简单的方法是制作一个扩展方法,将 IQueryable<Fault>
作为输入,returns IQueryable<FaultDto>
。如果您不熟悉扩展方法,请参阅 Extension methods demystified
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)。
存储库
如果需要,您可以隐藏故障源:它可以是使用 entity framework 的数据库,但也可以是 CSV 文件或字典(用于单元测试?),也许是互联网上的 REST 调用?
如果需要,请创建一个“存储库”class。用户只知道您可以将项目放入存储库,然后再次获取它们,即使在程序重新启动后也是如此。存储库隐藏了使用 entity framework.
访问项目的信息
如果您只需要查询项目,请创建一个具有 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:一些用户只想查询项目,一些需要添加/删除/更改项目,而只有超级用户需要创建或更改表。
我正在为客户使用 C#.NET 构建 REST API,它将用于从数据库中检索错误日志。数据库有3个table:Fault、Message和MessageData。 table的关系如下:
Fault <---* Message <---1 MessageData
意味着一个故障可以从消息 table 链接到多个消息,而消息数据 table.
又可以从一个消息数据链接到它我使用 Entity Framework 核心创建了代表这些 table 的实体模型。我还为每个实体模型创建了 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();'),还是在这个阶段部分执行了三次:'.ToList()', '. SingleOrDefault()' 和 '.ToListAsync()?
如前所述,主要关注点是延迟执行。话虽如此,我很乐意收到任何关于优化我的代码的总体性能建议。
GetFaultsAsync
将始终获得所有 Faults
...
首先,您将所有故障放在一个列表中,然后丢弃这些信息。如果用户(= 软件,而不是操作员)想要知道故障的数量,他们必须进行不同的查询,或者 Count()
所有元素。一项改进是 return 和 ICollection<Fault>
,这样人们就可以 Count
。他们甚至可以根据需要添加/删除故障。
当然你可以 return IList<Fault>
,但恕我直言,索引没有意义:
IList<Fault> fetchedFaults = await GGetFaultsAsync(...)
Fault fault4 = fetchedFaults[4];
数字 4 将毫无意义。因此,我的建议是 return ICollection<Fault>
,或者 IReadonlyCollection<Fault>
,如果您不希望人们向获取的数据添加/删除项目。另一方面,如果这意味着人们会将获取的数据复制到新列表,为什么不允许他们更改最初获取的数据?
另一个改进:
var fetchedFaults = await GGetFaultsAsync(...)
var faultsToProcess = fetchedFaults.Take(3);
获取所有 10.000 个错误,然后只使用其中的前 3 个,真是浪费!
When using LINQ it is wise to keep the return value IQueryable / IEnumerable as long as possible. Let the user of the query decide whether he want to add other LINQ statements or not and when he wants to materialize them: `ToList() / FirstOrDefault() / Count() / etc.
最简单的方法是制作一个扩展方法,将 IQueryable<Fault>
作为输入,returns IQueryable<FaultDto>
。如果您不熟悉扩展方法,请参阅 Extension methods demystified
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)。
存储库
如果需要,您可以隐藏故障源:它可以是使用 entity framework 的数据库,但也可以是 CSV 文件或字典(用于单元测试?),也许是互联网上的 REST 调用?
如果需要,请创建一个“存储库”class。用户只知道您可以将项目放入存储库,然后再次获取它们,即使在程序重新启动后也是如此。存储库隐藏了使用 entity framework.
访问项目的信息如果您只需要查询项目,请创建一个具有 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:一些用户只想查询项目,一些需要添加/删除/更改项目,而只有超级用户需要创建或更改表。