ASP.NET Core Blazor Server com Entity Framework Core (EFCore)

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

Observação

Este artigo aborda EF Core em Blazor Server aplicativos. Blazor WebAssembly os aplicativos são executados em uma área sandbox WebAssembly que impede a maioria das conexões diretas de banco de dados. A EF Core no Blazor WebAssembly está além do escopo deste artigo.

Aplicativo de exemplo

O aplicativo de exemplo foi criado como uma referência para Blazor Server aplicativos que usam EF Core. O aplicativo de exemplo inclui uma grade com operações de classificação e filtragem, exclusão, aplicação e atualização. O exemplo demonstra o uso de EF Core para lidar com a concurreência otimista.

Exibir ou baixar código de exemplo (como baixar)

O exemplo usa um banco de dados SQLite local para que ele possa ser usado em qualquer plataforma. O exemplo também configura o log do banco de dados para mostrar as consultas SQL geradas. Isso é configurado em appsettings.Development.json :

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

Os componentes de grade, adicionar e exibir usam o padrão "contexto por operação", em que um contexto é criado para cada operação. O componente editar usa o padrão "context-per-component", 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 em funcionamento, incluindo as diretivas e necessárias @using @inject para Razor exemplos, consulte o aplicativo de exemplo.

Acesso ao banco de dados

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

  • O Singleton compartilha o estado entre todos os usuários do aplicativo e leva a um 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 como os componentes podem ser de longa duração, isso resulta em um contexto de vida mais longa do que pode ser pretendido.

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

  • Por padrão, considere o uso de um contexto por operação. O contexto foi projetado para uma insta instaciaçã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 Loading = true; a linha no bloco try .

  • Para operações de vida mais longa que aproveitam o controle de EF Core controle de alterações ou simultância do EF Core ,de escopo do contexto para o tempo de vida do componente.

Novas instâncias dbContext

A maneira mais rápida de criar uma DbContext nova instância é usando para criar uma nova new instância. No entanto, há vários cenários que podem exigir a resolução de dependências adicionais. Por exemplo, talvez você queira usar DbContextOptions para configurar o contexto.

A solução recomendada para criar um novo DbContext com dependências é usar uma fábrica. EF Core 5.0 ou posterior fornece um factory integrado para criar novos contextos.

O exemplo a seguir configura o 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:

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

A fábrica é injetada em componentes e usada para criar novas instâncias. Por exemplo, em Pages/Index.razor:

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

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

Novas instâncias podem ser criadas com uma fábrica que permite configurar a cadeia de conexão por , como quando você usa o modelo ASP.NET DbContext DbContext Core Identity :

services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

services.AddScoped<ApplicationDbContext>(p => 
    p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>()
    .CreateDbContext());

Escopo para o tempo de vida do componente

Talvez você queira 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 integrados, como controle de alterações e resolução de simultância. Você pode usar a fábrica para criar um contexto e rastreá-lo durante o tempo de vida do componente. Primeiro, IDisposable implemente e injete a fábrica conforme mostrado em 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();
}

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();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

Habilitar 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 valores de parâmetro para comandos enviados ao banco de dados. Registrar dados com é um risco de segurança, pois pode expor senhas e outras PII (informações de identificação pessoal) quando registra instruções SQL executadas no banco EnableSensitiveDataLogging de dados.

Recomendamos habilenciar somente EnableSensitiveDataLogging 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

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

Observação

Este artigo aborda EF Core em Blazor Server aplicativos. Blazor WebAssembly os aplicativos são executados em uma área sandbox WebAssembly que impede a maioria das conexões diretas de banco de dados. A EF Core no Blazor WebAssembly está além do escopo deste artigo.

Aplicativo de exemplo

O aplicativo de exemplo foi criado como uma referência para Blazor Server aplicativos que usam EF Core. O aplicativo de exemplo inclui uma grade com operações de classificação e filtragem, exclusão, aplicação e atualização. O exemplo demonstra o uso de EF Core para lidar com a concurreência otimista.

Exibir ou baixar código de exemplo (como baixar)

O exemplo usa um banco de dados SQLite local para que ele possa ser usado em qualquer plataforma. O exemplo também configura o log do banco de dados para mostrar as consultas SQL geradas. Isso é configurado em appsettings.Development.json :

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

Os componentes de grade, adicionar e exibir usam o padrão "contexto por operação", em que um contexto é criado para cada operação. O componente editar usa o padrão "context-per-component", 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 em funcionamento, incluindo as diretivas e necessárias @using @inject para Razor exemplos, consulte o aplicativo de exemplo.

Acesso ao banco de dados

EF Core se baseia em um como o meio de configurar o acesso ao banco de dados e DbContext atuar como uma unidade de trabalho. EF Core fornece a extensão para aplicativos ASP.NET Core que registram o contexto como um serviço AddDbContext com escopo por padrão. Em Blazor Server aplicativos, isso pode ser problemático 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 tempo de vida existentes são inadequados por estes motivos:

  • O Singleton compartilha o estado entre todos os usuários do aplicativo e leva a um 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 como os componentes podem ser de longa duração, isso resulta em um contexto de vida mais longa do que pode ser pretendido.

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

  • Por padrão, considere o uso de um contexto por operação. O contexto foi projetado para uma insta instaciaçã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 Loading = true; a linha no bloco try .

  • Para operações de vida mais longa que aproveitam o controle de EF Core controle de alterações ou simultância do EF Core ,de escopo do contexto para o tempo de vida do componente.

Novas instâncias dbContext

A maneira mais rápida de criar uma DbContext nova instância é usando para criar uma nova new instância. No entanto, há vários cenários que podem exigir a resolução de dependências adicionais. Por exemplo, talvez você queira usar DbContextOptions para configurar o contexto.

A solução recomendada para criar um novo DbContext com dependências é usar uma fábrica. O aplicativo de exemplo implementa sua própria fábrica no Data/DbContextFactory.cs .

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

        public TContext CreateDbContext()
        {
            if (provider == null)
            {
                throw new InvalidOperationException(
                    $"You must configure an instance of IServiceProvider");
            }

            return ActivatorUtilities.CreateInstance<TContext>(provider);
        }
    }
}

Na fábrica anterior:

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

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

A fábrica é injetada em componentes e usada para criar novas instâncias. Por exemplo, em Pages/Index.razor:

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

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

Novas DbContext instâncias podem ser criadas com uma fábrica que permite que você configure a cadeia de conexão por DbContext , como quando você usa o [modelo de ASP.NET Core Identity ]) (xref: segurança/autenticação/customize_identity_model):

services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

services.AddScoped<ApplicationDbContext>(p => 
    p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>()
    .CreateDbContext());

Escopo para o tempo de vida do componente

Talvez você queira criar um DbContext que existe 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 o controle de alterações e a 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 insira a fábrica conforme mostrado em 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();
}

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

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 é definido de volta como false , as operações assíncronas devem ser concluídas.
  • Coloque a lógica de tratamento de erro adicional em um catch bloco.

Habilitar log de dados confidenciais

EnableSensitiveDataLogging inclui dados de aplicativo em mensagens de exceção e log de estrutura. 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 dado. O registro em log de dados EnableSensitiveDataLogging é um risco de segurança, pois ele pode expor senhas e outras informações de identificação pessoal (PII) ao registrar as instruções SQL executadas no banco de dados.

Recomendamos habilenciar somente EnableSensitiveDataLogging 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