Contoso 大學示例 Web 應用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創建 ASP.NET Core 2.0 MVC Web 應用程序。 如欲了解更多本教程相關信息,請參閱 一、入門
在前面的教程中,您實現了 Table-per-Hierarchy 繼承。 本教程介紹幾個有用的知識點,當您掌握如何使用 Entity Framework Core 開發 ASP.NET Core Web 應用并進行部署的基礎知識后, 需要了解這些主題。
原始 SQL 查詢
使用 Entity Framework 的優點之一是,它避免了將代碼綁定到一個特定的數據存儲方法。它通過為您生成 SQL 查詢和命令來實現這一點,同時將你從SQL查詢編寫中解放出來。但是,總有一些意外的場景需要你運行手動創建的 SQL 查詢。 對于這些場景,EF Code First API 包含有讓您能夠將 SQL 命令直接傳遞到數據庫的方法。 在 EF Core 1 中有以下選項。
- 使用
Dbset.FromSql
返回實體類型的查詢方法。 返回的對象必須是 Dbset 對象期待的類型,然后這些數據將自動被數據上下文(Database Context)進行跟蹤,除非你手動 關閉跟蹤。 - 使用
Database.ExecuteSqlCommand
執行無查詢結果返回的命令。
如果你需要運行的查詢返回類型不是實體類型,你可以通過 EF 提供的數據庫連接使用 ADO.NET 。 返回的數據將不會被數據庫上下文跟蹤,即使你使用此方法檢索實體類型。
在 Web 應用程序中執行 SQL 命令時總是如此,您必須采取預防措施來保護您的站點免受 SQL 注入攻擊。 一種方法是使用參數化查詢來確保網頁提交的字符串不能被解釋為 SQL 命令。 在本教程中,您將在把用戶輸入集成到查詢中時使用參數化查詢。
調用返回實體的查詢
DbSet<Tentity>
類提供了一種方法,可以使用該方法執行返回類型為 TEntity 實體的查詢。 想要看它是如何工作的,你可以通過更改 Department 控制器的 Details 方法中的代碼來進行。
在 DepartmentsController.cs 文件, Details 方法中,用 FromSql 方法調用替換檢索部門的代碼,如以下突出顯示的代碼所示:
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}
為驗證新代碼是否工作正常,請選擇 Deparment 菜單,然后點擊其中任一部門的 Detail 鏈接。
調用返回其他類型的查詢
之前,您為關于頁面創建了一個學生統計表格,顯示每個日期的學生注冊人數。 您從學生實體集合(_context.Students)獲得了數據,并使用 LINQ 將結果投影到 EnrollmentDateGroup 視圖模型對象列表中。 現在假設你不想使用 LINQ 而想直接編寫 SQL 。要做到這一點,你需要運行一個 SQL 查詢,返回一些并非實體對象的東西。 在 EF Core 1.0 中,一種方法是從 EF 獲取數據庫連接并編寫 ADO.NET 代碼。
在 HomeController.cs 中,用下面的代碼替換 About 方法:
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}
添加了一行 using 聲明:
using System.Data.Common;
運行應用并轉到關于頁面。 它顯示與以前一樣的數據。
調用更新查詢
假設 Contoso 大學的管理員希望在數據庫中執行全局更改,例如更改每門課程的學分數。 如果大學有大量的課程,將它們全部作為實體進行檢索并單獨進行更改將是低效的。 在本節中,您將實現一個網頁,使用戶能夠指定一個因子來更改所有課程的學分數,然后通過執行 SQL UPDATE 語句來進行更改。 該網頁如下圖所示:
在 CoursesContoller.cs 中,為 HttpGet 和 HttpPost 添加 UpdateCourseCredits 方法:
public IActionResult UpdateCourseCredits()
{
return View();
}
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}
當控制器處理 HttpGet 請求時,ViewData [“RowsAffected”] 中將不返回任何內容,視圖顯示一個空的文本框和一個提交按鈕,如上圖所示。
當點擊 Update 按鈕時,HttpPost 方法被調用, 參數 multiplier 得到文本框中輸入的值。 然后代碼執行 SQL 更新 Courses 數據表,并將受影響的行數返回到 ViewData 供視圖使用。 當視圖獲得 RowsAffected 值時,顯示更新的行數。
在解決方案資源管理器中,右鍵單擊 Views/Courses 文件夾,然后單擊 添加>新建項。
在 添加新項 對話框中, 點擊左側面板中 “已安裝” 下的 “ASP.NET", 再點擊 "MVC View Page (中文:MVC 視圖頁)",并為新視圖命名 UpdateCourseCredits.cshtml 。
在 Views/Courses/UpdateCourseCredits.cshtml 中,用以下代碼替換模板代碼:
@{
ViewBag.Title = "UpdateCourseCredits";
}
<h2>Update Course Credits</h2>
@if (ViewData["RowsAffected"] == null)
{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default" />
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
通過點擊 Courses 鏈接,然后在地址欄 URL 后面加入 /UpdateCourseCredits
(例如: http://localhost:5813/Courses/UpdateCourseCredits) 的方法可以運行 UpdateCourseCredits 方法。 在文本框中輸入一個數字:
點擊 Update 。 你可以看到受影響的行數:
點擊 Back to List 查看修改學分后的課程列表。
請注意,在生產代碼中,須確保更新總是確保數據是有效的。 此處的簡化代碼可能導致學分數字大于 5 (通常學分有一個固定范圍 0-5 )。 更新查詢將可以正常工作,但無效的數據可能將導致系統的其他部分產生意外的結果(因為預先假設學分應該在 0-5 之間)。
有關如何使用原始 SQL 查詢的信息, 請參閱 Raw SQL Queries 。
檢查發送到數據庫的 SQL 語句
有時能夠看到發送到數據庫的實際 SQL 查詢會很有幫助。 EF Core 自動使用 ASP.NET Core 內置的日志記錄功能記錄查詢和更新中包含的 SQL 日志。 在本節中,您將看到一些SQL 日志記錄的例子。
打開 StudentsController.cs 并在 Details 方法 if (student == null)
語句上設置一個斷點。
在 Debug 模式下運行應用程序, 并轉至 學生
的 詳細
頁面。
轉到輸出顯示調試窗口輸出,你可以看到對應的查詢語句:
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
SELECT TOP(1) [s0].[ID]
FROM [Person] AS [s0]
WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]
你會注意到一些可能讓你感到意外的東西:SQL 從 Person 表中選擇最多 2 行(TOP(2))。 SingleOrDefaultAsync 方法并不是解析為服務器上的一行。 原因如下:
- 如果查詢可能返回多個行,該方法返回 null。
- 若要確定查詢是否將返回多個行,EF 必須檢查是否它返回至少為 2。
請注意,您不必使用調試模式并在斷點處停止以在“輸出”窗口中獲取日志輸出。這只是一個方便的方法來停止日志記錄在你想看看輸出點。 如果你不這樣做,日志繼續輸出,你必須向前滾動滾動條才能找到你感興趣的部分。
倉儲和單元工作模式
許多開發人員編寫代碼來實現倉儲和單元工作模式,作為與 Entity Framework 一起工作的包裝器。 這些模式旨在創建應用程序中數據訪問層和業務邏輯層之間的抽象層。實現這些模式可以幫助您將應用程序與數據存儲區中的更改隔離開來,并且可以促進自動化單元測試或測試驅動開發(TDD)。但是,編寫額外的代碼來實現這些模式并不總使用 EF 開發應用程序的最佳選擇,原因如下:
- EF 上下文類本身已經將你的代碼與特定的數據存儲代碼分離開了。
- EF 上下文類也可以充當
Unit of Work
類來更新數據庫。 - EF 包含實現 TDD 的特性,無需編寫倉儲庫代碼。
有關如何實現倉儲和單元工作模式, 請參閱 the Entity Framework 5 Version of this tutorial series. 。
Entity Framework Core 實現了一個內存數據庫提供程序。有關更多信息, 請參閱 Testing with InMemory 。
自動更改檢測
實體框架通過將實體的當前值與原始值進行比較來確定實體如何改變(以及因此需要將哪些更新發送到數據庫)。 原始值在實體被查詢或附加時被存儲。 一些導致自動更改檢測的方法如下:
- DbContext.SaveChagnes
- DbContext.Entry
- ChangeTracker.Entries
如果您正在跟蹤大量實體,并且您在循環中多次調用其中一個方法,則可以通過使用 ChangeTracker.AutoDetectChangesEnabled 屬性臨時關閉自動更改檢測來顯著提高性能。例如:
_context.ChangeTracker.AutoDetectChangesEnabled = false;
Entity Framework Core 源代碼及開發計劃
Entity Framework Core 的源代碼托管在 https://github.com/aspnet/EntityFrameworkCore 。 源碼倉庫中包含 夜間構建, 問題跟蹤, 功能規范, 設計會議記錄以及未來發展的路線圖。 你可以提交或發行錯誤,并作出貢獻。
盡管源代碼是開放的,但 Entity Framework Core 得到了微軟的全面支持。 微軟 Entity Framework Core 團隊保持對貢獻的控制,哪些可以被接受,并對所有的代碼修改進行測試,以確保每個發行版的質量。
從現有數據庫進行逆向工程
要從現有數據庫中反向得到數據模型和實體類, 使用 scaffold-dbcontext
命令, 請參閱 getting-started tutorial / 入門教程。
使用 dynamic LINQ 來簡化排序選擇代碼
在 本教程第三章 中, 展示了如何通過在 switch 語句中對列名進行硬編碼來編寫 LINQ 代碼。 此處只有兩列可供選擇,可以正常工作,但當你有很多列時,代碼可能會變得冗長。 為了解決這個問題,可以使用 EF.Property 方法以字符串的形式指定屬性的名稱。 要嘗試這種方法,請使用以下代碼替換 StudentsController 中的 Index 方法。
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? page)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
bool descending = false;
if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}
if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}
int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
page ?? 1, pageSize));
}
后續步驟
本篇完成了 如何使用 Entity Framewrok Core 創建 ASP.NET MVC Web 應用程序 的教程。
如欲了解有關 EF Core 的更多信息, 請參閱 Entity Framework Core 文檔 。 還有一本書可供選擇: Entity Framework in Action 。
有關如何部署一個 WEB 應用,請參閱 Host and depoly 。
與 ASP.NET Core MVC 相關的其他主題(如身份驗證和授權)的信息,請參閱 ASP.NET Core 文檔。
致謝
Tom Dykstra 和 Rick Anderson(twitter @RickAndMSFT)編寫了這個教程。 Rowan Miller,Diego Vega 和 Entity Framework 團隊的其他成員協助代碼審查,并幫助調試
在編寫教程代碼時出現的問題。
常見錯誤
ContosoUniversity.dll used by another process
錯誤信息:
Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process.
解決方案:
停止 IIS Express 中的站點。 找到 Windows 系統托盤中的 IIS Express ,右鍵點擊圖標,選擇 Contoso University 站點,單擊停止站點。
Migration scaffolded with no code in Up and Down methods
可能的原因:
EF CLI 命令不會自動關閉和保存文件。 如果你在運行 migrations add 命令時有未保存的修改, EF 將找不到你所做的修改。
解決方案:
運行 migrations remove
命令, 保存你的代碼修改,然后重新運行 migrations add
命令。
Errors while running database update(運行數據庫更新命令時出錯)
在已有數據的數據庫中進行架構更改,可能會發生其他錯誤。如果遇到無法解析的遷移錯誤,則可以更改連接字符串中的數據庫名稱或刪除數據庫。 使用新的數據庫,不需要遷移任何數據,update-database 命令更可能完成而不會出錯。
最簡單的方法是在appsettings.json中重命名數據庫。 下一次運行數據庫更新時,將創建一個新的數據庫。
要在 SSOX 中刪除一個數據庫, 右鍵點擊數據庫,單擊 刪除
, 在 刪除數據庫
對話框中選項關閉現有連接,然后單擊 確定
。
若要使用 CLI 刪除數據庫,運行 database drop
CLI 命令:
dotnet ef database drop
Error locating SQL Server instance(無法定位SQL Server 實例)
錯誤信息:
A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)
解決方案:
檢查連接字符串。 如果您手動刪除了數據庫文件,則更改構造字符串中的數據庫名稱以重建新的數據庫。