具有 Entity Framework Core (EF Core) 的 ASP.NET Core Blazor
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
本文介绍如何在服务器端 Blazor 应用中使用 Entity Framework Core (EF Core)。
服务器端 Blazor 是有状态的应用框架。 应用保持与服务器的持续连接,且用户的状态保留在线路中的服务器内存中。 用户状态的一个示例是在线路范围内的依赖关系注入 (DI) 服务实例中保留的数据。 Blazor 提供的唯一应用程序模型需要使用特殊方法来使用 Entity Framework Core。
注意
本文介绍服务器端 Blazor 应用中的 EF Core。 Blazor WebAssembly 应用在可阻止大多数直接数据库连接的 WebAssembly 沙盒中运行。 本文不介绍在 Blazor WebAssembly 中运行 EF Core。
示例应用
该示例应用构建为使用 EF Core 的服务器端 Blazor 应用的参考。 示例应用中有一个网格,其中具有排序和筛选、删除、添加和更新操作。 该示例演示了如何使用 EF Core 来处理乐观并发。
示例使用本地 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"
}
}
}
网格、添加和视图组件使用“每操作上下文”模式;在此模式下,将为每个操作创建一个上下文。 编辑组件使用“每组件上下文”模式;在此模式下,将为每个组件创建一个上下文。
数据库访问
EF Core 依赖于 DbContext 来配置数据库访问和充当工作单元。 EF Core 为 ASP.NET Core 应用提供 AddDbContext 扩展,这些应用在默认情况下将上下文注册为区分范围的服务。 在服务器端 Blazor 应用中,范围服务注册可能会出现问题,因为该实例在用户线路中的各个组件之间共享。 DbContext 并非线程安全,且不是为并发使用而设计的。 由于以下原因,现有生存期不适当:
- 单一实例在应用的所有用户之间共享状态,并导致不适当的并发使用。
- 范围(默认)在同一用户的组件之间会造成类似的问题。
- 暂时性会导致每个请求均生成一个新实例;但由于组件的生存期很长,这会导致上下文生存期比预期更长。
以下建议旨在提供在服务器端 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 文档)。
要新建具有依赖项的 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 通过服务提供程序满足任意依赖项。
- 可从 EF Core ASP.NET Core 5.0 或更高版本中使用
IDbContextFactory
,以便在 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"));
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
在 DeleteContactAsync
方法中使用工厂 (DbFactory
) 创建 DbContext
以删除联系人:
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
其他资源
反馈
提交和查看相关反馈