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 informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

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

O lado do servidor Blazor é uma estrutura do aplicativo com estado. 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 do estado do usuário são os dados retidos nas instâncias de serviço de DI (injeção de dependência) que têm como escopo o circuito. O modelo de aplicativo exclusivo que Blazor fornece/requer uma abordagem especial para o uso do Entity Framework Core.

Observação

Este artigo aborda EF Core em aplicativos Blazor do servidor. Blazor WebAssembly os aplicativos são executados em uma área restrita WebAssembly que impede a maioria das conexões de banco de dados diretas. A execução de EF Core em Blazor WebAssembly não faz parte do escopo deste artigo.

Essas diretrizes se aplicam a componentes que adotam a SSR interativa (renderização interativa do lado do servidor) em um Blazor Web App.

Essas diretrizes se aplicam ao projeto Server de uma solução 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 Blazor de teste e produção implantados, consulte os artigos no nó BlazorSegurança e Identity.

Para serviços do Microsoft Azure, recomendamos o uso de identidades gerenciadas. As identidades gerenciadas autenticam de maneira segura para serviços do Azure sem armazenar credenciais no código do aplicativo. Para saber mais, consulte os recursos a seguir:

Aplicativo de exemplo

O aplicativo de exemplo foi criado como referência para aplicativos Blazor do servidor 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.

O exemplo demonstra o uso de EF Core para lidar com a simultaneidade otimista. No entanto, não há suporte para tokens de simultaneidade gerados por banco de dados nativos 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 dê suporte a tokens de simultaneidade gerados pelo banco de dados (por exemplo, o provedor do SQL Server).

Exibir ou baixar o código de amostra (como baixar): selecione a pasta que corresponde à versão do .NET que você está adotando. Na pasta de versão, acesse a amostra chamada BlazorWebAppEFCore.

Exibir ou baixar o código de amostra (como baixar): selecione a pasta que corresponde à versão do .NET que você está adotando. Na pasta de versão, acesse a amostra chamada BlazorServerEFCoreSample.

O exemplo usa um banco de dados SQLite local para que possa ser usado em qualquer plataforma. O exemplo também configura o registro em log do banco de dados para mostrar as consultas SQL 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 de grade, adição e exibição usam o padrão "contexto por operação", no qual um contexto é criado para cada operação. O componente de edição usa o padrão "contexto por componente", em que 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 totalmente funcional, incluindo as diretivas @using e @inject necessárias para Razor a obtenção de exemplos, consulte o aplicativo de exemplo.

Tutorial Criar um Blazor aplicativo de banco de dados de filmes

Para obter uma experiência de tutorial na criação de um aplicativo que usa EF Core para trabalhar com um banco de dados, consulte Criar um Blazor aplicativo de banco de dados de filme (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 ao banco de dados

EF Core depende de um DbContext como meio de configuração do acesso ao banco de dados e atuar como uma unidade de trabalho. EF Core fornece a extensão AddDbContext para aplicativos ASP.NET Core que registram o contexto como um serviço com escopo. Em aplicativos Blazor do servidor, os registros de serviço com escopo podem ser problemáticos porque a instância é compartilhada entre componentes dentro do circuito do usuário. DbContext não é thread-safe e não foi projetado para uso simultâneo. Os tempos de vida existentes são inadequados pelos motivos a seguir:

  • Singleton compartilha o estado com todos os usuários do aplicativo e acarreta uso simultâneo inadequado.
  • Com escopo (o padrão) representa um problema semelhante entre componentes para o mesmo usuário.
  • Resultados transitórios em uma nova instância por solicitação, mas, a possibilidade de longa duração dos componentes resulta em um contexto de vida mais longa que o esperado.

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

  • Considere usar um único 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 impedir 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.

    O acesso thread-safe não é uma preocupação. então o carregamento da lógica não requer o bloqueio de registros de banco de dados. A lógica de carregamento é usada para desabilitar controles de interface do usuário para que os usuários não selecionem botões ou atualizem campos inadvertidamente durante a busca por dados.

  • Se houver alguma chance de que vários threads possam acessar o mesmo bloco de código, injete um alocador e crie uma nova instância por operação. Caso contrário, injetar e usar o contexto costuma ser suficiente.

  • Para operações de vida mais longa que aproveitam o acompanhamento de alterações EF Core's ou o controle de simultaneidade, crie o escopo do contexto para o tempo de vida do componente.

Novas instâncias DbContext

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

Aviso

Não armazene segredos de aplicativo, cadeias de conexão, credenciais, senhas, PINs (números de identificação pessoal), código C#/.NET privado ou chaves/tokens privados no código do lado do cliente, que é sempre inseguro. Em ambientes de teste/preparo e produção, o código do lado do Blazor servidor e as APIs Web devem usar fluxos de autenticação seguros que evitam 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 Gerenciador de segredos é recomendada para proteger dados confidenciais. Para obter mais informações, consulte Manter dados e credenciais confidenciais com segurança.

A abordagem recomendada para criar um novo DbContext com dependências é o uso de um alocador. EF Core 5.0 ou posterior fornece um alocador interno para a criação de 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);
    }
}

No alocador anterior:

O exemplo a seguir configura o SQLite e habilita o registro de dados. O código usa um método de extensão (AddDbContextFactory) para configurar o alocador de banco de dados para DI (injeção de dependência) 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"));

O alocador é injetado em componentes e usado para criar novas instâncias DbContext.

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

@inject IDbContextFactory<ContactContext> DbFactory

Um DbContext é criado usando o alocador (DbFactory) para excluir um contato no DeleteContactAsync método:

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 IContactFilters injetado, e Wrapper é uma referência de componente para o componente GridWrapper. Consulte o Home componente (Components/Pages/Home.razor) no aplicativo de exemplo.

Observação

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

Escopo para o tempo de vida do componente

Talvez seja desejado criar um DbContext que exista durante o tempo de vida de um componente. Isso permite que seja usado como uma unidade de trabalho e aproveitar os recursos internos, como o acompanhamento de alterações e a resolução de simultaneidade.

O alocador pode ser usado para criar um contexto e rastreá-lo durante o tempo de vida do componente. Primeiramente, implemente IDisposable e injete o alocador conforme mostrado no componente EditContact (Components/Pages/EditContact.razor):

O alocador pode ser usado para criar um contexto e rastreá-lo durante o tempo de vida do componente. Primeiramente, implemente IDisposable e injete o alocador 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 for 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();
}

Por fim, 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 é definido como true, as operações assíncronas podem começar. Quando Busy é redefinido como false, as operações assíncronas devem ser finalizadas.
  • Coloque a lógica de tratamento de erro adicional em um bloco catch.

Habilite o registro em log de dados confidenciais

EnableSensitiveDataLogging inclui dados do aplicativo em mensagens de exceção e registro em log de estrutura. Os dados registrados podem incluir os valores atribuídos às propriedades de instâncias de entidade e de valores de parâmetro para comandos enviados ao banco de dados. O registro de dados com EnableSensitiveDataLogging é um risco de segurança, pois pode expor senhas e outras informações de identificação pessoal (PII) ao registrar instruções SQL executadas no banco de dados.

Recomenda-se habilitar EnableSensitiveDataLogging apenas para desenvolvimento e teste:

#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