具有 Entity Framework Core (EF Core) 的 ASP.NET Core Blazor

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .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 App 中采用交互式服务器端呈现(交互式 SSR)的组件。

本指导适用于托管 Blazor WebAssembly 解决方案或 Blazor Server 应用的 Server 项目。

生产应用所需的安全身份验证流

本教程使用不需要对用户进行身份验证的本地数据库。 生产应用应使用可用的最安全的身份验证流。 有关已部署测试和生产 Blazor 应用的身份验证的详细信息,请参阅 Blazor安全和 Identity 节点中的文章。

对于 Microsoft Azure 服务,我们建议使用托管标识。 托管标识可安全地向 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"
    }
  }
}

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

注意

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

生成 Blazor 电影数据库应用教程

有关构建用于EF Core处理数据库的应用的教程体验,请参阅“生成Blazor电影数据库应用”(概述)。 本教程介绍如何创建 Blazor Web App 可在电影数据库中显示和管理电影的电影。

数据库访问

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

警告

不要在客户端代码中存储应用机密、连接字符串、凭据、密码、个人标识号(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);
    }
}

在前面的工厂中:

以下示例会配置 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

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;

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

其他资源