如何在ASP.NET Entity Framework Code First中添加子分类并分离关注点
嘿,我来帮你搞定这个问题!要在ASP.NET里用EF Code First实现分类和子分类的层级结构,同时做好分离关注点,咱们一步步来拆解:
一、修改Category类实现父子层级关系
首先咱们要给你的Category类加上父子关联的属性,让EF能识别出层级结构。这里用自关联的方式(一个分类既可以是父类,也可以是子类):
public class Category { public int Id { get; set; } public string Name { get; set; } // 父分类的外键,允许为空(根分类没有父类) public int? ParentCategoryId { get; set; } // 导航属性:关联到父分类 public virtual Category ParentCategory { get; set; } // 导航属性:当前分类的所有子分类集合 public virtual ICollection<Category> ChildrenCategories { get; set; } // 构造函数初始化集合,避免空引用异常 public Category() { ChildrenCategories = new HashSet<Category>(); } }
小提示:virtual关键字是为了启用EF的懒加载,方便后续加载子分类数据;ParentCategoryId用int?是因为根分类没有父分类,需要允许空值。
二、配置DbContext确保关系正确
接下来在你的DbContext里配置这个自关联关系,用Fluent API会更灵活可控:
public class AppDbContext : DbContext { public DbSet<Category> Categories { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // 配置父子分类的关联规则 modelBuilder.Entity<Category>() .HasOptional(c => c.ParentCategory) // 父分类是可选的 .WithMany(c => c.ChildrenCategories) // 一个父分类可以有多个子分类 .HasForeignKey(c => c.ParentCategoryId) // 指定外键字段 .WillCascadeOnDelete(false); // 重点!禁止级联删除,避免删父分类时误删所有子分类 } }
三、用分离关注点优化代码结构
要实现分离关注点,咱们可以采用仓储模式+服务层的组合,把数据访问、业务逻辑、控制器职责拆分开,让代码更易维护和测试。
1. 定义仓储接口(数据访问层契约)
先搞通用仓储接口,再扩展Category专属的接口:
// 通用仓储接口,定义基础CRUD操作 public interface IRepository<T> where T : class { T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity); void Update(T entity); void Delete(T entity); } // Category专属仓储接口,添加自定义数据操作方法 public interface ICategoryRepository : IRepository<Category> { // 获取包含子分类的完整分类树 IEnumerable<Category> GetCategoriesWithChildren(); }
2. 实现仓储类(具体数据访问逻辑)
public class CategoryRepository : ICategoryRepository { private readonly AppDbContext _context; // 通过构造函数注入DbContext,实现解耦 public CategoryRepository(AppDbContext context) { _context = context; } public Category GetById(int id) { // 加载当前分类时同时加载子分类 return _context.Categories.Include(c => c.ChildrenCategories) .FirstOrDefault(c => c.Id == id); } public IEnumerable<Category> GetAll() { return _context.Categories.ToList(); } public void Add(Category entity) { _context.Categories.Add(entity); _context.SaveChanges(); } public void Update(Category entity) { _context.Entry(entity).State = EntityState.Modified; _context.SaveChanges(); } public void Delete(Category entity) { _context.Categories.Remove(entity); _context.SaveChanges(); } public IEnumerable<Category> GetCategoriesWithChildren() { // 只获取根分类(无父分类的),并加载所有子分类 return _context.Categories .Include(c => c.ChildrenCategories) .Where(c => c.ParentCategoryId == null) .ToList(); } }
3. 服务层(处理业务逻辑)
把业务规则、验证逻辑放在服务层,仓储只负责数据读写:
public interface ICategoryService { void AddRootCategory(Category category); void AddSubCategory(int parentCategoryId, Category subCategory); IEnumerable<Category> GetFullCategoryTree(); Category GetCategoryWithSubCategories(int categoryId); } public class CategoryService : ICategoryService { private readonly ICategoryRepository _categoryRepository; // 注入仓储接口,不依赖具体实现 public CategoryService(ICategoryRepository categoryRepository) { _categoryRepository = categoryRepository; } public void AddRootCategory(Category category) { // 业务验证:根分类名称不能重复 if (_categoryRepository.GetAll().Any(c => c.Name.Equals(category.Name, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("该根分类名称已存在"); } category.ParentCategoryId = null; _categoryRepository.Add(category); } public void AddSubCategory(int parentCategoryId, Category subCategory) { var parentCategory = _categoryRepository.GetById(parentCategoryId); if (parentCategory == null) { throw new ArgumentException("指定的父分类不存在"); } // 业务验证:同一父分类下子分类名称不能重复 if (parentCategory.ChildrenCategories.Any(c => c.Name.Equals(subCategory.Name, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("该父分类下已存在同名子分类"); } subCategory.ParentCategoryId = parentCategoryId; _categoryRepository.Add(subCategory); } public IEnumerable<Category> GetFullCategoryTree() { return _categoryRepository.GetCategoriesWithChildren(); } public Category GetCategoryWithSubCategories(int categoryId) { return _categoryRepository.GetById(categoryId); } }
4. 控制器层(处理请求响应)
在Controller里注入服务层,不用直接操作DbContext,彻底解耦:
public class CategoryController : Controller { private readonly ICategoryService _categoryService; public CategoryController(ICategoryService categoryService) { _categoryService = categoryService; } // 展示完整分类树 public ActionResult Index() { var categoryTree = _categoryService.GetFullCategoryTree(); return View(categoryTree); } // 添加子分类的POST请求处理 [HttpPost] public ActionResult AddSubCategory(int parentId, Category subCategory) { try { _categoryService.AddSubCategory(parentId, subCategory); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(subCategory); } } }
四、测试示例
比如你要创建「汽车」根分类,再添加「新车」「旧车」子分类:
// 添加根分类 var carCategory = new Category { Name = "汽车" }; _categoryService.AddRootCategory(carCategory); // 添加子分类:新车 var newCar = new Category { Name = "新车" }; _categoryService.AddSubCategory(carCategory.Id, newCar); // 添加子分类:旧车 var usedCar = new Category { Name = "旧车" }; _categoryService.AddSubCategory(carCategory.Id, usedCar);
运行后数据库会自动生成Categories表,包含Id、Name、ParentCategoryId字段,EF会自动维护父子分类的关联关系。
内容的提问来源于stack exchange,提问作者Simple_tech
相关产品推荐
相关产品推荐

