Blazor应用中DbContextFactory引发Entity Framework Core性能问题求助
将应用从单一DbContext改为DbContextFactory后功能正常,但性能明显下降,导致整个应用出现输入延迟,而使用长生命周期数据上下文时无此问题。是否有人遇到过类似问题,并有解决办法?
注册代码如下:
builder.Services.AddDbContextFactory<DBContext>( options => options.UseSqlServer(builder.Configuration.GetConnectionString("DevConnection")), ServiceLifetime.Scoped);
补充背景:我知道在Blazor中使用Entity Framework Core时,DbContextFactory是调用DbContext的正确方式。最初所有数据都从单一作用域DbContext实例获取,为解决并发问题,我改成每次工作单元创建新上下文,使用后释放:
DBContext _ContextInstance = _DBContextFactory.CreateDbContext(); ContextInstance.Dispose();
默认情况下DbContextFactory是单例服务,我把它改成了Scoped以适配其他Scoped服务。奇怪的是,本地运行时修改前一切正常,修改后出现了作用域服务尝试使用单例服务的错误。
以下是一个运行缓慢的方法示例(本不应花费3-4秒,VS2022调试器中本地运行速度快一倍):
public List<VMRAMarginDetail> GetFilteredProjects(FilterRef FilterRef, List<int> ProjectIds) { if (ProjectIds != null) { List<VMRAMarginDetail> FilteredProjects = new(); var dateFilterValue = Convert.ToDateTime(FilterRef.MonthEnding); using (MarginReportAppContext _MRAInstance = _MRAFactory.CreateDbContext()) { FilteredProjects = _MRAInstance.V_MRA_MarginDetails.Where(a => a.MonthEnding == dateFilterValue && ProjectIds.Contains(a.ProjectId)).ToList(); } if (FilterRef.Region != null && FilterRef.Region.Length > 0) { //按区域筛选 FilteredProjects = FilteredProjects.Where(a => a.Region.ToUpper() == FilterRef.Region.ToUpper() && a.Group.ToUpper() == FilterRef.Group.ToUpper() && a.Division.ToUpper() == FilterRef.Division).ToList(); } else if (FilterRef.Group != null && FilterRef.Group.Length > 0) { //按组筛选 FilteredProjects = FilteredProjects.Where(a => a.Group.ToUpper() == FilterRef.Group.ToUpper() && a.Division.ToUpper() == FilterRef.Division).ToList(); } else if (FilterRef.Division != null && FilterRef.Division.Length > 0) { //按部门筛选 FilteredProjects = FilteredProjects.Where(a => a.Division.ToUpper() == FilterRef.Division).ToList(); } if (FilterRef.MprApprover != null && FilterRef.MprApprover.Length > 0) { //按MPR审批人筛选 FilteredProjects = FilteredProjects.Where(a => a.MPRApprover == FilterRef.MprApprover).ToList(); } if (FilterRef.Company != null && FilterRef.Company.Length > 0) { //按公司筛选 FilteredProjects = FilteredProjects.Where(a => a.CompanyName == FilterRef.Company).ToList(); } return FilteredProjects; } else { return new(); } }
一、性能问题优化
将内存筛选移至数据库层执行
当前代码先把符合日期和ProjectIds的数据全查出来,再在内存中做后续筛选,这会加载大量不必要的数据到内存,数据量较大时性能极差。应该把所有筛选条件合并到EF查询中,让SQL Server完成筛选:using (MarginReportAppContext _MRAInstance = _MRAFactory.CreateDbContext()) { var query = _MRAInstance.V_MRA_MarginDetails.Where(a => a.MonthEnding == dateFilterValue && ProjectIds.Contains(a.ProjectId)); if (FilterRef.Region != null && FilterRef.Region.Length > 0) { query = query.Where(a => a.Region.ToUpper() == FilterRef.Region.ToUpper() && a.Group.ToUpper() == FilterRef.Group.ToUpper() && a.Division.ToUpper() == FilterRef.Division); } else if (FilterRef.Group != null && FilterRef.Group.Length > 0) { query = query.Where(a => a.Group.ToUpper() == FilterRef.Group.ToUpper() && a.Division.ToUpper() == FilterRef.Division); } else if (FilterRef.Division != null && FilterRef.Division.Length > 0) { query = query.Where(a => a.Division.ToUpper() == FilterRef.Division); } if (FilterRef.MprApprover != null && FilterRef.MprApprover.Length > 0) { query = query.Where(a => a.MPRApprover == FilterRef.MprApprover); } if (FilterRef.Company != null && FilterRef.Company.Length > 0) { query = query.Where(a => a.CompanyName == FilterRef.Company); } FilteredProjects = query.ToList(); }这样EF会生成包含所有筛选条件的SQL,只返回需要的数据,大幅减少数据传输和内存占用。
优化
ProjectIds.Contains的性能
当ProjectIds数量较多时,Contains会生成IN子句,SQL Server处理大量IN值时性能会下降。可以考虑:- 如果ProjectIds数量超过几百条,改用临时表或表值参数传递给SQL
- 确保
V_MRA_MarginDetails表的ProjectId字段有索引
按需复用DbContext实例
虽然Blazor中不建议长生命周期DbContext,但不必每次数据库操作都创建新实例。同一请求/作用域内的多个数据库操作,可复用同一个DbContext实例,减少连接初始化开销。
二、作用域服务错误修复
- 恢复DbContextFactory的默认Singleton生命周期
DbContextFactory默认配置为Singleton是EF的推荐方案。出现作用域服务依赖单例的错误,大概率是你的服务依赖链存在问题:比如有Singleton服务注入了Scoped服务。检查所有服务的注册生命周期,确保依赖关系合规。 - 坚持用
using语句管理DbContext生命周期
用using创建并释放DbContext是正确的方式,能避免上下文泄漏,保证资源正常回收。
内容的提问来源于stack exchange,提问作者Avery

