來個目錄吧:
第一章-入門
第二章- Entity Framework Core Nuget包管理
第三章-創建、修改、刪除、查詢
第四章-排序、過濾、分頁、分組
第五章-遷移,EF Core 的codefirst使用
暫時就這么多。后面陸續更新吧
創建、查詢、更新、刪除
這章主要講解使用EF完成 增刪改查的功能。
自定義“詳情信息”頁面
我們通過基架生成的代碼,沒有包含“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中
在最后一個標記之前,添加以下代碼以顯示登記列表:
<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();