共用方式為


教學課程:瞭解進階案例 - 使用 EF Core ASP.NET MVC

在上一個教學課程中,您已實作每個階層的數據表繼承。 本教學課程介紹數個值得注意的主題,當您進階使用 Entity Framework Core 開發 ASP.NET Core Web 應用程式時,這些主題將很有用。

在本教學課程中,您會:

  • 執行原始 SQL 查詢
  • 執行查詢以傳回實體
  • 執行查詢以傳回其他類型的結果
  • 執行更新查詢
  • 檢查 SQL 查詢
  • 建立抽象層
  • 了解自動變更偵測
  • 瞭解 EF Core 原始程式碼和開發計劃
  • 瞭解如何使用動態 LINQ 來簡化程式碼

先決條件

執行原始 SQL 查詢

使用 Entity Framework 的優點之一是,它避免將程式代碼與儲存數據的特定方法緊密結合。 其方式是為您產生 SQL 查詢和命令,這也可讓您不必自行撰寫它們。 但當您需要執行手動建立的特定 SQL 查詢時,會有例外狀況。 在這些案例中,Entity Framework Code First API 包含方法,可讓您將 SQL 命令直接傳遞至資料庫。 您在 EF Core 1.0 中有下列選項:

  • 針對傳回實體類型的查詢,請使用 DbSet.FromSql 方法。 傳回的對象必須是 DbSet 物件所預期的型別,除非您 關閉追蹤,否則資料庫內容會自動追蹤這些物件。

  • 針對非查詢命令使用 Database.ExecuteSqlCommand

如果您需要執行傳回非實體類型的查詢,您可以使用 ADO.NET 搭配 EF 所提供的資料庫連線。 即使使用此方法來擷取實體類型,傳回的數據也不會由資料庫內容追蹤。

如同在 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()
        .FirstOrDefaultAsync();

    if (department == null)
    {
        return NotFound();
    }

    return View(department);
}

若要確認新程式代碼正常運作,請選取 [部門] 標籤頁,然後選取 [其中一個部門的詳細數據]。

部門詳細數據

呼叫一個查詢以返回其他類型的数据或結果。

稍早,您已為 [關於] 頁面建立學生統計數據方格,其中顯示每個註冊日期的學生數目。 您從 Students 實體集取得資料(_context.Students),並使用 LINQ 將結果投影到 EnrollmentDateGroup 檢視模型物件清單中。 假設您想要撰寫 SQL 本身,而不是使用 LINQ。 若要這樣做,您必須執行 SQL 查詢,以傳回不包含實體物件的其他內容。 在 EF Core 1.0 中,其中一種方法是撰寫 ADO.NET 程式代碼,並從 EF 取得資料庫連線。

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 語句來進行變更。 網頁看起來會像下圖所示:

更新課程點數頁面

CoursesController.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 方法,而乘數為在文字框中輸入的值。 然後,程式代碼會執行更新課程的 SQL,並將受影響的數據列數目傳回至 ViewData中的檢視。 當檢視取得 RowsAffected 值時,它會顯示更新的數據列數目。

[方案總管]中,在 [Views/Courses] 資料夾上按一下滑鼠右鍵,然後選擇 [新增 > 新項目]

在 [[新增專案] 對話框中,按兩下左窗格中 [已安裝] 底下的 [ASP.NET Core],按兩下 [檢視 ]Razor ,並將新檢視命名為 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] 索引卷標,然後將 “/UpdateCourseCredits” 新增至瀏覽器網址列中 URL 的結尾,以執行 UpdateCourseCredits 方法(例如:http://localhost:5813/Courses/UpdateCourseCredits)。 在文字盒中輸入數位:

更新課程點數頁面

點選 「更新」。 您會看到已受影響的資料列數目:

更新課程學分頁面受到影響的數據列

返回清單,以查看已修訂學分的課程清單。

請注意,生產程式代碼可確保更新一律會產生有效的數據。 此處顯示的簡化程式代碼可能會將點數相乘,結果可能大於5的數字。 (Credits 屬性具有 [Range(0, 5)] 屬性。)更新查詢會運作,但無效的數據可能會導致系統其他部分出現意外結果,因為系統假定點數為5或更少。

如需原始 SQL 查詢的詳細資訊,請參閱 原始 SQL 查詢

檢查 SQL 查詢

有時候,能夠查看傳送至資料庫的實際 SQL 查詢會很有幫助。 ASP.NET Core 的內建記錄功能會自動由 EF Core 用來寫入包含查詢和更新 SQL 的記錄。 在本節中,您會看到一些 SQL 記錄範例。

開啟 StudentsController.cs,並在 Details 方法中設定 if (student == null) 語句上的斷點。

以偵錯模式執行應用程式,然後移至學生的詳細數據頁面。

請轉到 輸出 視窗顯示偵錯輸出,然後您會看到查詢:

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 上下文類別可以作為您使用 EF 進行資料庫更新時的工作單元類別。

  • EF 包含不需要撰寫存放庫程式碼即可實作 TDD 的功能。

如需如何實作存放庫和工作單位模式的詳細資訊,請參閱本教學課程系列 Entity Framework 5 版本。

Entity Framework Core 會實作可用於測試的記憶體內部資料庫提供者。 如需詳細資訊,請參閱 使用 InMemory 測試

自動變動偵測

Entity Framework 會比較實體目前的值與原始值,以判斷實體變更的方式(因此需要傳送哪些更新至資料庫)。 原始值會在查詢或附加實體時儲存。 造成自動變更偵測的一些方法如下:

  • DbContext.SaveChanges

  • DbContext.Entry

  • ChangeTracker.Entries

如果您要追蹤大量的實體,而且您在迴圈中多次呼叫其中一種方法,您可能會使用 ChangeTracker.AutoDetectChangesEnabled 屬性暫時關閉自動變更偵測來獲得顯著的效能改善。 例如:

_context.ChangeTracker.AutoDetectChangesEnabled = false;

EF Core 原始程式碼和開發計劃

Entity Framework Core 來源位於 https://github.com/dotnet/efcore。 EF Core 存放庫包含夜間組建、問題追蹤、功能規格、設計會議筆記,以及 未來開發的藍圖。 您可以提交或尋找問題,並參與貢獻。

雖然原始程式碼是開放的,但 Entity Framework Core 作為 Microsoft 產品,仍然獲得完整支援。 Microsoft Entity Framework 小組會控制接受哪些貢獻,並測試所有程式代碼變更,以確保每個版本的品質。

從現有資料庫進行反向工程

若要從現有資料庫中反向生成數據模型,包括實體類別,請使用 scaffold-dbcontext 命令。 請參閱 入門指南

使用動態 LINQ 簡化程式碼

本系列中的 第三個教學課程 示範如何在 switch 語句中硬式編碼數據行名稱來撰寫 LINQ 程序代碼。 有兩個欄位可供選擇時,這樣運作良好,但如果您有許多欄位,程式碼可能會變得冗長。 若要解決此問題,您可以使用 EF.Property 方法,將屬性的名稱指定為字串。 若要試用此方法,請將 StudentsController 中的 Index 方法取代為下列程序代碼。

 public async Task<IActionResult> Index(
     string sortOrder,
     string currentFilter,
     string searchString,
     int? pageNumber)
 {
     ViewData["CurrentSort"] = sortOrder;
     ViewData["NameSortParm"] = 
         String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
     ViewData["DateSortParm"] = 
         sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

     if (searchString != null)
     {
         pageNumber = 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(), 
         pageNumber ?? 1, pageSize));
 }

