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

El servidor Blazor Server 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 Server requiere un enfoque especial para usar Entity Framework Core.

Nota

En este artículo se describe EF Core en aplicaciones Blazor Server. 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.

Aplicación de ejemplo

La aplicación de ejemplo se ha compilado como una referencia para las aplicaciones Blazor Server en las que se usa 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.

Vea o descargue el código de ejemplo (cómo descargarlo)

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

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. Para inspeccionar el código totalmente operativo, incluidas las directivas obligatorias @using e @inject para ejemplos de Razor, vea la aplicación de ejemplo.

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 Server, 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 Server.

  • De forma predeterminada, considere la posibilidad de 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();
    
  • Use una marca para evitar varias operaciones simultáneas:

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

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

  • 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. Pero hay varios escenarios es los que puede ser necesario resolver dependencias adicionales. Por ejemplo, es posible que quiera usar DbContextOptions para configurar el contexto.

La solución recomendada para crear un objeto DbContext con dependencias es usar un generador. En EF Core 5.0 o posterior se proporciona un generador integrado para crear contextos.

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

El generador se inserta en los componentes y se usa para crear instancias. Por ejemplo, en Pages/Index.razor:

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

    if (Wrapper is not null)
    {
        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

Wrapper es una referencia de componente del componente de GridWrapper. Vea el componente de Index (Pages/Index.razor) en la aplicación de ejemplo.

Se pueden crear instancias de DbContext con un generador que permite configurar la cadena de conexión por DbContext, como cuando se usa el modelo Identity de ASP.NET Core. Para obtener más información, consulte el blog sobre servicios multiinquilino con EF Core en Blazor Server Apps.

Ámbito de la duración del componente

Es posible que quiera crear un objeto DbContext que exista mientras dure un componente. Esto le permite usarlo como una unidad de trabajo y aprovechar características integradas como el seguimiento de cambios y la resolución de simultaneidad. Puede usar el generador para crear un contexto y realizar su seguimiento mientras dure el componente. En primer lugar, implemente IDisposable e inserte el generador como se muestra en 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();
}

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

    await base.OnInitializedAsync();
}

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

El servidor Blazor Server 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 Server requiere un enfoque especial para usar Entity Framework Core.

Nota

En este artículo se describe EF Core en aplicaciones Blazor Server. 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.

Aplicación de ejemplo

La aplicación de ejemplo se ha compilado como una referencia para las aplicaciones Blazor Server en las que se usa 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.

Vea o descargue el código de ejemplo (cómo descargarlo)

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": "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. Para inspeccionar el código totalmente operativo, incluidas las directivas obligatorias @using e @inject para ejemplos de Razor, vea la aplicación de ejemplo.

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 Server, 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 Server.

  • De forma predeterminada, considere la posibilidad de 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();
    
  • Use una marca para evitar varias operaciones simultáneas:

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

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

  • 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. Pero hay varios escenarios es los que puede ser necesario resolver dependencias adicionales. Por ejemplo, es posible que quiera usar DbContextOptions para configurar el contexto.

La solución recomendada para crear un objeto DbContext con dependencias es usar un generador. En EF Core 5.0 o posterior se proporciona un generador integrado para crear contextos.

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:

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

El generador se inserta en los componentes y se usa para crear instancias. Por ejemplo, en 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();
}

Nota

Wrapper es una referencia de componente del componente de GridWrapper. Vea el componente de Index (Pages/Index.razor) en la aplicación de ejemplo.

Se pueden crear instancias de DbContext con un generador que permite configurar la cadena de conexión por DbContext, como cuando se usa el modelo Identity de ASP.NET Core. Para obtener más información, consulte el blog sobre servicios multiinquilino con EF Core en Blazor Server Apps.

Ámbito de la duración del componente

Es posible que quiera crear un objeto DbContext que exista mientras dure un componente. Esto le permite usarlo como una unidad de trabajo y aprovechar características integradas como el seguimiento de cambios y la resolución de simultaneidad. Puede usar el generador para crear un contexto y realizar su seguimiento mientras dure el componente. En primer lugar, implemente IDisposable e inserte el generador como se muestra en 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();
}

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

    await base.OnInitializedAsync();
}

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

El servidor Blazor Server 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 Server requiere un enfoque especial para usar Entity Framework Core.

Nota

En este artículo se describe EF Core en aplicaciones Blazor Server. 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.

Aplicación de ejemplo

La aplicación de ejemplo se ha compilado como una referencia para las aplicaciones Blazor Server en las que se usa 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.

Vea o descargue el código de ejemplo (cómo descargarlo)

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": "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. Para inspeccionar el código totalmente operativo, incluidas las directivas obligatorias @using e @inject para ejemplos de Razor, vea la aplicación de ejemplo.

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 Server, esto puede suponer un problema, 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 Server.

  • De forma predeterminada, considere la posibilidad de 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();
    
  • Use una marca para evitar varias operaciones simultáneas:

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

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

  • 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. Pero hay varios escenarios es los que puede ser necesario resolver dependencias adicionales. Por ejemplo, es posible que quiera usar DbContextOptions para configurar el contexto.

La solución recomendada para crear un objeto DbContext con dependencias es usar un generador. La aplicación de ejemplo implementa su propio generador en 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 ?? 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 para configurar el generador de bases de datos para la inserción de dependencias y proporcionar opciones predeterminadas:

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

El generador se inserta en los componentes y se usa para crear instancias. Por ejemplo, en 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();
}

Nota

Wrapper es una referencia de componente del componente de GridWrapper. Vea el componente de Index (Pages/Index.razor) en la aplicación de ejemplo.

Se pueden crear instancias de DbContext con un generador que permite configurar la cadena de conexión por DbContext, como cuando se usa [modelo de Identity de ASP.NET Core])(xref:security/authentication/customize_identity_model). Para obtener más información, consulte el blog sobre servicios multiinquilino con EF Core en Blazor Server Apps.

Ámbito de la duración del componente

Es posible que quiera crear un objeto DbContext que exista mientras dure un componente. Esto le permite usarlo como una unidad de trabajo y aprovechar características integradas como el seguimiento de cambios y la resolución de simultaneidad. Puede usar el generador para crear un contexto y realizar su seguimiento mientras dure el componente. En primer lugar, implemente IDisposable e inserte el generador como se muestra en 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();
}

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