基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-3

来个目录吧:
第一章-入门
第二章- Entity Framework Core Nuget包管理
第三章-创建、修改、删除、查询
第四章-排序、过滤、分页、分组
第五章-迁移,EF Core 的codefirst使用
暂时就这么多。后面陆续更新吧

创建、查询、更新、删除

这章主要讲解使用EF完成 增删改查的功能。

Paste_Image.png

Paste_Image.png

Paste_Image.png

Paste_Image.png

自定义“详情信息”页面

我们通过基架生成的代码,没有包含“Enrollments”的属性,该导航属性是一个集合,所以我们在详情信息页面,需要将他们显示到html表格中。

在Controllers / StudentsController.cs中,详细信息视图的操作方法使用该SingleOrDefaultAsync方法查询单个Student实体。添加Include、ThenInclude,和AsNoTracking方法,如下面突出显示的代码所示。

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var student = await _context.Students
        .Include(s => s.Enrollments)
            .ThenInclude(e => e.Course)
        .AsNoTracking()
        .SingleOrDefaultAsync(m => m.ID == id);

    if (student == null)
    {
        return NotFound();
    }

    return View(student);
}

Include 和 ThenInclude 两个方法会让Context去额外加载Student的导航属性Enrollments,和Enrollments的导航属性Course。

而AsNoTracking方法在其中返回的实体信息,不存在在DbContext的生命周期中,他可以提高我们的查询性能。AsNoTracking 在后面会额外提及。

路由数据

传递到Details方法中的参数信息,是通过路由控制的。路由是数据从模型绑定中获取到的URL。例如,默认路由指定Controller、Action和id来组成。

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");//手动高亮
    });

    DbInitializer.Initialize(context);
}

在下面的URL中,路由将由Instructor作为控制器,Index作为操作,1作为指定id;

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

URL的最后一部分(“?courseID = 2021”)是一个查询字符串值。如果将其作为查询字符串值传递,则模型绑定器还会将ID值传递给Details方法id参数:

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

在Index页面中,超链接是由Razor视图中的标记语句创建的,在下面的Razor代码中,id参数作为默认路由相匹配,因此id会添加到“asp-route-id”中。

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>

在以下的代码中,studentID与默认的路由参数不匹配,因此将会被作为添加查询操作。

<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

将enrollments 添加到“详情信息”页面中

打开“ Views/Students/Details.cshtml” 使用DisplayNameForDisplayFor显示每个字段,如以下示例所示:

<dt>
    @Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
    @Html.DisplayFor(model => model.LastName)
</dd>

需要你在Details.cshtml中
在最后一个标记之前,添加以下代码以显示登记列表:

<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>

以上代码会循环Enrollments导航属性中的所有实体信息。显示出每个学生登记了的课程名称、成绩信息。课程标题是通过Enrollments的导航属性Course显示出来。

运行程序, 选择student 菜单,然后再选择“Details”按钮,可以看到如下信息

Paste_Image.png

修改创建页面

SchoolController中,修改标记了HttpPost特性的Create方法,添加一个try-catch块,并且从Bind特性中将“ID”参数删除掉。

  [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
        [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    _context.Add(student);
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
            }
            catch (DbUpdateException /* ex */)
            {
                //错误日志(可以在这里记录错误的变量名称,把他写到日志文件中)
                //Log the error (uncomment ex variable name and write a log.
                ModelState.AddModelError("", $"信息无法保存更改,请再试一次, 如果问题依然存在。可以联系你的系统管理员 - 角落的白板笔");
            }
            return View(student);
        }
  • 以上代码是指 由ASP.NET MVC的模型,绑定创建的一个Student实体添加到Students实体集合中,然后将发生的更改保存到数据库中。

  • 而需要将ID从Bind特性中删除,是因为ID为主键值,SQL Server将在插入行时自动递增该值。不需要用户进行ID设置。

  • 除了Bind特性之外,添加的try-catch块是对代码做的额外的变动,如果DbUpdateException在保存更改时捕获到异常,则会显示一个通用错误消息。DbUpdateException异常有时是由程序外部的某些东西引起的,而不是程序本身错误,因此建议用户重试。

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

