C# MVC中添加ModelState.IsValid校验导致页面异常求助
问题:添加书籍时ModelState校验引发空引用错误
我正在开发一个数据库系统,支持用户添加课程、为课程添加书籍,课程与学生为多对多关系。目前已实现页面展示选中课程、其书籍列表及关联学生,且可在该页面添加书籍,但添加ModelState.IsValid校验后,点击提交按钮页面崩溃,出现空引用错误。现在只能通过在表单中添加Course的必填隐藏字段(CourseName、Credits、CourseId)让功能正常,但这不符合仅添加书籍的业务逻辑,希望通过控制器解决该问题。
实体类与视图模型
Course实体类
using System.ComponentModel.DataAnnotations; namespace SchoolPracticeDB.Entities { public class Course { public int CourseId { get; set; } [Required(ErrorMessage = "Please enter a course name.")] public string? CourseName { get; set; } [Required(ErrorMessage = "Please enter credits worth.")] public int? Credits { get; set; } // 导航到学生 public ICollection<Enrollment>? Enrollments { get; set; } // 导航到关联书籍(零到多) public ICollection<Book>? Books { get; set; } } }
CourseDetailsViewModel类
using SchoolPracticeDB.Entities; namespace SchoolPracticeDB.Models { public class CourseDetailsViewModel { public Course? ActiveCourse { get; set; } public Book? NewBook { get; set; } public double? AverageMark { get; set; } } }
控制器代码
未添加校验的正常方法
[HttpPost("/courses/{id}/books")] public IActionResult AddBookToCourseById(int id, CourseDetailsViewModel courseDetailsViewModel) { var course = _courseDbContext.Courses .Include(c => c.Books) .Include(c => c.Enrollments) .ThenInclude(e => e.Student) .Where(c => c.CourseId == id) .FirstOrDefault(); course.Books.Add(courseDetailsViewModel.NewBook); _courseDbContext.SaveChanges(); return RedirectToAction("GetCourseById", "Course", new { id = id }); }
添加校验后引发错误的方法
[HttpPost("/courses/{id}/books")] public IActionResult AddBookToCourseById(int id, CourseDetailsViewModel courseDetailsViewModel) { var course = _courseDbContext.Courses .Include(c => c.Books) .Include(c => c.Enrollments).ThenInclude(e => e.Student) .Where(c => c.CourseId == id) .FirstOrDefault(); if (ModelState.IsValid) { course.Books.Add(courseDetailsViewModel.NewBook); _courseDbContext.SaveChanges(); return RedirectToAction("GetCourseById", "Course", new { id = id }); } else { courseDetailsViewModel.ActiveCourse = _courseDbContext.Courses .Include(c => c.Books) .Include(c => c.Enrollments).ThenInclude(e => e.Student) .Where(c => c.CourseId == id) .FirstOrDefault(); courseDetailsViewModel.NewBook.Course = courseDetailsViewModel.ActiveCourse; return View("CourseDetails", courseDetailsViewModel); } }
表单代码
<div asp-validation-summary="All" class="text-danger"></div> <form class="row" asp-controller="Course" asp-action="AddBookToCourseById" asp-route-id="@Model.ActiveCourse.CourseId" method="post" enctype="application/x-www-form-urlencoded"> <div class="col-sm-4"> <label class="visually-hidden" asp-for="NewBook.Title">Title</label> <div class="input-group"> <div class="input-group-text">Title</div> <input type="text" class="form-control" asp-for="NewBook.Title" /> </div> </div> <div class="col-sm-4"> <label class="visually-hidden" asp-for="NewBook.Author">Author</label> <div class="input-group"> <div class="input-group-text">Author</div> <input type="text" class="form-control" asp-for="NewBook.Author" /> </div> </div> <div class="col-sm-2"> <label class="visually-hidden" asp-for="NewBook.YearPublished">Year</label> <div class="input-group"> <div class="input-group-text">Year</div> <input type="text" class="form-control" asp-for="NewBook.YearPublished" /> </div> </div> <!-- 只有添加这些ActiveCourse的隐藏字段才能正常工作,但不符合业务逻辑 --> <input type="hidden" asp-for="ActiveCourse.CourseName" /> <input type="hidden" asp-for="ActiveCourse.Credits" /> <!-- 添加主键隐藏字段 --> <input type="hidden" asp-for="ActiveCourse.CourseId" /> <div class="col-sm-2"> <button type="submit" class="btn btn-primary">Add Book</button> </div> </form>
问题原因与解决方案
原因
ModelState.IsValid会校验整个CourseDetailsViewModel,包括其中的ActiveCourse对象。而表单提交时,ActiveCourse的CourseName和Credits是必填字段(实体类上有[Required]特性),如果不添加隐藏字段,这两个值为空会导致ModelState无效,进入else分支。分支中若courseDetailsViewModel.NewBook或courseDetailsViewModel.ActiveCourse为空,就会引发空引用异常。
解决方案
方案1:拆分视图模型(推荐)
为添加书籍单独创建视图模型,避免校验无关的ActiveCourse:
public class AddBookViewModel { [Required] public string? Title { get; set; } [Required] public string? Author { get; set; } public int? YearPublished { get; set; } }
修改控制器方法:
[HttpPost("/courses/{id}/books")] public IActionResult AddBookToCourseById(int id, AddBookViewModel addBookViewModel) { var course = _courseDbContext.Courses .Include(c => c.Books) .Include(c => c.Enrollments).ThenInclude(e => e.Student) .FirstOrDefault(c => c.CourseId == id); if (course == null) { return NotFound(); } if (ModelState.IsValid) { var newBook = new Book { Title = addBookViewModel.Title, Author = addBookViewModel.Author, YearPublished = addBookViewModel.YearPublished, Course = course }; _courseDbContext.Books.Add(newBook); _courseDbContext.SaveChanges(); return RedirectToAction("GetCourseById", "Course", new { id = id }); } // 校验失败时重新构建视图模型返回页面 var viewModel = new CourseDetailsViewModel { ActiveCourse = course, NewBook = new Book { Title = addBookViewModel.Title, Author = addBookViewModel.Author, YearPublished = addBookViewModel.YearPublished }, AverageMark = // 按原有逻辑计算平均成绩 }; return View("CourseDetails", viewModel); }
表单无需再添加ActiveCourse相关隐藏字段,保持原有输入控件即可。
方案2:手动移除无关校验
如果不想修改视图模型,可在控制器中排除ActiveCourse的校验规则:
[HttpPost("/courses/{id}/books")] public IActionResult AddBookToCourseById(int id, CourseDetailsViewModel courseDetailsViewModel) { // 移除ActiveCourse的必填校验,我们不需要校验已存在的课程 ModelState.Remove("ActiveCourse.CourseName"); ModelState.Remove("ActiveCourse.Credits"); var course = _courseDbContext.Courses .Include(c => c.Books) .Include(c => c.Enrollments).ThenInclude(e => e.Student) .FirstOrDefault(c => c.CourseId == id); if (course == null) { return NotFound(); } if (ModelState.IsValid) { courseDetailsViewModel.NewBook.Course = course; _courseDbContext.Books.Add(courseDetailsViewModel.NewBook); _courseDbContext.SaveChanges(); return RedirectToAction("GetCourseById", "Course", new { id = id }); } else { courseDetailsViewModel.ActiveCourse = course; return View("CourseDetails", courseDetailsViewModel); } }
此方法无需修改视图模型和表单,直接在控制器中跳过对现有课程字段的校验,符合业务逻辑。
内容的提问来源于stack exchange,提问作者RedGecko
相关产品推荐
相关产品推荐

