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

相關文章
相關標籤/搜索