Partilhar via


ASP.NET Core Blazor com o Entity Framework Core (EF Core)

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Este artigo explica como usar do Entity Framework Core (EF Core) em aplicativos de Blazor do lado do servidor.

O Blazor do lado do servidor é um framework de aplicação stateful. O aplicativo mantém uma conexão contínua com o servidor e o estado do usuário é mantido na memória do servidor em um circuito . Um exemplo de estado do usuário são os dados mantidos em injeção de dependência (DI) instâncias de serviço que têm escopo para o circuito. O modelo de aplicativo exclusivo que Blazor fornece requer uma abordagem especial para usar o Entity Framework Core.

Observação

Este artigo aborda EF Core em aplicações de Blazor do lado do servidor. Blazor WebAssembly aplicações são executadas em um ambiente isolado WebAssembly que impede a maioria das conexões diretas ao banco de dados. Executar EF Core em Blazor WebAssembly está além do escopo deste artigo.

Esta orientação se aplica a componentes que adotam renderização interativa do lado do servidor (SSR interativo) em um Blazor Web App.

Esta orientação se aplica ao projeto Server de uma solução de Blazor WebAssembly hospedada ou de um aplicativo Blazor Server.

Fluxo de autenticação seguro necessário para aplicativos de produção

Este artigo usa um banco de dados local que não requer autenticação do usuário. Os aplicativos de produção devem usar o fluxo de autenticação mais seguro disponível. Para obter mais informações sobre autenticação para aplicativos de teste e produção Blazor implantados, consulte os artigos na segurança Blazore no nó Identity.

Para serviços do Microsoft Azure, recomendamos o uso de identidades gerenciadas . As identidades gerenciadas são autenticadas com segurança nos serviços do Azure sem armazenar credenciais no código do aplicativo. Para obter mais informações, consulte os seguintes recursos:

Aplicativo de exemplo

A aplicação de exemplo foi criada como uma referência para aplicações de servidor Blazor que usam EF Core. O aplicativo de exemplo inclui uma grade com operações de classificação e filtragem, exclusão, adição e atualização.

Exibir ou baixar código de exemplo (como baixar): Selecione a pasta que corresponde à versão do .NET que você está adotando. Dentro da pasta versão, aceda ao exemplo chamado BlazorWebAppEFCore.

Exibir ou baixar código de exemplo (como baixar): Selecione a pasta que corresponde à versão do .NET que você está adotando. Dentro da pasta versão, aceda ao exemplo chamado BlazorServerEFCoreSample.

Use o exemplo com SQLite

O exemplo usa um banco de dados SQLite local para que ele possa ser usado em qualquer plataforma.

O exemplo demonstra o uso de EF Core para lidar com simultaneidade otimista. No entanto, tokens de concorrência gerados por banco de dados nativo não são suportados para bancos de dados SQLite, que é o provedor de banco de dados para o aplicativo de exemplo. Para demonstrar a simultaneidade com o aplicativo de exemplo, adote um provedor de banco de dados diferente que ofereça suporte a tokens de simultaneidade gerados por banco de dados (por exemplo, o provedor SQL Server). Você pode utilizar o SQL Server para a aplicação de exemplo seguindo as instruções indicadas na secção seguinte, Usar o exemplo com o SQL Server e simultaneidade otimista.

Utilize o exemplo com o SQL Server e simultaneidade otimista

O exemplo demonstra o uso de EF Core para lidar com simultaneidade otimista, mas apenas para um provedor de banco de dados que usa tokens de simultaneidade gerados por banco de dados nativo, que é um recurso com suporte para o SQL Server. Para demonstrar a simultaneidade com o aplicativo de exemplo, o aplicativo de exemplo pode ser convertido do provedor SQLite para usar o provedor SQL Server com um novo banco de dados do SQL Server criado usando scaffolding .NET.

Use as diretrizes a seguir para adotar o SQL Server para o aplicativo de exemplo usando o Visual Studio.

Abra o arquivo Program (Program.cs) e comente as linhas que adicionam a fábrica de contexto do banco de dados com o provedor SQLite:

- 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"));

Salve o arquivo Program.cs.

Clique com o botão direito do mouse no projeto em Explorador de Soluções e selecione Adicionar>Novo Item Estruturado.

Na caixa de diálogo Adicionar Novo Item Estruturado, selecione Instalado>Comum>Blazor>Razor Componente>Razor Componentes Usando o Entity Framework (CRUD). Selecione o botão Adicionar.

