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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 23:50:33