基于Asp.Net Core Mvc和EntityFramewo

作者: 角落的白板笔 | 来源:发表于2017-03-09 17:13 被阅读2210次

    来个目录吧:
    第一章-入门
    第二章- 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中
    在最后一个</dl>标记之前,添加以下代码以显示登记列表:

    <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();

    相关文章

      网友评论

      • Steven_e30d:请问有源码吗
        角落的白板笔:@Steven_e30d 访问地址:https://school.yoyocms.com/
        github地址:https://github.com/52ABP/52ABP.School

      本文标题:基于Asp.Net Core Mvc和EntityFramewo

      本文链接:https://www.haomeiwen.com/subject/nwqigttx.html