Na caixa de diálogo Adicionar componentes usando o Entity Framework (CRUD), use as seguintes configurações:

  • Modelo: Use a seleção padrão (CRUD).
  • Classe de modelo: Selecione o modelo Contact na lista suspensa.
  • classe DbContext: Selecione o sinal de adição (+) e selecione Adicionar usando o nome da classe de contexto padrão gerado pelo scaffolder.
  • Provedor de banco de dados: Use a seleção padrão (SQL Server).

Selecione Adicionar para criar o modelo e criar o banco de dados do SQL Server.

Quando a operação de andaime for concluída, exclua a classe de contexto gerada da pasta Data (Data/{PROJECT NAME}Context.cs, onde o espaço reservado {PROJECT NAME} é o nome/namespace do projeto).

Elimine a pasta ContactPages da pasta Components/Pages, que contém páginascom base no QuickGrid para a gestão de contactos. Para obter uma demonstração completa de páginas baseadas em QuickGrid para gerir dados, use o tutorial Build a Blazor movie database app (Overview).

Abra o arquivo Program (Program.cs) e localize a linha que o scaffolding adicionou para criar uma fábrica de contexto de banco de dados usando o provedor do SQL Server. Altere o contexto da classe de contexto gerada (excluída anteriormente) para a classe ContactContext existente do aplicativo:

- builder.Services.AddDbContextFactory<BlazorWebAppEFCoreContext>(options =>
+ builder.Services.AddDbContextFactory<ContactContext>(options =>

Neste ponto, o aplicativo está usando o provedor do SQL Server e um banco de dados do SQL Server criado para a classe de modelo Contact. A simultaneidade otimista funciona usando tokens de simultaneidade gerados por banco de dados nativo que já estão implementados na classe ContactContext do aplicativo de exemplo.

Registo da base de dados

O exemplo também configura o log do banco de dados para mostrar as consultas SQL que são geradas. Isso é configurado em 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"
    }
  }
}

Os componentes grid, add e view usam o padrão "context-per-operation", onde um contexto é criado para cada operação. O componente de edição usa o padrão "contexto por componente", onde um contexto é criado para cada componente.

Observação

Alguns dos exemplos de código neste tópico exigem namespaces e serviços que não são mostrados. Para inspecionar o código em pleno funcionamento, incluindo as diretivas @using e @inject necessárias para os exemplos de Razor, consulte a aplicação de exemplo .

Criar um tutorial do aplicativo de banco de dados de filmes Blazor

Para obter uma experiência de tutorial criando um aplicativo que usa EF Core para trabalhar com um banco de dados, consulte Criar um aplicativo de banco de dados de filmes Blazor (Visão geral). O tutorial mostra como criar um Blazor Web App que pode exibir e gerenciar filmes em um banco de dados de filmes.

Acesso à base de dados

EF Core conta com um DbContext como meio para configurar o acesso ao banco de dados e atuar como uma unidade de trabalho. fornece a extensão de para aplicativos ASP.NET Core que registra o contexto como um serviço com escopo . Em aplicações do lado do servidor Blazor, os registos de serviços com escopo podem causar problemas, pois a instância é partilhada entre componentes dentro do circuito de utilizador. DbContext não é thread safe e não foi projetado para uso simultâneo. Os tempos de vida existentes são inadequados pelas seguintes razões:

  • Singleton compartilha o estado entre todos os utilizadores do aplicativo e leva à utilização simultânea inadequada.
  • com escopo (o padrão) cria um problema semelhante entre componentes para o mesmo utilizador.
  • transitório resulta em uma nova instância por solicitação; Mas, como os componentes podem ser de longa duração, isso resulta em um contexto de vida mais longa do que se pode imaginar.

As recomendações a seguir foram projetadas para fornecer uma abordagem consistente ao uso de EF Core em aplicativos Blazor do lado do servidor.

  • Considere o uso de um contexto por operação. O contexto foi projetado para instanciação rápida e de baixa sobrecarga:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Use um sinalizador para evitar várias operações simultâneas:

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

    Coloque as operações após a linha Loading = true; no bloco try.

    A segurança de threads não é uma preocupação, portanto, a lógica de carregamento não requer o bloqueio de registros de banco de dados. A lógica de carregamento é usada para desabilitar os controles da interface do usuário para que os usuários não selecionem inadvertidamente botões ou atualizem campos enquanto os dados são buscados.

  • Se houver alguma chance de que vários threads possam aceder aos mesmos blocos de código, injete uma fábrica e crie uma nova instância por operação. Caso contrário, injetar e usar o contexto geralmente é suficiente.

  • Para operações de vida mais longa que aproveitam a de controle de alterações do EF Coreou controle de simultaneidade, escopo o contexto para o tempo de vida do componente.

