教程:在 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 中,默认路由映射 InstructorcontrollerIndex action 作为第 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存储在实体导航属性Enrollments中的Course实体中检索。 如果需要,将从数据库自动检索所有这些数据。 换句话说,你在此处使用延迟加载。 未为导航属性指定预先加载Courses,因此未在获取学生的同一查询中检索注册。 相反,首次尝试访问 Enrollments 导航属性时,会将新查询发送到数据库以检索数据。 可以在本系列后面的阅读相关数据教程中阅读有关延迟加载和急切加载的详细信息。

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

    可以看到所选学生的课程和成绩列表。

  4. 关闭浏览器。

更新“创建”页

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

    [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);
    }
    

    此代码将 ASP.NET MVC 模型绑定器创建的实体添加到StudentStudents实体集,然后将更改保存到数据库。 模型绑定器 是指 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 工具。

    显示“撰写器”选项卡的屏幕截图。在右上角,“执行”以红色圆圈。在右下角,机密等于 Post 以红色圆圈。

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

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

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

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

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

    Views\Student\Create.cshtml 中的代码类似于在 Details.cshtml 中看到的代码,但每个EditorFor字段和ValidationMessageFor帮助程序都用于而不是 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操作方法替换为以下代码:

    [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.cs 中, HttpGet Edit 方法(不含 HttpPost 属性的方法)使用 Find 方法检索所选 Student 实体,如方法 Details 所示。 不需要更改此方法。

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

    新代码读取现有实体,并调用 TryUpdateModel 从已发布的表单数据中的用户输入更新字段。 Entity Framework 的自动更改跟踪设置 实体上的 EntityState.Modified 标志。 调用 SaveChanges 方法时,标志Modified会导致 Entity Framework 创建 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时,Entity Framework 将生成一个 SQL UPDATE 语句,该语句仅更新更改的实际属性。

    Web 应用的断开连接性质不允许此连续序列。 在呈现页面后释放读取实体的 DbContextHttpPost Edit调用操作方法时,将发出新请求,并且拥有 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-catchHttpPostAttributeDelete,以处理更新数据库时可能发生的任何错误。 如果发生错误,该方法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);
    }
    

    此代码接受一个 可选参数,该参数 指示在保存更改失败后是否调用了该方法。 此参数是在 false HttpGet Delete 调用方法时没有之前失败的情况。 当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。 命名HttpPostDelete方法DeleteConfirmed的基架代码,为方法提供HttpPost唯一签名。 (CLR 要求重载的方法具有不同的方法参数。现在,签名是唯一的,可以坚持 MVC 约定,对和HttpGet删除方法使用相同的名称HttpPost

    如果提高大容量应用程序中的性能是一个优先级,可以通过将调用 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.cshtmlh2 ,在标题和 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的情况下,Entity Framework 会自动确保所有更改都成功或全部失败。 如果完成某些更改后发生错误,这些更改会自动回退。 对于需要更多控制的方案(例如,如果要在事务中包含实体框架外部执行的操作),请参阅 “使用事务”。

获取代码

下载已完成的项目

其他资源

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

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

后续步骤

本教程介绍以下操作:

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

转到下一篇文章,了解如何向项目添加排序、筛选和分页。