來個目錄吧: 第一章-入門 第二章- Entity Framework Core Nuget包管理 第三章-建立、修改、删除、查詢 第四章-排序、過濾、分頁、分組 第五章-遷移,EF Core 的codefirst使用 暫時就這麼多。後面陸續更新吧
建立、查詢、更新、删除
這章主要講解使用EF完成 增删改查的功能。

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” 使用DisplayNameFor和DisplayFor顯示每個字段,如以下示例所示:
<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”按鈕,可以看到如下資訊
修改建立頁面
在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”)
盡管你沒有從網頁上顯示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的白名單中了。
替代原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”頁籤,點選“編輯”超連結。
更改一些資料,然後點選儲存按鈕。傳回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();