You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

递归商品分类树转ViewModel后子分类加载失败及模型匹配报错

解决递归商品分类树ViewModel中子分类为Null及视图类型不匹配问题

看起来你在把实体驱动的递归分类树改成ViewModel架构时遇到了两个典型问题:子分类Children属性为null,以及修改后出现的视图类型不匹配异常。我来帮你一步步解决这些问题:

问题根源分析

  1. 子分类为Null的原因:你在控制器的Select投影里只映射了根分类的基础属性,但完全没处理子分类的递归映射——实体的Children是ProductCategory集合,不会自动转换成ViewModelProductCategory集合,而且你没把这部分数据映射到ViewModel里,所以视图里的item.Children自然是null。
  2. 视图类型不匹配的原因:后来你直接赋值Children = v.Children,导致ViewModel的Children是实体类型(ProductCategory),和视图期望的ViewModelProductCategory类型不匹配;再加上新增的CategoryRecursiveModel和视图声明的模型类型不一致,就抛出了类型错误。

解决方案一:递归映射实体到ViewModel

要生成完整的ViewModel分类树,你需要一个递归方法来把每个实体节点(包括子节点)转换成对应的ViewModel。这样既能保证Children属性正确赋值,也能统一类型。

1. 编写实体转ViewModel的递归辅助方法

在你的控制器里添加这个私有方法(或者放到单独的映射类里,更符合SOLID原则):

private ViewModelProductCategory MapToViewModel(ProductCategory entity)
{
    if (entity == null) return null;
    
    var vm = new ViewModelProductCategory
    {
        Id = entity.Id,
        ParentId = entity.ParentId,
        Title = entity.Title,
        SortOrder = entity.SortOrder,
        // 映射当前分类下的商品,支持ProductCountInfo计算
        Products = entity.ProductInCategory?
            .Select(pc => new ViewModelProduct
            {
                Id = pc.Product.Id,
                Title = pc.Product.Title,
                Price = pc.Product.Price,
                Info = pc.Product.Info,
                SortOrder = pc.Product.SortOrder
            })
            .OrderBy(s => s.SortOrder)
            .ToList(),
        // 递归处理子分类,确保整个树形结构完整
        Children = entity.Children?
            .Select(child => MapToViewModel(child))
            .OrderBy(s => s.SortOrder)
            .ToList()
    };

    return vm;
}

2. 修改控制器查询逻辑

现在你需要先查询完整的分类树(包括子分类和关联商品),再映射成ViewModel:

// 先加载所有分类及其子分类、关联商品,确保数据完整
var allCategories = _context.ProductCategories
    .Include(e => e.Children) // 加载子分类
    .Include(e => e.ProductInCategory)
        .ThenInclude(pc => pc.Product) // 加载分类关联的商品
    .OrderBy(s => s.SortOrder)
    .ToList();

// 筛选根分类并映射成ViewModel树形结构
var VMCategories = allCategories
    .Where(r => r.ParentId == null)
    .Select(MapToViewModel)
    .ToList();

// 单独查询无分类商品(不要放在每个分类里,避免重复查询)
var orphanProducts = _context.Products
    .Where(p => !_context.ProductInCategories.Any(pc => pc.ProductId == p.Id))
    .Select(p => new ViewModelProduct
    {
        Id = p.Id,
        Title = p.Title,
        Price = p.Price,
        Info = p.Info,
        SortOrder = p.SortOrder
    })
    .OrderBy(s => s.SortOrder)
    .ToList();

// 用ViewBag传递无分类商品给视图,或者创建一个父ViewModel包裹分类树和无分类商品
ViewBag.OrphanProducts = orphanProducts;

return View(VMCategories);

解决方案二:修复视图类型不匹配问题

现在你的ViewModel树形结构已经是ViewModelProductCategory类型了,需要确保递归视图的模型类型正确:

修改递归视图_CategoryRecursive.cshtml

把视图顶部的模型声明改成:

@model IEnumerable<ViewModelProductCategory>

@if (Model != null && Model.Any())
{
    foreach (var item in Model.OrderBy(s => s.SortOrder))
    {
        <li>
            @item.Title (@item.ProductCountInfo)
            <ul>
                // 这里传入的item.Children已经是ViewModelProductCategory集合,和视图模型匹配
                @Html.Partial("_CategoryRecursive.cshtml", item.Children)
            </ul>
        </li>
    }
}

额外优化建议

  • 修正无分类商品的查询逻辑:你原来的代码里查询无分类商品的逻辑是错误的——v.ProductInCategory.Where(o => !_context.ProductsInCategories.Any(pc => o.Id == pc.ProductId))是在当前分类的关联记录里找不存在的关联记录,这完全不对。正确的逻辑应该是从Products表中找没有任何ProductInCategory关联的商品,也就是上面代码里的orphanProducts查询。
  • 避免重复查询:无分类商品只需要查询一次,不要放到每个分类ViewModel里,不然每个分类都会触发一次相同的SQL查询,严重影响性能。
  • 使用AutoMapper简化映射:如果你的项目里有AutoMapper,可以配置递归映射规则,这样就不用手动写MapToViewModel方法了,代码会更简洁。

内容的提问来源于stack exchange,提问作者Stian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:06:01