ASP.NET Core Blazor con Entity Framework Core (EF Core)

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 10 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la Directiva de soporte de .NET y .NET Core. Para la versión actual, consulte la versión de .NET 10 de este artículo.

Este artículo explica cómo usar Entity Framework Core (EF Core) en aplicaciones Blazor del lado del servidor.

El Blazor del lado servidor es un marco para aplicaciones con estado. La aplicación mantiene una conexión continua con el servidor, y el estado del usuario se mantiene en la memoria del servidor en un circuito. Un ejemplo de estado del usuario son los datos contenidos en las instancias de servicio de la inserción de dependencias (DI) que se encuentran en el ámbito del circuito. El modelo de aplicación único que proporciona Blazor requiere un enfoque especial para usar Entity Framework Core.

Nota

Este artículo aborda EF Core en aplicaciones Blazor del lado del servidor. Blazor WebAssembly aplicaciones se ejecutan en un espacio aislado de WebAssembly que evita la mayoría de conexiones de base de datos directas. La ejecución de EF Core en Blazor WebAssembly queda fuera del alcance de este artículo.

Esta guía se aplica a los componentes que adoptan la representación interactiva en el servidor (SSR interactivo) en una Blazor Web App.

Esta guía se aplica al proyecto Server de una solución hospedada Blazor WebAssembly o a una aplicación Blazor Server.

Flujo de autenticación seguro necesario para aplicaciones de producción

Este artículo pertenece al uso de una base de datos local que no requiere autenticación de usuario. Las aplicaciones de producción deben usar el flujo de autenticación más seguro disponible. Para obtener más información sobre la autenticación para Blazor aplicaciones de prueba y producción implementadas, consulte los artículos en el nodo BlazorSeguridad e Identity nodo.

Para los servicios de Microsoft Azure, se recomienda usar identidades administradas. Las identidades administradas proporcionan una autenticación segura en los servicios de Azure sin almacenar credenciales en el código de la aplicación. Para obtener más información, consulta los siguientes recursos:

Tutorial de creación de una Blazor aplicación de base de datos de películas

Para obtener una experiencia de tutorial sobre la creación de una aplicación que utiliza EF Core en las operaciones de base de datos, consulte Construcción de una aplicación de base de datos de películas de Blazor (Descripción general). En el tutorial se muestra cómo crear un objeto Blazor Web App que pueda mostrar y administrar películas en una base de datos de películas.

Acceso a la base de datos

EF Core se basa en un objeto DbContext como medio para configurar el acceso a la base de datos y actuar como una unidad de trabajo. EF Core proporciona la extensión AddDbContext para las aplicaciones de ASP.NET Core, que registra el contexto como un servicio con ámbito de forma predeterminada. En las aplicaciones Blazor del lado del servidor, los registros de servicios con ámbito pueden ser problemáticos, ya que la instancia se comparte entre los componentes del circuito del usuario. DbContext no es seguro para subprocesos y no está diseñado para su uso simultáneo. Las duraciones existentes no son adecuadas por estos motivos:

  • Singleton comparte el estado entre todos los usuarios de la aplicación y lleva a un uso simultáneo inadecuado.
  • Con ámbito (el valor predeterminado) supone un problema similar entre los componentes para el mismo usuario.
  • Transitorio genera una nueva instancia por solicitud, pero como los componentes pueden ser de larga duración, esto da lugar a un contexto que dura más de lo previsto.

