Compartir vía


Blazor de ASP.NET Core 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, consulta la versión .NET 8 de este artículo.

Advertencia

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

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 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. Las aplicaciones Blazor WebAssembly 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 supera el ámbito de este artículo.

Esta guía se aplica a los componentes que adoptan la representación interactiva del lado 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

En este artículo se usa una base de datos local que no requiere autenticación del 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 aplicaciones Blazor de prueba y producción implementadas, consulta los siguientes artículos en el nodo BlazorSeguridad y Identity.

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:

Aplicación de ejemplo

La aplicación de ejemplo fue compilada como referencia para aplicaciones Blazor del lado del servidor que usan EF Core. La aplicación de ejemplo incluye una cuadrícula con operaciones de ordenación y filtrado, eliminación, adición y actualización.

En el ejemplo se muestra el uso de EF Core para controlar la simultaneidad optimista. Sin embargo, los tokens de simultaneidad generados por bases de datos nativas no son compatibles con las bases de datos SQLite, que es el proveedor de bases de datos de la aplicación de ejemplo. Para demostrar la simultaneidad con la aplicación de ejemplo, adopte un proveedor de base de datos diferente que admita tokens de simultaneidad generados por la base de datos (por ejemplo, el proveedor de SQL Server).

Ver o descargar código de ejemplo (cómo descargar): seleccione la carpeta que coincida con la versión de .NET que está adoptando. Dentro de la carpeta de versión, acceda al ejemplo denominado BlazorWebAppEFCore.

Ver o descargar código de ejemplo (cómo descargar): seleccione la carpeta que coincida con la versión de .NET que está adoptando. Dentro de la carpeta de versión, acceda al ejemplo denominado BlazorServerEFCoreSample.

En el ejemplo se usa una base de datos SQLite local para que se pueda utilizar en cualquier plataforma. En el ejemplo también se configura el registro de base de datos para mostrar las consultas SQL que se generan. Esto se configura en 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"
    }
  }
}

Los componentes de cuadrícula, adición y vista usan el patrón de "contexto por operación", en el que se crea un contexto para cada operación. El componente de edición usa el patrón de "contexto por componente", en el que se crea un contexto para cada componente.

Nota

En algunos de los ejemplos de código de este tema se necesitan espacios de nombres y servicios que no se muestran. Si deseas inspeccionar el código totalmente operativo, como las directivas obligatorias @using e @inject para ejemplos de Razor, consulta la aplicación de muestra.

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 usa EF Core para trabajar con una base de datos, consulte Compilación de una Blazor aplicación de base de datos de películas (Informació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. En las aplicaciones Blazor del lado del servidor, los registros con ámbito de servicio 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 MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Usa una marca para evitar varias operaciones simultáneas:

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

    Coloca las operaciones 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 capturan los datos.

  • Si hay alguna posibilidad de que varios subprocesos tengan acceso al mismo bloque de código, inserta un generador y realiza una nueva instancia por operación. De lo contrario, la inserción y el uso del contexto suelen ser suficientes.

  • 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 instancia de DbContext consiste en usar new. Sin embargo, hay escenarios que requieren que se resuelvan dependencias adicionales:

Advertencia

No almacene secretos de aplicación, cadena de conexión s, 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 entornos de prueba/ensayo y producción, el código del lado Blazor servidor y las API web deben usar 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. En EF Core 5.0 o posterior se proporciona un generador integrado para crear 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);
    }
}

En el generador anterior:

En el ejemplo siguiente se configura SQLite y se habilita el registro de datos. El código usa un método de extensión (AddDbContextFactory) para configurar el generador de bases de datos para la inserción de dependencias y proporcionar opciones predeterminadas:

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

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

En la página home de la aplicación de ejemplo, se inserta IDbContextFactory<ContactContext> en el componente:

@inject IDbContextFactory<ContactContext> DbFactory

Se crea DbContext mediante la fábrica (DbFactory) para eliminar un contacto en el 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();
}

Nota:

Filters es un elemento IContactFilters insertado y Wrapper es una referencia de componente al componente GridWrapper. Consulta el componente Home (Components/Pages/Home.razor) en la aplicación de ejemplo.

Nota:

Filters es un elemento IContactFilters insertado y Wrapper es una referencia de componente al componente GridWrapper. Consulta el componente Index (Pages/Index.razor) en la aplicación de ejemplo.

Ámbito de la duración del componente

Es posible que quieras 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 simultaneidad.

Puedes usar el generador para crear un contexto y realizar su seguimiento mientras dure el componente. En primer lugar, implementa IDisposable e inserta la fábrica como se muestra en el componente EditContact (Components/Pages/EditContact.razor):

Puede usar el generador para crear un contexto y realizar su seguimiento mientras dure el componente. En primer lugar, implemente IDisposable e inserte la fábrica como se muestra en el componente EditContact (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

La aplicación de ejemplo garantiza que el contexto se desecha cuando se desecha el componente:

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 último, se invalida OnInitializedAsync para crear un contexto. En la aplicación de ejemplo, OnInitializedAsync carga el contacto en el mismo 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();
}

En el ejemplo anterior:

  • Cuando Busy se establece en true, pueden comenzar las operaciones asincrónicas. Cuando Busy se vuelve a establecer en false, las operaciones asincrónicas deben finalizar.
  • Coloque la lógica de control de errores adicional en un bloque catch.

Habilitar el registro de datos confidenciales

EnableSensitiveDataLogging incluye datos de la aplicación en los mensajes de excepción y en la plataforma de registro. 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 desarrollo y pruebas:

#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 adicionales