搭配 Entity Framework Core (EF Core) 的 ASP.NET Core Blazor
注意
這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .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 App 中採用互動式伺服器端轉譯 (互動式 SSR) 的元件。
本指導適用於託管 Blazor WebAssembly 解決方案或 Blazor Server 應用程式的 Server
專案。
生產應用程式所需的安全驗證流程
本文使用本機資料庫,其不需要使用者進行驗證。 實際執行應用程式應該使用可用的最安全驗證流程。 如需已部署測試和生產 Blazor 應用程式驗證的詳細資訊,請參閱 Blazor安全性和 Identity 節點中的文章。
針對 Microsoft Azure 服務,我們建議使用 受控識別。 受控識別可以以安全的方式向 Azure 服務進行驗證,而無需在應用程式程式碼中儲存認證。 如需詳細資訊,請參閱以下資源:
- 什麼是 Azure 資源受控識別? (Microsoft Entra 文件)
- Azure 服務文件
範例應用程式
範例應用程式已建置為使用 EF Core 的伺服器端 Blazor 應用程式的參考。 範例應用程式包含具有排序和篩選、刪除、新增和更新作業的格線。
範例會示範如何使用 EF Core 來處理開放式同步存取。 不過, SQLite 資料庫不支援原生資料庫產生的並行令牌 ,這是範例應用程式的資料庫提供者。 若要示範範例應用程式的並行,請採用支援資料庫產生並行令牌的不同資料庫提供者(例如 SQL Server 提供者)。
檢視或下載範例程式碼 (如何下載):選取與您採用之 .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.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" 模式,其中會為每個元件建立內容。
建置 Blazor 電影資料庫應用程式教學課程
如需建置用來 EF Core 處理資料庫之應用程式的教學課程體驗,請參閱 建 Blazor 置電影資料庫應用程式 (概觀) 。 本教學課程說明如何建立 Blazor Web App ,以在電影資料庫中顯示和管理電影。
資料庫存取
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
來建立新的執行個體。 不過,在某些情況下,需要解決其他相依性:
- 使用
DbContextOptions
來設定內容。 - 針對每個 DbContext 使用連接字串,例如當您使用 ASP.NET Core Identity的模型 時。 如需詳細資訊,請參閱 多租用戶 (EF Core 文件)。
警告
請勿在用戶端程式代碼中儲存應用程式秘密、連接字串、認證、密碼、個人標識碼 (PIN)、私人 C#/.NET 程式代碼或私鑰/令牌,這一律不安全。 在測試/預備和生產環境中,伺服器端 Blazor 程序代碼和 Web API 應該使用安全驗證流程,以避免在專案程式代碼或組態檔內維護認證。 在本機開發測試之外,建議您避免使用環境變數來儲存敏感數據,因為環境變數不是最安全的方法。 針對本機開發測試, 建議使用秘密管理員工具 來保護敏感數據。 如需詳細資訊,請參閱 安全地維護敏感數據和認證。
使用相依性建立新 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);
}
}
在上述處理站中:
- ActivatorUtilities.CreateInstance 透過服務提供者滿足任何相依性。
IDbContextFactory
可在 EF Core ASP.NET Core 5.0 或更新版本中取得,因此介面會在 ASP.NET Core 3.x 的範例應用程式中實作。
下列範例會設定 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"));
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
執行個體。
在 home 範例應用程式分頁中, 會插入 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;
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();
}
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;
}
}
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;
}
}
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