Novas instâncias DbContext

A maneira mais rápida de criar uma nova instância de DbContext é usando new para criar uma nova instância. No entanto, há cenários que exigem a resolução de dependências adicionais:

Advertência

Não armazene segredos de aplicativos, cadeias de conexão, credenciais, senhas, números de identificação pessoal (PINs), código C#/.NET privado ou chaves/tokens privados no código do lado do cliente, que é sempre inseguro. Em ambientes de teste/preparação e produção, o código Blazor do lado do servidor e as APIs da Web devem usar fluxos de autenticação seguros que evitem a manutenção de credenciais no código do projeto ou nos arquivos de configuração. Fora dos testes de desenvolvimento local, recomendamos evitar o uso de variáveis de ambiente para armazenar dados confidenciais, pois as variáveis de ambiente não são a abordagem mais segura. Para testes de desenvolvimento local, a ferramenta Secret Manager é recomendada para proteger dados confidenciais. Para obter mais informações, consulte Manter com segurança dados confidenciais e credenciais.

A abordagem recomendada para criar um novo DbContext com dependências é usar uma fábrica. EF Core 5.0 ou posterior fornece uma fábrica integrada para criar novos contextos.

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

Na fábrica anterior:

O exemplo a seguir configura SQLite e habilita o registro em log de dados. O código usa um método de extensão (AddDbContextFactory) para configurar a fábrica de banco de dados para DI e fornecer opções padrão:

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"));

A fábrica é injetada nos componentes e utilizada para criar novas instâncias de DbContext.

Na página home do aplicativo de exemplo, IDbContextFactory<ContactContext> é injetado no componente:

@inject IDbContextFactory<ContactContext> DbFactory

Um DbContext é criado usando a fábrica (DbFactory) para excluir um contato no método 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();
}

Observação

Filters é um IContactFiltersinjetado e Wrapper é um de referência de componente para o componente GridWrapper. Consulte o componente Home (Components/Pages/Home.razor) no aplicativo de exemplo.

Observação

Filters é um IContactFiltersinjetado e Wrapper é um de referência de componente para o componente GridWrapper. Consulte o componente Index (Pages/Index.razor) no aplicativo de exemplo.

Escopo do tempo de vida do componente

Você pode querer criar um DbContext que exista durante o tempo de vida de um componente. Isso permite que você o use como uma unidade de trabalho e aproveite os recursos internos, como controle de alterações e resolução de simultaneidade.

Você pode usar a fábrica para criar um contexto e rastreá-lo durante o tempo de vida do componente. Primeiro, implemente IDisposable e injete a fábrica como mostrado no componente EditContact (Components/Pages/EditContact.razor):

Você pode usar a fábrica para criar um contexto e rastreá-lo durante o tempo de vida do componente. Primeiro, implemente IDisposable e injete a fábrica conforme mostrado no componente EditContact (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

O aplicativo de exemplo garante que o contexto seja descartado quando o componente é descartado:

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

Finalmente, OnInitializedAsync é substituído para criar um novo contexto. No aplicativo de exemplo, OnInitializedAsync carrega o contato no mesmo método:

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

No exemplo anterior:

  • Quando Busy estiver definido como true, as operações assíncronas podem começar. Quando Busy é redefinido para false, as operações assíncronas devem ser concluídas.
  • Coloque uma lógica adicional de tratamento de erros em um bloco catch.

Habilite o registro de dados confidenciais

EnableSensitiveDataLogging inclui dados da aplicação em mensagens de exceção e registo do framework. Os dados registrados podem incluir os valores atribuídos às propriedades de instâncias de entidade e valores de parâmetro para comandos enviados ao banco de dados. O registo de dados com o EnableSensitiveDataLogging é um risco de segurança , dado que pode expor senhas e outras informações pessoalmente identificáveis (PII) ao registar instruções SQL executadas contra a base de dados.

Recomendamos habilitar apenas EnableSensitiveDataLogging para desenvolvimento e testes:

#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

Recursos adicionais