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

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

建立、查询、更新、删除

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

Paste_Image.png

Paste_Image.png

Paste_Image.png

Paste_Image.png

自定义“详情信息”页面

咱们经过基架生成的代码,没有包含“Enrollments”的属性,该导航属性是一个集合,因此咱们在详情信息页面,须要将他们显示到html表格中。程序员

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

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

而AsNoTracking方法在其中返回的实体信息,不存在在DbContext的生命周期中,他能够提升咱们的查询性能。AsNoTracking 在后面会额外说起。sql

路由数据

传递到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();

相关文章
相关标签/搜索