使用LINQ和Entity Framework时如何避免重复调用ToList()?
我有一个DbSet<T>需要投影到另一种类型,投影操作我使用Select方法。
如果我直接在DbSet<T>上调用Select,代码如下:
private IEnumerable<PersonPOCO> _getPersons(ILocator loc) { using(var service = loc.GetService()) { return service.GetPersons().Select(p => Mapper.ToPoco(p)); } }
这会抛出NotSupportedException异常,因为Entity Framework无法识别该Select调用。
因此我需要调用ToList()来“具体化”查询结果,代码调整为:
private IEnumerable<PersonPOCO> _getPersons(ILocator loc) { using(var service = loc.GetService()) { return service.GetPersons() .ToList() .Select(p => Mapper.ToPoco(p)); } }
如果保持这个写法,Select的懒加载特性会让它只在列表真正被枚举时才执行计算,但此时服务使用的Context已经在using语句外被释放,会触发DbContextDisposedException异常。
因此我需要在using语句内部就完成列表的枚举,最终代码如下:
private IEnumerable<PersonPOCO> _getPersons(ILocator loc) { using(var prov = loc.GetService()) { return prov.GetPersons() .ToList() .Select(p => Mapper.ToPOCO(p)) .ToList(); } }
请问有没有办法可以避免在同一段逻辑中两次调用ToList()?
完全可以避免两次调用ToList(),这里提供三种常用方案,可根据项目情况选择:
方案1:使用List<T>.ConvertAll方法合并转换步骤
List<T>自带的ConvertAll方法是立即执行的,会直接返回转换后的新List,不需要额外调用ToList()触发执行,对原有逻辑改动最小:
private IEnumerable<PersonPOCO> _getPersons(ILocator loc) { using(var prov = loc.GetService()) { return prov.GetPersons() .ToList() .ConvertAll(p => Mapper.ToPOCO(p)); } }
方案2:使用AutoMapper的ProjectTo方法(推荐)
如果项目用AutoMapper实现对象映射,可以直接用ProjectTo扩展方法把投影逻辑下推到EF查询层面,EF可直接识别该方法生成对应SQL,仅查询需要的字段,性能更好,也只需要一次ToList():
private IEnumerable<PersonPOCO> _getPersons(ILocator loc) { using(var prov = loc.GetService()) { return prov.GetPersons() .ProjectTo<PersonPOCO>(mapperConfiguration) .ToList(); } }
只需提前配置好Person到PersonPOCO的映射规则,传入对应的AutoMapper配置对象即可。
方案3:手动遍历转换
如果需要更灵活的控制转换逻辑,可以手动遍历查询得到的实体集合,直接生成目标类型的列表,还可以提前给结果列表分配容量,避免动态扩容的性能损耗,适合数据量较大的场景:
private IEnumerable<PersonPOCO> _getPersons(ILocator loc) { using(var prov = loc.GetService()) { var personList = prov.GetPersons().ToList(); var pocoList = new List<PersonPOCO>(personList.Count); foreach(var person in personList) { pocoList.Add(Mapper.ToPOCO(person)); } return pocoList; } }
内容的提问来源于stack exchange,提问作者Arthur Attout

