教程:在 ASP.NET MVC 中使用实体框架实现 CRUD 功能

上一教程中,你创建了一个 MVC 应用程序,该应用程序使用 Entity Framework (EF) 6 和 SQL Server LocalDB 来存储和显示数据。 在本教程中,你将查看并自定义 MVC 基架在控制器和视图中自动创建的创建、读取、更新、删除 (CRUD) 代码。

注意

为了在控制器和数据访问层之间创建一个抽象层,常见的做法是实现存储库模式。 为了保持这些教程的简单性,重点介绍如何使用 EF 6 本身,它们不使用存储库。 有关如何实现存储库的信息,请参阅 ASP.NET 数据访问内容映射

下面是你创建的网页的示例:

学生详细信息页的屏幕截图。

学生创建页面的屏幕截图。

学生删除页面的屏幕截图。

在本教程中,你将了解:

  • 创建详细信息页
  • 更新“创建”页
  • 更新 HttpPost Edit 方法
  • 更新“删除”页
  • 关闭数据库连接
  • 处理事务

先决条件

创建详细信息页

“学生 Index ”页的基架代码遗漏了 Enrollments 属性,因为该属性包含一个集合。 在 Details 页面中,你将在 HTML 表中显示集合的内容。

Controllers\StudentController.cs 中,视图的操作方法 Details 使用 Find 方法检索单个 Student 实体。

public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

键值作为 参数传递给 方法,id并且来自“索引”页上“详细信息”超链接中的路由数据

提示: 路由数据

路由数据是模型联编程序在路由表中指定的 URL 段中找到的数据。 例如,默认路由指定 controlleractionid 段:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

在以下 URL 中,默认路线映射Instructor为 ,映射为 controlleractionIndex 1 映射为 id;这些是路线数据值。

http://localhost:1230/Instructor/Index/1?courseID=2021

?courseID=2021 是查询字符串值。 如果将 作为查询字符串值传递, id 则模型联编程序也将正常工作:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

URL 由 ActionLink Razor 视图中的 语句创建。 在以下代码中 id , 参数与默认路由匹配,因此 id 会添加到路由数据中。

@Html.ActionLink("Select", "Index", new { id = item.PersonID  })

在以下代码中, courseID 与默认路由中的参数不匹配,因此将其添加为查询字符串。

@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })

创建“详细信息”页

  1. 打开 Views\Student\Details.cshtml

    每个字段都使用 DisplayFor 帮助程序显示,如以下示例所示:

    <dt>
        @Html.DisplayNameFor(model => model.LastName)
    </dt>
    <dd>
        @Html.DisplayFor(model => model.LastName)
    </dd>
    
  2. EnrollmentDate 字段后面和结束 </dl> 标记之前,添加突出显示的代码以显示注册列表,如以下示例所示:

    <dt>
                @Html.DisplayNameFor(model => model.EnrollmentDate)
            </dt>
    
            <dd>
                @Html.DisplayFor(model => model.EnrollmentDate)
            </dd>
            <dt>
                @Html.DisplayNameFor(model => model.Enrollments)
            </dt>
            <dd>
                <table class="table">
                    <tr>
                        <th>Course Title</th>
                        <th>Grade</th>
                    </tr>
                    @foreach (var item in Model.Enrollments)
                    {
                        <tr>
                            <td>
                                @Html.DisplayFor(modelItem => item.Course.Title)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => item.Grade)
                            </td>
                        </tr>
                    }
                </table>
            </dd>
        </dl>
    </div>
    <p>
        @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
        @Html.ActionLink("Back to List", "Index")
    </p>
    

    如果粘贴代码后代码缩进错误,请按 Ctrl+KCtrl+D 设置其格式。

    此代码循环通过 Enrollments 导航属性中的实体。 对于 属性中的每个 Enrollment 实体,它显示课程标题和成绩。 课程标题是从 Course 实体的导航属性中 Course 存储的实体检索的 Enrollments 。 需要时,系统会自动从数据库中检索所有这些数据。 换句话说,你在此处使用延迟加载。 你没有为Courses导航属性指定预先加载,因此未在获取学生的同一查询中检索注册。 相反,当你第一次尝试访问 Enrollments 导航属性时,会向数据库发送一个新查询以检索数据。 可以在本系列后面的 读取相关数据 教程中详细了解延迟加载和预先加载。

  3. 打开“详细信息”页,方法是 (ctrl+F5) 启动程序,选择“ 学生 ”选项卡,然后单击“亚历山大·卡森 的详细信息” 链接。 (如果在 Details.cshtml 文件打开时按 Ctrl+F5,则会收到 HTTP 400 错误。这是因为 Visual Studio 尝试运行“详细信息”页,但未从指定要显示的学生的链接访问它。如果发生这种情况,请从 URL 中删除“学生/详细信息”并重试,或者关闭浏览器,右键单击项目,然后单击“在 Browser 中查看>视图”。)

    你会看到所选学生的课程和成绩列表。

  4. 关闭浏览器。

