來個目錄吧:
第一章-入門
第二章- Entity Framework Core Nuget包管理
第三章-建立、修改、刪除、查詢
第四章-排序、過濾、分頁、分組
第五章-遷移,EF Core 的codefirst使用
暫時就這麼多。後面陸續更新吧html
這章主要講解使用EF完成 增刪改查的功能。git
咱們經過基架生成的代碼,沒有包含「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>
打開「 Views/Students/Details.cshtml」 使用DisplayNameFor和DisplayFor顯示每一個字段,如如下示例所示:
<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」按鈕,能夠看到以下信息
在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)攻擊。
經過基架生成的代碼Create方法中包含了Bind特性是爲了防止發生overposting的一種狀況。
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」)
儘管你沒有從網頁上顯示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以查看錯誤消息。
這個是默認經過服務器端驗證,報錯的信息。在後面的教程中,會講解若是添加客戶端的驗證信息。
[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的白名單中了。
推薦的方法能夠保證,咱們只修改了能夠保證業務須要的字段,可是可能會引起併發衝突。他也增長了一次數據庫額外的查詢開銷。
如下是替代方法,可是咱們當前項目不要使用如下代碼。這裏只是做爲一個說明。
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語句。
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」選項卡,點擊「編輯」超連接。
更改一些數據,而後點擊保存按鈕。返回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,而且錯誤消息傳遞到視圖。
咱們修改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
咱們看生成的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();