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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM