递归读取树形结构Linq查询报错:ArgumentNullException排查求助
首先,咱们来拆解你遇到的几个核心问题:
1. 空引用异常的直接原因
你在OrphanProducts的查询里调用了v.ProductInCategory.Where(...),但如果某个分类没有关联任何产品,v.ProductInCategory会是null(因为实体里定义的是List<ProductInCategory>,EF默认不会初始化空列表),这时候调用Where就会抛出ArgumentNullException。
2. 孤儿产品的查询逻辑错误
你当前的查询逻辑是从当前分类的产品关联记录里找“不在任何产品关联记录里的产品”,这本身就是矛盾的——孤儿产品应该是没有任何ProductInCategory关联记录的产品,和某个具体分类无关。
3. 树形视图的类型不匹配问题
你的递归视图_CategoryRecursive接收的是IEnumerable<ViewModelProductCategory>,但你在构建ViewModel时直接把实体的v.Children(类型是IEnumerable<ProductCategory>)赋值给了ViewModelProductCategory.Children,这会导致视图渲染时类型不匹配,后续肯定会报错。
修正后的完整解决方案
第一步:先单独查询孤儿产品
把孤儿产品的查询和分类查询分开,逻辑更清晰,也避免重复查询:
// 获取所有孤儿产品(没有任何分类关联的产品) var orphanProducts = _context.Products .Where(p => !_context.ProductsInCategories.Any(pc => pc.ProductId == p.Id)) .Select(p => new ViewModelProduct { Id = p.Id, Title = p.Title, Price = p.Price, Info = p.Info, SortOrder = 0 // 孤儿产品可以设一个默认排序值 }) .OrderBy(s => s.SortOrder) .ToList();
第二步:递归构建分类ViewModel
因为要生成树形结构,我们需要一个递归方法来把实体ProductCategory转换成ViewModelProductCategory:
// 递归转换分类实体到ViewModel private ViewModelProductCategory MapToViewModel(ProductCategory category) { if (category == null) return null; // 获取当前分类下的产品,处理null情况 var categoryProducts = category.ProductInCategory? .Select(pic => new ViewModelProduct { Id = pic.Product.Id, Title = pic.Product.Title, Price = pic.Product.Price, Info = pic.Product.Info, SortOrder = pic.SortOrder }) .OrderBy(s => s.SortOrder) .ToList() ?? new List<ViewModelProduct>(); return new ViewModelProductCategory { Id = category.Id, ParentId = category.ParentId, Title = category.Title, SortOrder = category.SortOrder, Products = categoryProducts, // 递归转换子分类,处理null情况 Children = category.Children? .OrderBy(c => c.SortOrder) .Select(MapToViewModel) .ToList() ?? new List<ViewModelProductCategory>() }; }
第三步:构建最终的分类ViewModel集合并赋值孤儿产品
// 获取根分类并转换为ViewModel,注意Include所有关联数据 var rootCategories = _context.ProductCategories .Include(e => e.Children) .Include(e => e.ProductInCategory) .ThenInclude(pic => pic.Product) // 必须Include关联的Product,否则会触发懒加载或空引用 .Where(r => r.ParentId == null) .OrderBy(s => s.SortOrder) .Select(MapToViewModel) .ToList(); // 把孤儿产品作为特殊根分类添加进去(也可以通过ViewBag单独传递到视图) rootCategories.Add(new ViewModelProductCategory { Id = -1, Title = "未分类产品", SortOrder = int.MaxValue, OrphanProducts = orphanProducts });
第四步:修正递归视图的类型匹配问题
你的递归视图现在接收的是IEnumerable<ViewModelProductCategory>,所以在渲染子分类时要传递item.Children(现在已经是ViewModel集合了):
@model IEnumerable<MyStore.Models.ViewModels.ViewModelProductCategory> <ul style="list-style:none;padding-left:20px;"> @if (Model != null) { foreach (var item in Model.OrderBy(c => c.SortOrder)) { <li> <h3>@item.Title (@item.ProductCountInfo)</h3> <!-- 显示当前分类的产品 --> @if (item.Products.Any()) { <ul> @foreach (var product in item.Products) { <li>@product.Title - @product.Price</li> } </ul> } <!-- 显示孤儿产品(如果是特殊分类的话) --> @if (item.OrphanProducts != null && item.OrphanProducts.Any()) { <h4>未分类产品</h4> <ul> @foreach (var product in item.OrphanProducts) { <li>@product.Title - @product.Price</li> } </ul> } <!-- 递归渲染子分类 --> @Html.Partial("_CategoryRecursive", item.Children) </li> } } </ul>
额外注意事项
- 一定要确保EF查询中
Include了所有需要的关联数据(比如ProductInCategory和对应的Product),避免懒加载导致的空引用或性能问题。 - 实体类中的集合属性建议初始化为空列表(比如
public List<ProductInCategory> ProductInCategory { get; set; } = new List<ProductInCategory>();),这样即使没有关联数据也不会为null,从根源避免空引用。 - 孤儿产品的展示逻辑可以根据你的需求调整:比如单独放在视图顶部,或者作为一个特殊分类节点,上面的代码示例是后者的实现方式。
内容的提问来源于stack exchange,提问作者Stian

