具有 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。

注意

本文介绍服务器端 Blazor 应用中的 EF Core。 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"
    }
  }
}

网格、添加和视图组件使用“每操作上下文”模式;在此模式下,将为每个操作创建一个上下文。 编辑组件使用“每组件上下文”模式;在此模式下,将为每个组件创建一个上下文。

注意

本主题中的一些代码示例需要未显示的命名空间和服务。 若要检查完全运行的代码(包括 Razor 示例所需的 @using@inject 指令),请参阅示例应用

数据库访问

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 创建新实例。 但是,存在一些需要解析其他依赖项的方案:

要新建具有依赖项的 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

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 是注入的 IContactFiltersWrapper 是对 GridWrapper 组件的组件引用。 请参阅示例应用中的 Home 组件 (Components/Pages/Home.razor)。

注意

Filters 是注入的 IContactFiltersWrapper 是对 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

其他资源