更新“创建”页

  1. Controllers\StudentController.cs 中,将 HttpPostAttributeCreate action 方法替换为以下代码。 此代码添加 块try-catch,并从基架方法的 属性中删除IDBindAttribute

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student)
    {
        try
        {
            if (ModelState.IsValid)
            {
                db.Students.Add(student);
                db.SaveChanges();
                return RedirectToAction("Index");
            }
        }
        catch (DataException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
        }
        return View(student);
    }
    

    此代码将 Student ASP.NET MVC 模型联编程序创建的实体添加到 Students 实体集,然后将更改保存到数据库。 模型绑定器 是指 ASP.NET MVC 功能,使你能够更轻松地处理表单提交的数据;模型联编程序将已发布的表单值转换为 CLR 类型,并将其传递给参数中的操作方法。 在这种情况下,模型联编程序使用集合中的Form属性值实例化Student实体。

    已从 Bind 属性中删除ID,因为 ID 是主键值,SQL Server插入行时自动设置该值。 用户的输入不会设置 ID 值。

    安全警告 - 属性 ValidateAntiForgeryToken 有助于防止 跨站点请求伪造 攻击。 它需要在视图中使用相应的 Html.AntiForgeryToken() 语句,稍后将看到该语句。

    属性 Bind 是一种在创建方案中防止 过度发布 的方法。 例如,假设 Student 实体包含 Secret 不希望此网页设置的属性。

    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
        public string Secret { get; set; }
    
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
    

    即使网页上没有 Secret 字段,黑客也可以使用 fiddler 等工具或编写一些 JavaScript 来发布 Secret 表单值。 BindAttribute如果没有 属性限制模型绑定器在创建Student实例时使用的字段模型绑定器将选取该Secret表单值并使用它来创建Student实体实例。 然后将在数据库中更新黑客为 Secret 表单字段指定的任意值。 下图显示了将值“OverPost”) 字段添加到 Secret 已发布的表单值 (fiddler 工具。

    显示“编辑器”选项卡的屏幕截图。在右上角,“执行”以红色圆圈。在右下角,Secret 等于 Over Post 以红色圆圈。

    然后值“OverPost”将成功添加到插入行的 Secret 属性,尽管你从未打算网页可设置该属性。

    最好将 参数与 属性结合使用 IncludeBind 显式列出字段。 还可以使用 Exclude 参数来阻止要排除的字段。 更安全的原因是 Include ,向实体添加新属性时,新字段不会自动受到 Exclude 列表的保护。

    在编辑方案中,可以通过先从数据库读取实体,然后调用 TryUpdateModel,然后传入显式允许的属性列表,从而防止过度发布。 这是这些教程中使用的 方法。

    许多开发人员首选的防止过度发布的另一种方法是使用具有模型绑定的视图模型而不是实体类。 仅包含想要在视图模型中更新的属性。 MVC 模型联编程序完成后,可以选择使用 AutoMapper 等工具将视图模型属性复制到实体实例。 使用 db。实体实例上的条目将其状态设置为“未更改”,然后将 Property (“PropertyName”) 。在视图模型中包含的每个实体属性上,IsModified 为 true。 此方法同时适用于编辑和创建方案。

    除了 Bind 属性之外, try-catch 块是你对基架代码所做的唯一更改。 如果保存更改时捕获到来自 DataException 的异常,则会显示一般错误消息。 有时 DataException 异常是由应用程序外部的某些内容而非编程错误引起的,因此建议用户再次尝试。 尽管在本示例中未实现,但生产质量应用程序会记录异常。 有关详细信息,请参阅监视和遥测(使用 Azure 构建真实世界云应用)中的“见解记录”部分。

    Views\Student\Create.cshtml 中的代码类似于 Details.cshtml 中的代码,只不过 EditorForValidationMessageFor 帮助程序用于每个字段而不是 DisplayFor。 下面是相关的代码:

    <div class="form-group">
        @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.LastName)
            @Html.ValidationMessageFor(model => model.LastName)
        </div>
    </div>
    

    Create.cshtml 还包括 @Html.AntiForgeryToken(),它与控制器中的 属性一起使用 ValidateAntiForgeryToken ,以帮助防止 跨站点请求伪造 攻击。

    Create.cshtml 中不需要更改。

  2. 通过启动程序,选择“ 学生 ”选项卡,然后单击“ 新建”来运行页面。

  3. 输入名称和无效日期,然后单击“ 创建 ”以查看错误消息。

    这是你默认获取的服务器端验证。 在后面的教程中,你将了解如何添加为客户端验证生成代码的属性。 以下突出显示的代码显示了 Create 方法中的模型验证检查。

    if (ModelState.IsValid)
    {
        db.Students.Add(student);
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    
  4. 将日期更改为有效值,并单击“创建”,查看“索引”页中显示的新学生。

  5. 关闭浏览器。

更新 HttpPost Edit 方法

  1. HttpPostAttributeEdit action 方法替换为以下代码:

    [HttpPost, ActionName("Edit")]
    [ValidateAntiForgeryToken]
    public ActionResult EditPost(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        var studentToUpdate = db.Students.Find(id);
        if (TryUpdateModel(studentToUpdate, "",
           new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
        {
            try
            {
                db.SaveChanges();
    
                return RedirectToAction("Index");
            }
            catch (DataException /* dex */)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
            }
        }
        return View(studentToUpdate);
    }
    

    注意

    Controllers\StudentController.csHttpGet Edit ,方法 (没有 HttpPost 属性) 方法 Find 检索所选 Student 实体,如方法中 Details 所示。 不需要更改此方法。

    这些更改实现安全最佳做法以防止 过度发布,基架生成了一个 Bind 属性,并将模型联编程序创建的实体添加到具有 Modified 标志的实体集中。 不再建议使用该代码,因为 属性 Bind 会清除 参数中未列出的 Include 字段中任何预先存在的数据。 将来,MVC 控制器基架将更新,以便它不会为 Edit 方法生成 Bind 属性。

    新代码读取现有实体,并调用 TryUpdateModel 以从已发布表单数据中的用户输入更新字段。 实体框架的自动更改跟踪设置 实体上的 EntityState.Modified 标志。 调用 SaveChanges 方法时, Modified 标志会导致实体框架创建 SQL 语句以更新数据库行。 将忽略并发冲突,并更新数据库行的所有列,包括用户未更改的列。 (后面的教程介绍如何处理并发冲突,如果只想更新数据库中的各个字段,可以将实体设置为 EntityState.Unchanged ,并将单个字段设置为 EntityState.Modified.)

    为防止过度发布,参数中 TryUpdateModel 列出了要通过“编辑”页更新的字段。 目前没有要保护的额外字段,但是列出希望模型绑定器绑定的字段可确保以后将字段添加到数据模型时,它们将自动受到保护,直到明确将其添加到此处为止。

    由于这些更改,HttpPost Edit 方法的方法签名与 HttpGet 编辑方法相同;因此,你已重命名了 EditPost 方法。

    提示

    实体状态以及 Attach 和 SaveChanges 方法

    数据库上下文跟踪内存中的实体是否与数据库中相应的行同步,并且此信息确定调用 SaveChanges 方法时会发生的情况。 例如,将新实体传递给 Add 方法时,该实体的状态设置为 Added。 然后,调用 SaveChanges 方法时,数据库上下文会发出 SQL INSERT 命令。

    实体可能处于以下 状态之一:

    • Added. 该实体尚不存在于数据库中。 方法 SaveChanges 必须发出 INSERT 语句。
    • Unchanged. 不需要通过 SaveChanges 方法对此实体执行操作。 从数据库读取实体时,实体将从此状态开始。
    • Modified. 已修改实体的部分或全部属性值。 方法 SaveChanges 必须发出 UPDATE 语句。
    • Deleted. 已标记该实体进行删除。 方法 SaveChanges 必须发出 DELETE 语句。
    • Detached. 数据库上下文未跟踪该实体。

    在桌面应用程序中,通常会自动设置状态更改。 在桌面类型的应用程序中,读取实体并对其某些属性值进行更改。 这将使其实体状态自动更改为 Modified。 然后,调用 SaveChanges时,实体框架将生成一个 SQL UPDATE 语句,该语句仅更新已更改的实际属性。

    Web 应用的断开连接性质不允许此连续序列。 读取实体的 DbContext 在呈现页面后释放。 HttpPostEdit调用操作方法时,会发出一个新请求,并且你有一个新的 DbContext 实例,因此必须在调用 SaveChanges时手动将实体状态设置为 Modified. Then,实体框架会更新数据库行的所有列,因为上下文无法知道你更改了哪些属性。

    如果希望 SQL Update 语句仅更新用户实际更改的字段,则可以以某种方式保存原始值, (如隐藏字段) ,以便在调用 方法时 HttpPostEdit 它们可用。 然后,可以使用原始值创建实体 Student ,使用实体的原始版本调用 Attach 方法,将实体的值更新为新值,然后调用 SaveChanges. 有关详细信息,请参阅 实体状态和 SaveChanges本地数据

    Views\Student\Edit.cshtml 中的 HTML 和 Razor 代码类似于在 Create.cshtml 中看到的代码,并且无需更改。

  2. 通过启动程序,选择“ 学生 ”选项卡,然后单击 “编辑” 超链接来运行页面。

  3. 更改某些数据并单击“保存”。 可以在“索引”页中看到已更改的数据。

  4. 关闭浏览器。

更新“删除”页

Controllers\StudentController.cs 中,方法的HttpGetAttributeDelete模板代码使用 Find 方法检索所选Student实体,如 和 Edit 方法中Details所示。 但是,若要在调用 SaveChanges 失败时实现自定义错误消息,请将部分功能添加到此方法及其相应的视图中。

正如所看到的更新和创建操作,删除操作需要两个操作方法。 为响应 GET 请求而调用的方法将显示一个视图,该视图使用户有机会批准或取消删除操作。 如果用户批准,则创建 POST 请求。 发生这种情况时,将 HttpPostDelete 调用 方法,然后该方法实际执行删除操作。

你将向 方法添加一个 try-catch 块, HttpPostAttributeDelete 以处理更新数据库时可能发生的任何错误。 如果发生错误,该方法 HttpPostAttributeDelete 将调用 HttpGetAttributeDelete 方法,并向其传递一个指示已发生错误的参数。 然后, HttpGetAttributeDelete 方法将重新显示确认页和错误消息,使用户有机会取消或重试。

  1. HttpGetAttributeDelete 操作方法替换为以下代码,用于管理错误报告:

    public ActionResult Delete(int? id, bool? saveChangesError=false)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        if (saveChangesError.GetValueOrDefault())
        {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
        }
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }
    

    此代码接受一个 可选参数 ,该参数指示是否在保存更改失败后调用了方法。 此参数是在falseHttpGetDelete调用 方法时没有之前失败的情况。 当 方法调用 HttpPostDelete 它以响应数据库更新错误时, 参数为 true ,并将错误消息传递给视图。

  2. HttpPostAttributeDelete 名为 DeleteConfirmed) 的操作方法 (替换为以下代码,该代码执行实际删除操作并捕获任何数据库更新错误。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(int id)
    {
        try
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
        }
        catch (DataException/* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
        }
        return RedirectToAction("Index");
    }
    

    此代码检索所选实体,然后调用 Remove 方法,将实体的状态设置为 Deleted。 调用 SaveChanges 时生成 SQL DELETE 命令。 你还将操作方法名称从 DeleteConfirmed 更改为了 Delete。 名为 方法DeleteConfirmed的基架代码,HttpPostDelete为方法提供HttpPost唯一签名。 (CLR 要求重载的方法具有不同的方法参数。) 现在签名是唯一的,可以坚持 MVC 约定,对 HttpPostHttpGet delete 方法使用相同的名称。

    如果提高大容量应用程序中的性能是重中之重,则可以通过将调用 FindRemove 方法的代码行替换为以下代码来避免不必要的 SQL 查询来检索行:

    Student studentToDelete = new Student() { ID = id };
    db.Entry(studentToDelete).State = EntityState.Deleted;
    

    此代码仅使用主键值实例化 Student 实体,然后将实体状态设置为 Deleted。 这是 Entity Framework 删除实体需要执行的所有操作。

    如前所述, HttpGetDelete 方法不会删除数据。 执行删除操作以响应 GET 请求 (或为此执行任何编辑操作、创建操作或任何其他更改数据的操作) 会产生安全风险。 有关详细信息,请参阅 ASP.NET MVC 提示 #46 — 不要使用删除链接,因为它们在 Stephen Walther 的博客上创建了安全漏洞。

  3. Views\Student\Delete.cshtml 中,在 h2 标题和 h3 标题之间添加错误消息,如以下示例所示:

    <h2>Delete</h2>
    <p class="error">@ViewBag.ErrorMessage</p>
    <h3>Are you sure you want to delete this?</h3>
    
  4. 通过启动程序,选择“ 学生 ”选项卡,然后单击 “删除” 超链接来运行页面。

  5. 显示是否确实要删除此内容?的页面上,选择“删除”。

    显示“索引”页时不显示已删除的学生。 (你将在 并发教程中看到运行中的错误处理代码的示例。)

关闭数据库连接

若要关闭数据库连接并尽快释放它们保留的资源,请在使用完上下文实例时释放它。 这就是为什么基架代码在 StudentController.cs 的类末尾StudentController提供 Dispose 方法的原因,如以下示例所示:

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        db.Dispose();
    }
    base.Dispose(disposing);
}

Controller 类已实现 IDisposable 接口,因此此代码只是将重写添加到 Dispose(bool) 方法以显式释放上下文实例。

处理事务

默认情况下,Entity Framework 隐式实现事务。 在对多个行或表进行更改,然后调用 SaveChanges的情况下,实体框架会自动确保所有更改都成功或全部失败。 如果完成某些更改后发生错误,这些更改会自动回退。 有关需要更多控制的方案(例如,如果要在事务中包含实体框架外部执行的操作),请参阅 使用事务

获取代码

下载已完成项目

其他资源

现在,你拥有一组完整的页面,用于对 Student 实体执行简单的 CRUD 操作。 你使用 MVC 帮助程序为数据字段生成 UI 元素。 有关 MVC 帮助程序的详细信息,请参阅 使用 HTML 帮助程序呈现窗体 (本文适用于 MVC 3,但仍与 MVC 5) 相关。

可以在 ASP.NET 数据访问 - 推荐资源中找到指向其他 EF 6 资源的链接。

后续步骤

在本教程中,你将了解:

  • 已创建“详细信息”页
  • 已更新“创建”页
  • 更新了 HttpPost Edit 方法
  • 已更新“删除”页
  • 已关闭数据库连接
  • 已处理事务

请继续学习下一篇文章,了解如何向项目添加排序、筛选和分页。