搭配 Entity Framework Core (EF Core) 的 ASP.NET Core Blazor

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

本文說明如何在伺服器端 Blazor 應用程式中使用 Entity Framework Core (EF Core)

伺服器端 Blazor 是具狀態的應用程式架構。 應用程式會維持伺服器的持續連線,且使用者的狀態會以線路形式保留在伺服器記憶體中。 使用者狀態的其中一個範例,是在線路範圍內的相依性插入 (DI) 服務執行個體中保存的資料。 Blazor 提供的唯一應用程式模型需要使用 Entity Framework Core 的特殊方法。

注意

本文說明伺服器端 EF Core 應用程式中的 Blazor。 Blazor WebAssembly 應用程式會在 WebAssembly 沙箱中執行,以防止大多數直接的資料庫連接。 在 Blazor WebAssembly 中執行 EF Core 超出本文的範圍。

本指南適用於在 Blazor Web 應用程式中採用互動式伺服器端轉譯 (互動式 SSR) 的元件。

本指導適用於託管 Blazor WebAssembly 解決方案或 Blazor Server 應用程式的 Server 專案。

範例應用程式

範例應用程式已建置為使用 EF Core 的伺服器端 Blazor 應用程式的參考。 範例應用程式包含具有排序和篩選、刪除、新增和更新作業的格線。 範例會示範如何使用 EF Core 來處理開放式同步存取。

檢視或下載範例程式碼 (如何下載):選取與您採用之 .NET 版本相符的資料夾。 在版本資料夾中,存取名為 BlazorWebAppEFCore 的範例。

檢視或下載範例程式碼 (如何下載):選取與您採用之 .NET 版本相符的資料夾。 在版本資料夾中,存取名為 BlazorServerEFCoreSample 的範例。

範例會使用本機 SQLite 資料庫,以便可以在任何平台上使用。 範例也會設定資料庫記錄,以顯示產生的 SQL 查詢。 這是在 appsettings.Development.json 中設定的:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

格線、新增和檢視元件會使用 "context-per-operation" 模式,其中會為每個作業建立內容。 編輯元件會使用 "context-per-component" 模式,其中會為每個元件建立內容。

注意

本主題中的部分程式碼範例需要未顯示的命名空間和服務。 若要檢查完整運作的程式碼,包括 Razor 範例的必要 @using@inject 指示詞,請參閱範例應用程式

資料庫存取

EF Core 會依賴 DbContext 作為設定資料庫存取的方法,並作為工作單位。 EF Core 會針對預設將內容註冊為範圍服務的 ASP.NET Core 應用程式,提供 AddDbContext 延伸模組。 在伺服器端 Blazor 應用程式中,範圍服務註冊可能會造成問題,因為執行個體會在使用者線路內的元件之間共用。 DbContext 不是安全的執行緒,而且不是針對同時使用而設計。 現有的存留期不合適,原因如下:

  • Singleton 會共用應用程式所有使用者的狀態,並造成不合適的同時使用。
  • 範圍 (預設值) 會在相同使用者元件之間造成類似的問題。
  • 暫時性會造成每個要求產生新的執行個體;但是,由於元件可以長期存在,因此會產生比預期留存更久的內容。

下列建議旨在提供在伺服器端 Blazor 應用程式中使用 EF Core 的一致方法。

  • 根據預設,請考慮針對每個作業使用一個內容。 內容是專為快速、低負荷具現化而設計:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • 使用旗標來防止多個同時作業:

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    將作業放在 try 區塊中的 Loading = true; 行之後。

    載入邏輯不需要鎖定資料庫記錄,因為執行緒安全性不是問題。 載入邏輯是用來停用 UI 控制項,讓使用者在擷取資料時不會意外選取按鈕或更新欄位。

  • 如果有多個執行緒可能存取相同的程式碼區塊,插入處理站,並為每個作業建立新的執行個體。 否則,插入和使用內容通常就已足夠。

  • 針對利用 EF Core 的變更追蹤並行控制的較長存留期作業,將內容範圍限制在元件的存留期內

新的 DbContext 執行個體

建立新 DbContext 執行個體的最快方式是使用 new 來建立新的執行個體。 不過,在某些情況下,需要解決其他相依性:

使用相依性建立新 DbContext 的建議方法是使用處理站。 EF Core 5.0 或更新版本提供內建處理站來建立新內容。

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider ?? throw new ArgumentNullException(
                $"{nameof(provider)}: You must configure an instance of " +
                "IServiceProvider");
        }

        public TContext CreateDbContext() => 
            ActivatorUtilities.CreateInstance<TContext>(provider);
    }
}

在上述處理站中:

下列範例會設定 SQLite 並啟用資料記錄。 程式碼會使用擴充方法 (AddDbContextFactory) 來設定適用於 DI 的資料庫處理站,並提供預設選項:

builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

處理站會插入元件,並用來建立新的 DbContext 執行個體。

在範例應用程式的首頁中,IDbContextFactory<ContactContext> 會插入元件中:

@inject IDbContextFactory<ContactContext> DbFactory

系統會使用處理站 (DbFactory) 建立 DbContext,以刪除 DeleteContactAsync 方法中的連絡人:

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

注意

Filters 是插入的 IContactFilters,且 Wrapper元件參考GridWrapper 元件。 請參閱範例應用程式中的 Home 元件 (Components/Pages/Home.razor)。

注意

Filters 是插入的 IContactFilters,且 Wrapper元件參考GridWrapper 元件。 請參閱範例應用程式中的 Index 元件 (Pages/Index.razor)。

元件存留期的範圍

您可能想要建立存在於元件存留期的 DbContext。 這可讓您將其作為工作單位,並利用內建功能,例如變更追蹤和並行解析。

您可以使用處理站來建立內容,並追蹤元件的存留期。 首先,實作 IDisposable 並插入處理站,如 EditContact 元件所示 (Components/Pages/EditContact.razor):

您可以使用處理站來建立內容,並追蹤元件的存留期。 首先,實作 IDisposable 並插入處理站,如 EditContact 元件所示 (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

範例應用程式可確保在處置元件時處置內容:

public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}

最後,會覆寫 OnInitializedAsync 以建立新的內容。 在範例應用程式中,OnInitializedAsync 以相同的方法載入連絡人:

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

在前述範例中:

  • Busy 設定為 true 時,非同步作業可能會開始。 當 Busy 設定回 false 時,應該會完成非同步作業。
  • 將額外的錯誤處理邏輯放在 catch 區塊中。

啟用敏感資料記錄

EnableSensitiveDataLogging 在例外狀況訊息和架構記錄中包含應用程式資料。 記錄的資料可以包含指派給實體執行個體屬性的值,以及傳送至資料庫的命令的參數值。 使用 EnableSensitiveDataLogging 記錄資料是安全性風險,因為可能會在記錄對資料庫執行的 SQL 陳述式時,暴露密碼和其他個人識別資訊 (PII)

我們建議只針對開發和測試啟用 EnableSensitiveDataLogging

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

其他資源