关于 overposting(过多发布)的安全注意

通过基架生成的代码Create方法中包含了Bind特性是为了防止发生overposting的一种情况。

  • 举个栗子:假如学生实体包含 了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; }
}

overposting发生的情况就是,即使你的网页上没有Secret字段,但是黑客可以通过某些工具(如:findder)或者用JavaScript点,发布一个form表单请求。里面包含了Secret字段。
如果你没有Bind特性的话,就会创建一个含有Secret的Student实体信息,然后黑客伪造的值就会更新到数据库中。
下图,展示了使用Fiddler工具,给Secret字段赋值,发送请求到数据库中。(值为:“OverPost”)

Paste_Image.png

尽管你没有从网页上显示Secret字段,但是黑客通过工具,强行将值赋予了“Secret”。

使用带有Include的Bind特性来把参数列入白名单是一种最佳的方法。当然也可以使用Exclude参数来将字段排除除去作为黑名单,也可以实现。但是使用Exclude的问题是如果添加了新字段默认会被排除,不会被保护。所以最佳的做法还是使用Include的做法。

本教程中,使用了在编辑的时候先从数据库中查询实体,然后再调用TryUpdateModel方法,然后传递允许的属性列表,来防止overposting。

另一种防止overposting的方法是许多开发人员所接受的,它使用视图模型而不是直接使用实体类。 仅在视图模型中包含要更新的属性。 一旦MVC模型绑定完成,将视图模型属性复制到实体实例,可选地使用AutoMapper等工具。 使用实体实例上的_context.Entry将其状态设置为Unchanged,然后在视图模型中包含的每个实体属性上设置Property(“PropertyName”)IsModified为true。 此方法适用于编辑和创建场景。

作为优秀的程序员,尽量使用DTO,也就是上面说的viewmodel(视图模型),而不是使用实体。DTO的优点以后我们有机会再说。

修改创建视图页面

在路径“/Views/Students/Create.cshtml”,使用label,input,span标签(目的是为了做验证)帮助完善每个字段。

通过选择“Students”选项卡,点击“Create”运行该页面。

输入无效的时间,然后点击Create以查看错误消息。

Paste_Image.png