致謝

湯姆·迪克斯特拉和里克·安德森(推特 @RickAndMSFT) 撰寫了本教學課程。 Rowan Miller、Diego Vega 和 Entity Framework 小組的其他成員協助進行程式代碼檢閱,並協助偵錯我們在撰寫教學課程程式代碼時所引發的問題。 John Parente 和 Paul Goldman 致力於更新 ASP.NET Core 2.2 的教學課程。

解決常見錯誤

另一個進程所使用的 ContosoUniversity.dll

錯誤資訊:

無法開啟 '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll',因為它正由另一個進程使用。

解決方案:

在 IIS Express 中停止網站。 移至 Windows 系統匣,尋找 IIS Express,並以滑鼠右鍵單擊其圖示,選取 Contoso University 網站,然後單擊 [停止網站]

在 Up 和 Down 方法中,移轉無需代碼的支架設計

可能的原因:

EF CLI 命令不會自動關閉並儲存程式代碼檔案。 如果您在執行 migrations add 命令時尚未儲存變更,EF 就不會找到您的變更。

解決方案:

執行 migrations remove 命令、儲存程式代碼變更,然後重新執行 migrations add 命令。

執行資料庫更新時發生錯誤

在具有現有數據的資料庫中進行架構變更時,可能會收到其他錯誤。 如果您收到無法解析的移轉錯誤,您可以變更連接字串中的資料庫名稱或刪除資料庫。 使用新的資料庫時,沒有任何數據可移轉,而且 update-database 命令更有可能在沒有錯誤的情況下完成。

最簡單的方法是在 appsettings.json中重新命名資料庫。 下次執行 database update時,將會建立新的資料庫。

若要刪除 SSOX 中的資料庫,請在資料庫上按下滑鼠右鍵,按兩下 [刪除],然後在 [刪除資料庫] 對話框中,選取 [關閉現有的連線],然後按下 [確定 ]。

若要使用 CLI 刪除資料庫,請執行 database drop CLI 命令:

dotnet ef database drop

尋找 SQL Server 實例時發生錯誤

錯誤資訊:

建立 SQL Server 的連線時發生網路相關或實例特定錯誤。 找不到伺服器或無法存取。 確認實例名稱正確,且 SQL Server 已設定為允許遠端連線。 (提供者: SQL 網路介面, 錯誤: 26 - 尋找指定的伺服器/實例時發生錯誤)

解決方案:

檢查連接字串。 如果您已手動刪除資料庫檔案,請變更建構字串中的資料庫名稱,以從新的資料庫開始。

取得程序代碼

下載或檢視已完成的應用程式。

其他資源

如需 EF Core的詳細資訊,請參閱 Entity Framework Core 檔。 另外還有一本書:Entity Framework Core in Action

如需如何部署 Web 應用程式的資訊,請參閱 主機和部署 ASP.NET Core

如需與 ASP.NET Core MVC 相關的其他主題相關信息,例如驗證和授權,請參閱 ASP.NET Core概觀。

如需建立可靠、安全、高效能、可測試且可調整 ASP.NET Core 應用程式的指引,請參閱 Enterprise Web 應用程式模式。 提供可實作模式的完整生產品質範例 Web 應用程式。

後續步驟

在本教學課程中,您會:

  • 執行原始 SQL 查詢
  • 呼叫一個查詢以返回實體。
  • 呼叫查詢以傳回其他類型的
  • 被稱為更新查詢
  • 已檢查的 SQL 查詢
  • 建立抽象層
  • 了解自動變更偵測
  • 瞭解 EF Core 原始程式碼和開發計劃
  • 瞭解如何使用動態 LINQ 來簡化程式碼

這會完成在 ASP.NET Core MVC 應用程式中使用 Entity Framework Core 的這一系列教學課程。 此系列使用新的資料庫;另一個方法是從現有的資料庫 反向工程模型