• 基于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();

  • 相关阅读:
    SQL Azure (17) SQL Azure V12
    Microsoft Azure News(5) Azure新DV2系列虚拟机上线
    Azure Redis Cache (3) 在Windows 环境下使用Redis Benchmark
    Azure PowerShell (11) 使用自定义虚拟机镜像模板,创建Azure虚拟机并绑定公网IP(VIP)和内网IP(DIP)
    Windows Azure Virtual Machine (31) 迁移Azure虚拟机
    Windows Azure Web Site (16) Azure Web Site HTTPS
    Azure China (12) 域名备案问题
    一分钟快速入门openstack
    管理员必备的Linux系统监控工具
    Keepalived+Nginx实现高可用和双主节点负载均衡
  • 原文地址:https://www.cnblogs.com/wer-ltm/p/6526689.html
Copyright © 2020-2023  润新知