在 ASP.NET MVC 应用程序中使用实体框架实现基本 CRUD 功能(共 10 个)

作者:Tom Dykstra

Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 创建 ASP.NET MVC 4 应用程序。 若要了解系列教程,请参阅本系列中的第一个教程

注意

如果遇到无法解决的问题, 请下载已完成的章节 ,并尝试重现问题。 通常可以通过将代码与已完成的代码进行比较来查找问题的解决方案。 有关一些常见错误以及如何解决这些问题,请参阅 错误和解决方法。

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

注意

为了在控制器和数据访问层之间创建一个抽象层,常见的做法是实现存储库模式。 若要使这些教程保持简单,除非本系列教程的后续教程才能实现存储库。

在本教程中,你将创建以下网页:

显示 Contoso 大学生详细信息页的屏幕截图。

显示 Contoso 大学生编辑页面的屏幕截图。

显示 Contoso 大学生创建页面的屏幕截图。

显示“学生删除”页的屏幕截图。

创建详细信息页

学生 Index 页的基架代码将排除该 Enrollments 属性,因为该属性保存集合。 在 Details 页面中,你将在 HTML 表中显示集合的内容。

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

public ActionResult Details(int id = 0)
{
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

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

  1. 打开 Views\Student\Details.cshtml。 每个字段都使用 DisplayFor 帮助程序显示,如以下示例所示:

    <div class="display-label">
             @Html.DisplayNameFor(model => model.LastName)
        </div>
        <div class="display-field">
            @Html.DisplayFor(model => model.LastName)
        </div>
    
  2. EnrollmentDate 字段之后,紧接在结束 fieldset 标记之前,添加代码以显示注册列表,如以下示例所示:

    <div class="display-label">
            @Html.LabelFor(model => model.Enrollments)
        </div>
        <div class="display-field">
            <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>
        </div>
    </fieldset>
    <p>
        @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) |
        @Html.ActionLink("Back to List", "Index")
    </p>
    

    此代码循环通过 Enrollments 导航属性中的实体。 Enrollment对于属性中的每个实体,它显示课程标题和成绩。 课程标题从Course存储在实体导航属性Enrollments中的Course实体中检索。 如果需要,将从数据库自动检索所有这些数据。 (换句话说,你在这里使用懒惰加载。未为导航属性指定预先加载Courses,因此首次尝试访问该属性时,会向数据库发送查询以检索数据。可以在本系列后面的阅读相关数据教程中阅读有关延迟加载和急切加载的详细信息。

  3. 通过选择“ 学生 ”选项卡并单击 亚历山大·卡森的详细信息 链接来运行页面。 将看到所选学生的课程和年级列表:

    Student_Details_page

更新“创建”页

  1. Controllers\StudentController.cs 中,将HttpPost``Create操作方法替换为以下代码,将块和 Bind 属性添加到try-catch基架方法:

    [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 after DataException 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实体。

    ValidateAntiForgeryToken 属性有助于防止 跨站点请求伪造 攻击。

> [!WARNING]
    > Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
    > 
    > [!code-csharp[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample5.cs?highlight=7)]
    > 
    > Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/library/system.web.mvc.bindattribute(v=vs.108).aspx) attribute limiting the fields that the model binder uses when it creates a `Student` instance*,* the model binder would pick up that `Secret` form value and use it to update the `Student` entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
    > 
    > ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)  
    > 
    > The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to update that property.
    > 
    > It's a security best practice to use the `Include` parameter with the `Bind` attribute to *allowed attributes* fields. It's also possible to use the `Exclude` parameter to *blocked attributes* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
    > 
    > Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.

    Other than the `Bind` attribute, the `try-catch` block is the only change you've made to the scaffolded code. If an exception that derives from [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).

    The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:

    [!code-cshtml[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample6.cshtml)]

    *Create.cshtml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.

    No changes are required in *Create.cshtml*.
  1. 通过选择“学生”选项卡并单击“新建来运行页面。

    Student_Create_page

    某些数据验证默认有效。 输入名称和无效日期,然后单击“创建以查看错误消息。

    Students_Create_page_error_message

    以下突出显示的代码显示了模型验证检查。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Student student)
    {
        if (ModelState.IsValid)
        {
            db.Students.Add(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(student);
    }
    

    将日期更改为 2005 年 9 月 1 日等有效值,然后单击“创建”以查看新学生显示在“索引”页中

    Students_Index_page_with_new_student

更新“编辑 POST”页

Controllers\StudentController.cs 中,HttpGetEdit方法(没有HttpPost特性的方法)使用Find方法检索所选Student实体,如方法Details所示。 不需要更改此方法。

但是,将HttpPostEdit操作方法替换为以下代码以添加try-catch块和 Bind 属性

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
   Student student)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(student).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException 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);
}

此代码类似于你在方法中看到HttpPostCreate的内容。 但是,此代码不是将模型绑定器创建的实体添加到实体集,而是在实体上设置一个标志,指示该实体已更改。 调用 SaveChanges 方法时,修改后的标志会导致 Entity Framework 创建 SQL 语句以更新数据库行。 将更新数据库行的所有列,包括用户未更改的列,并忽略并发冲突。 (你将了解如何在本系列的后续教程中处理并发。

实体状态和 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. 有关详细信息,请参阅 MSDN Data Developer Center 中的实体状态和 SaveChanges本地数据

Views\Student\Edit.cshtml 中的代码类似于 Create.cshtml 中看到的代码,无需更改。

通过选择“ 学生 ”选项卡并单击“ 编辑 ”超链接来运行页面。

Student_Edit_page

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

Students_Index_page_after_edit

更新“删除”页

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

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

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

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

    public ActionResult Delete(bool? saveChangesError=false, int id = 0)
    {
        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. HttpPostDelete操作方法(命名DeleteConfirmed)替换为以下代码,该代码执行实际删除操作并捕获任何数据库更新错误。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(int id)
    {
        try
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
        }
        catch (DataException/* dex */)
        {
            // uncomment dex and log error. 
            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() { StudentID = 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>
    

    通过选择“ 学生 ”选项卡并单击“ 删除 ”超链接来运行页面:

    Student_Delete_page

  4. 单击“删除”。 将显示不含已删除学生的索引页。 (你将在操作中看到错误处理代码的示例 本系列后面的处理并发 教程。

确保数据库连接未保持打开状态

为了确保数据库连接已正确关闭,并且释放了它们的资源,应会看到上下文实例已释放。 这就是为什么基架代码在StudentController.cs类末尾StudentController提供 Dispose 方法的原因,如以下示例所示:

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

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

总结

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

在下一教程中,你将通过添加排序和分页来扩展索引页的功能。

可以在 ASP.NET 数据访问内容映射中找到指向其他 Entity Framework 资源的链接。