Las recomendaciones siguientes están diseñadas para proporcionar un enfoque coherente al uso de EF Core en aplicaciones Blazor del lado del servidor.

  • Considera usar un contexto por operación. El contexto está diseñado para la creación de instancias rápidas de baja sobrecarga:

    using var context = new ProductsDatabaseContext();
    
    return await context.Products.ToListAsync();
    
  • Usa una bandera para evitar varias operaciones simultáneas:

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

    Coloque las operaciones de base de datos después de la línea Loading = true; en el bloque try.

    La lógica de carga no requiere bloquear registros de base de datos porque la seguridad para subprocesos no es una preocupación. La lógica de carga se usa para deshabilitar los controles de interfaz de usuario para que los usuarios no seleccionen accidentalmente botones ni actualicen campos mientras se obtienen los datos.

  • Si hay alguna posibilidad de que varios subprocesos tengan acceso al mismo bloque de código, inserte un generador y realice una nueva instancia por operación. De lo contrario, inyectar y usar el contexto suele ser suficiente.

  • En el caso de las operaciones de larga duración que aprovechan el seguimiento de cambios o el control de simultaneidad de EF Core, el ámbito del contexto debe ser la duración del componente.

Nuevas instancias de DbContext

La manera más rápida de crear una nueva instancia de DbContext consiste en usar new para crear una nueva instancia. Sin embargo, hay escenarios que requieren que se resuelvan dependencias adicionales:

Advertencia

No almacene secretos de aplicación, cadenas de conexión, credenciales, contraseñas, números de identificación personal (PIN), código C#/.NET privado o claves o tokens privados en el código del lado cliente, lo que siempre es inseguro. En los entornos de prueba/establecimiento y producción, el código del lado del servidor Blazor y las API web deben utilizar flujos de autenticación seguros que eviten mantener las credenciales dentro del código del proyecto o los archivos de configuración. Fuera de las pruebas de desarrollo local, se recomienda evitar el uso de variables de entorno para almacenar datos confidenciales, ya que las variables de entorno no son el enfoque más seguro. Para las pruebas de desarrollo local, se recomienda la herramienta Secret Manager para proteger los datos confidenciales. Para obtener más información, consulte Mantener de forma segura los datos confidenciales y las credenciales.

El enfoque recomendado para crear un nuevo DbContext con dependencias es usar una fábrica. EF Core en .NET 5 o posterior se proporciona un generador integrado para crear contextos.

En las versiones de .NET anteriores a .NET 5, use lo siguiente DbContextFactory:

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

En el generador anterior:

En el ejemplo siguiente se configura SQLite y se habilita el registro de datos en una aplicación que administra los contactos. El código usa un método de extensión (AddDbContextFactory) para configurar la factoría de base de datos para la inyección de dependencias y proporcionar opciones predeterminadas:

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

La fábrica se inserta en los componentes para crear instancias de DbContext.

Delimitar el ámbito de un contexto de base de datos para un método de componente

El factoría se inserta en el componente:

@inject IDbContextFactory<ContactContext> DbFactory

Cree un DbContext para un método mediante la factoría (DbFactory):

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    if (context.Contacts is not null)
    {
        var contact = await context.Contacts.FirstAsync(...);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }
}

Delimitar un contexto de base de datos a la duración del componente

Es posible que quiera crear un objeto DbContext que exista mientras dure un componente. Esto te permite usarlo como una unidad de trabajo y aprovechar características integradas, como el seguimiento de cambios y la resolución de conflictos de concurrencia.

Implemente IDisposable e inserte la factoría en el componente:

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Establezca una propiedad para el DbContext:

private ContactContext? Context { get; set; }

OnInitializedAsync se invalida para crear el DbContext:

protected override async Task OnInitializedAsync()
{
    Context = DbFactory.CreateDbContext();
}

El DbContext se elimina cuando se elimina el componente:

public void Dispose() => Context?.Dispose();

Habilitar el registro de datos confidenciales

EnableSensitiveDataLogging incluye datos de la aplicación en los mensajes de excepción y en el registro del framework. Los datos registrados pueden incluir los valores asignados a las propiedades de las instancias de entidad y los valores de parámetro para los comandos enviados a la base de datos. El registro de datos con EnableSensitiveDataLogging es un riesgo de seguridad, ya que puede exponer contraseñas y otra Información de identificación personal (PII) cuando registra instrucciones SQL ejecutadas en la base de datos.

Se recomienda habilitar EnableSensitiveDataLogging solo para el desarrollo y las pruebas locales:

if (builder.Environment.IsDevelopment())
{
    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"));
}