共用方式為


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

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

在本教學課程中,您會:

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

Prerequisites

執行原始 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);
}

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

Department Details

執行查詢以傳回其他類型的結果

稍早,您已為 [關於] 頁面建立學生統計數據方格,其中顯示每個註冊日期的學生數目。 您從 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;

執行應用程式並移至 [關於] 頁面。 顯示與先前相同的數據。

About page

執行更新查詢

假設 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>

選取 [UpdateCourseCredits] 索引卷標,然後將 “/UpdateCourseCredits” 新增至瀏覽器網址列中 URL 的結尾,以執行 方法(例如:http://localhost:5813/Courses/UpdateCourseCredits)。 在文字盒中輸入數位:

更新課程點數頁面

Click Update. 您會看到已受影響的資料列數目:

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

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

請注意,生產程式代碼可確保更新一律會產生有效的數據。 此處顯示的簡化程式代碼可能會將點數相乘,結果可能大於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 方法無法在伺服器上解析為一列。 Here's why:

  • 如果查詢會傳回多個數據列,則方法會傳回 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 屬性暫時關閉自動變更偵測來獲得顯著的效能改善。 For example:

_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 方法,將屬性的名稱指定為字串。 若要試用此方法,請將 Index 中的 StudentsController 方法取代為下列程序代碼。

 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));
 }

Acknowledgments

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

解決常見錯誤

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

Error message:

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

Solution:

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

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

Possible cause:

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

Solution:

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

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

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

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

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

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

dotnet ef database drop

尋找 SQL Server 實例時發生錯誤

Error Message:

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

Solution:

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

取得程序代碼

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

Additional resources

如需 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 應用程式。

Next steps

在本教學課程中,您會:

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

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