这个是默认通过服务器端验证,报错的信息。在后面的教程中,会讲解如果添加客户端的验证信息。

  [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
        [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
        {
            try
            {
                if (ModelState.IsValid) //手动高亮,这里就是在做字段验证信息
                {
                    _context.Add(student);
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
            }
            catch (DbUpdateException /* ex */)
            {
                //错误日志(可以在这里记录错误的变量名称,把他写到日志文件中)
                //Log the error (uncomment ex variable name and write a log.
                ModelState.AddModelError("", $"信息无法保存更改,请再试一次, 如果问题依然存在。可以联系你的系统管理员 - 角落的白板笔");
            }
            return View(student);
        }

只需要将日期修改为正确的值,然后点击Create就可以添加信息成功。

修改编辑功能

SchoolController.cs文件中,HttpGet 特性的Edit方法(没有HttpPost属性的SingleOrDefaultAsync方法)该方法是搜索所选的学生实体,就像您在Details方法中看到的一样。您不需要更改此方法。

我们需要替换的是标记了HttpPost特性 的Edit方法代码为以下代码。

 [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> EditPost(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }
            var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
            if (await TryUpdateModelAsync<Student>(
                studentToUpdate,
                "",
                s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
                catch (DbUpdateException /* ex */)
                {
                     //错误日志(可以在这里记录错误的变量名称,把他写到日志文件中)
                    ModelState.AddModelError("", $"信息无法保存更改,请再试一次, 如果问题依然存在。可以联系你的系统管理员 - 角落的白板笔");

                }
            }
            return View(studentToUpdate);
        }

  • 上面的修改内容,我们一个个慢慢的说,目的就是为了防止overposting,采用了bind包含白名单的方法来进行参数传递。这是一种最佳的安全做法。

  • 新的代码会读取现有的实体,并执行TryUpdateModel方法,这里是mvccore的框架使用了taghelper语法,将页面上的Student实体信息做了更新。然后
    EF框架会自动更改实体状态为Modifed。然后当我们执行SaveChange的时候,EF会创建sql语句来更新数据到数据库中。(这里没有考虑并发冲突,我们后面再来解决这个问题)

  • 作为防止overposting的最佳做法,你在“Edit”视图页面中,显示的字段已经更新到了TryUpdateModel的白名单中了。

替代原HttpPost Edit方法

推荐的方法可以保证,我们只修改了可以保证业务需要的字段,但是可能会引发并发冲突。他也增加了一次数据库额外的查询开销。

以下是替代方法,但是我们当前项目不要使用以下代码。这里只是作为一个说明。

public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
{
    if (id != student.ID)
    {
        return NotFound();
    }
    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(student);
            await _context.SaveChangesAsync();
            return RedirectToAction("Index");
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    return View(student);
}

上面的方法是网页需要更新所有字段的时候,可以上面的方法,否则建议不考虑。

实体状态

数据库上下文跟踪内存中的实体是否和数据库的一致,并由此来确定在调用SaveChanges方法的时候进行何种操作。例如:当新的 实体传递给add方法的时候,该实体的状态将被设置为Added。然后调用SaveChange方法的时候,数据库上下文会发Sql inser命令。

实体状态可能有以下的状态:

  • Added。实体尚不在数据库中,执行SaveChange方法的时候发出Insert语句。

  • **Unchanged*。执行SaveChange方法的时候,不会对此实体进行任何操作。当你
    从数据库查询某个实体的时候,实体的状态就是从它开始的。

  • Modified。 实体的部分或者全部属性被修改的时候。调用SaveChange方法会发出Update 语句。

  • Deleted。表示实体已经被标记为删除状态。调用SaveChange方法会发出Delete语句。

  • Detached。该实体没有被数据库上下文跟踪。

在桌面程序中(C/S),状态更改通常会自动设置。您读取实体并更改某些字段的时候。这将导致其实体状态自动更改为Modified。然后调用SaveChanges时,Entity Framework生成一个SQL UPDATE语句,修改你实体的更改字段值。

在webapp开发中。DbContext读取实体并显示其要编辑的数据库展现在页面上,当发送Post请求到Edit方法的时候,会创建一个新的web请求,并创建一个新的DbContext,如果你在新上下文中重新获取实体,整个请求过程类似桌面处理。

但是如果你不想做额外的查询操作,你必须使用由model-binder创建的实体对象。最简单的方法是将实体状态设置为modifed,就像之前显示的HttpPost编辑代码中所做的那样。然后当调用SaveChanges时,Entity Framework会更新数据库行的所有字段信息,因为数据库上下文无法知道您更改了哪些属性。

如果想避免read-first方法,但是希望使用SQLUupdate语句来更新用户实际想更改的字段,代码会更加的复杂。你必须以某种方式保存原始值(例如,通过隐藏字段),以便调用post请求的edit方法的时候可以用。然后,可以使用原始值创建一个Student实体信息。调用Attach该实体的原始方法,将实体的值更新为新值,最后调用SaveChange。

测试编辑页面

运行应用程序并选择“Student”选项卡,点击“编辑”超链接。

Paste_Image.png

更改一些数据,然后点击保存按钮。返回Index视图页面,可以看到更改的数据。

修改删除页面

StudentController.cs文件中,HttpGet请求的Delete方法中使用了

SingleOrDefaultAsync

来查询实体,与“Detail”和“Editor”视图页面一样。但是为了调用SaveChange失败的时候实现一些自定义错误信息,我们需要向此方法和视图添加一些代码。

删除功能与编辑和创建功能一样,需要操作两个方法。相应Get请求去调用方法显示一个视图,该视图为用户提供一个删除或者取消的操作按钮。
如果用户同意的话,则会创建一个POST请求。然后就会调用Post的Delete方法,然后执行方法删除掉他。

我们将会对HttpPost特性下 的Delete方法添加一个try-catch块,以便显示处理数据库修改的时候发生的错误。

修改HttpPost特性的Delete代码如下:

···

    // GET: Students/Delete/5
    public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
    {
        if (id == null)
        {
            return NotFound();
        }

        var student = await _context.Students
            .AsNoTracking()
            .SingleOrDefaultAsync(m => m.ID == id);
        if (student == null)
        {
            return NotFound();
        }

        if (saveChangesError.GetValueOrDefault())
        {
            ViewData["ErrorMessage"] =
                $"删除{student.LastName}信息失败,请再试一次, 如果问题依然存在。可以联系你的系统管理员 - 角落的白板笔";
        }

        return View(student);
    }

···

此代码增加了一个可选参数,该参数指示在保存更改失败后是否调用该方法。当在Delete没有失败的情况下,调用HttpGet 方法时,此参数为false 。当HttpPost的 Delete方法执行数据库更新错误而调用它时,参数为true,并且错误消息传递到视图。

HttpPost的read-first的删除方法

我们修改DeleteConfirmed方法的代码,如下:

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var student = await _context.Students
        .AsNoTracking()
        .SingleOrDefaultAsync(m => m.ID == id);
    if (student == null)
    {
        return RedirectToAction("Index");
    }

    try
    {
        _context.Students.Remove(student);
        await _context.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction("Delete", new { id = id, saveChangesError = true });
    }
}


此代码先搜索选定的实体,然后调用Remove将实体的状态修改为Deleted。当SaveChanges调用时,将生成SQL DELETE命令。

另外的一种写法

如果程序需要提高性能作为优先级考虑,可以参考一下的代码。他是仅仅通过Id主键
实例化Student实体,然后通过更改实体的状态值来避免sql查询,然后来删除实体信息(
这段代码不要放到项目中去,只作为参考。)

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    try
    {
        Student studentToDelete = new Student() { ID = id };
        _context.Entry(studentToDelete).State = EntityState.Deleted;
        await _context.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction("Delete", new { id = id, saveChangesError = true });
    }
}

如果实体具有应删除的相关数据,请确保在数据库中配置开启级联删除。上面通过这种实体删除的方法,EF可能不会删除的相关实体。

修改“删除”视图

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

<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>

单击“ 删除”。将显示“Index”页面,但没有删除的学生。(您将在并发教程中看到一个错误处理代码的示例。)

关闭数据库连接

要释放数据库连接所拥有的资源,必须在完成上下文实例后尽快处理该上下文实例。
ASP.NET Core内置依赖注入为您完成此任务。

Startup.cs中,您调用AddDbContext扩展方法以DbContext在ASP.NET DI容器中配置类。默认服务生命周期设置为Scoped意味着上下文对象生存期与Web请求生命周期一致,并且该Dispose方法将在Web请求结束时自动调用。

事务处理

默认情况下,Entity Framework默认实现事务。
在您对多个行或表进行更改然后调用的情况下SaveChanges,Entity Framework会自动确保所有更改都成功或全部失败。
如果先执行某些更改,然后发生错误,那么这些更改会自动回滚。
对于需要更多控制的方案 - 例如,如果要在事务中包括在Entity Framework之外完成的操作 - 请参阅事务

无跟踪查询 AsNoTracking

这里我就不翻译了,自己摘录了博客园的实例

性能提升之AsNoTracking

我们看生成的sql

sql是生成的一模一样,但是执行时间却是4.8倍。原因仅仅只是第一条EF语句多加了一个AsNoTracking。
注意:
AsNoTracking干什么的呢?无跟踪查询而已,也就是说查询出来的对象不能直接做修改。所以,我们在做数据集合查询显示,而又不需要对集合修改并更新到数据库的时候,一定不要忘记加上AsNoTracking。
如果查询过程做了select映射就不需要加AsNoTracking。如:db.Students.Where(t=>t.Name.Contains("张三")).select(t=>new (t.Name,t.Age)).ToList();

posted @ 2017-03-09 17:20  梁桐铭  阅读(6880)  评论(1编辑  收藏  举报