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:
- O que são identidades gerenciadas para recursos do Azure? (Documentação do Microsoft Entra)
- Documentação dos serviços do Azure
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
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
- 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.
- 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 blocotry
.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:
- Usando
DbContextOptions
para configurar o contexto. - Usando uma cadeia de conexão por DbContext, como quando utilizas o modelo Identity do ASP.NET Core. Para obter mais informações, consulte Multi-tenancy (EF Core documentation).
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:
- ActivatorUtilities.CreateInstance satisfaz quaisquer dependências através do provedor de serviços.
- IDbContextFactory<TContext> está disponível no EF Core ASP.NET Core 5.0 ou posterior, portanto, a interface é implementada no aplicativo de exemplo para ASP.NET Core 3.x.
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();
}
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 comotrue
, as operações assíncronas podem começar. QuandoBusy
é redefinido parafalse
, 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