Condividi tramite


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

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Questo articolo illustra come usare Entity Framework Core (EF Core) nelle app lato Blazor server.

Il lato Blazor server è un framework di app con stato. L'app mantiene una connessione continua al server e lo stato dell'utente viene mantenuto nella memoria del server in un circuito. Un esempio di stato utente è costituito dai dati contenuti nelle istanze del servizio di inserimento delle dipendenze che hanno come ambito il circuito. Il modello di applicazione univoco che Blazor fornisce richiede un approccio speciale per l'uso di Entity Framework Core.

Nota

Questo articolo riguarda le EF Core app sul lato Blazor server. Blazor WebAssembly le app vengono eseguite in una sandbox WebAssembly che impedisce la maggior parte delle connessioni di database dirette. L'esecuzione EF Core in Blazor WebAssembly non rientra nell'ambito di questo articolo.

Queste linee guida si applicano ai componenti che adottano il rendering lato server interattivo (SSR interattivo) in un oggetto Blazor Web App.

Queste indicazioni si applicano al Server progetto di una soluzione ospitata Blazor WebAssembly o di un'app Blazor Server .

Flusso di autenticazione sicuro necessario per le app di produzione

Questo articolo usa un database locale che non richiede l'autenticazione utente. Le app di produzione devono usare il flusso di autenticazione più sicuro disponibile. Per altre informazioni sull'autenticazione per le app di test e produzione Blazor distribuite, vedere gli articoli nel Blazornodo Sicurezza e Identity distribuzione.

Per i servizi di Microsoft Azure, è consigliabile usare le identità gestite. Le identità gestite eseguono l'autenticazione sicura ai servizi di Azure senza archiviare le credenziali nel codice dell'app. Per ulteriori informazioni, vedi le seguenti risorse:

Esempio di app

L'app di esempio è stata compilata come riferimento per le app lato Blazor server che usano EF Core. L'app di esempio include una griglia con operazioni di ordinamento e filtro, eliminazione, aggiunta e aggiornamento.

L'esempio illustra l'uso di EF Core per gestire la concorrenza ottimistica. Tuttavia, i token di concorrenza generati dal database nativo non sono supportati per i database SQLite, ovvero il provider di database per l'app di esempio. Per illustrare la concorrenza con l'app di esempio, adottare un provider di database diverso che supporti i token di concorrenza generati dal database, ad esempio il provider SQL Server.

Visualizzare o scaricare il codice di esempio (procedura per il download): selezionare la cartella corrispondente alla versione di .NET che si sta adottando. All'interno della cartella della versione accedere all'esempio denominato BlazorWebAppEFCore.

Visualizzare o scaricare il codice di esempio (procedura per il download): selezionare la cartella corrispondente alla versione di .NET che si sta adottando. All'interno della cartella della versione accedere all'esempio denominato BlazorServerEFCoreSample.

L'esempio usa un database SQLite locale in modo che possa essere usato in qualsiasi piattaforma. L'esempio configura anche la registrazione del database per visualizzare le query SQL generate. Questa operazione è configurata in 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"
    }
  }
}

La griglia, l'aggiunta e la visualizzazione dei componenti usano il modello "context-per-operation", in cui viene creato un contesto per ogni operazione. Il componente di modifica usa il modello "context-per-component", in cui viene creato un contesto per ogni componente.

Nota

Alcuni degli esempi di codice in questo argomento richiedono spazi dei nomi e servizi non visualizzati. Per esaminare il codice completamente funzionante, incluse le direttive e obbligatorie @using per Razor esempi, vedere l'app @inject di esempio.

Esercitazione sulla creazione di un'app Blazor di database di film

Per un'esperienza di esercitazione sulla creazione di un'app che usa EF Core per usare un database, vedere Creare un'app Blazor di database di film (Panoramica). L'esercitazione illustra come creare un oggetto Blazor Web App in grado di visualizzare e gestire film in un database di film.

Accesso al database

EF Coresi basa su come DbContext mezzo per configurare l'accesso al database e fungere da unità di lavoro. EF Core fornisce l'estensione AddDbContext per le app ASP.NET Core che registra il contesto come servizio con ambito. Nelle app lato Blazor server, le registrazioni del servizio con ambito possono essere problematiche perché l'istanza viene condivisa tra i componenti all'interno del circuito dell'utente. DbContext non è thread-safe e non è progettato per l'uso simultaneo. Le durate esistenti non sono appropriate per questi motivi:

  • Singleton condivide lo stato in tutti gli utenti dell'app e porta a un uso simultaneo inappropriato.
  • L'ambito (impostazione predefinita) pone un problema simile tra i componenti per lo stesso utente.
  • I risultati temporanei in una nuova istanza per ogni richiesta, ma poiché i componenti possono essere di lunga durata, ciò comporta un contesto di durata superiore a quello previsto.

Le raccomandazioni seguenti sono progettate per offrire un approccio coerente all'uso EF Core nelle app lato Blazor server.

  • Prendere in considerazione l'uso di un contesto per ogni operazione. Il contesto è progettato per la creazione di istanze a sovraccarico rapido e basso:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Usare un flag per impedire più operazioni simultanee:

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

    Posizionare le operazioni dopo la Loading = true; riga nel try blocco.

    Thread safety non è un problema, quindi la logica di caricamento non richiede il blocco dei record del database. La logica di caricamento viene usata per disabilitare i controlli dell'interfaccia utente in modo che gli utenti non selezionino inavvertitamente pulsanti o aggiornino i campi durante il recupero dei dati.

  • Se è possibile che più thread possano accedere allo stesso blocco di codice, inserire una factory e creare una nuova istanza per ogni operazione. In caso contrario, l'inserimento e l'uso del contesto sono in genere sufficienti.

  • Per le operazioni di lunga durata che sfruttano EF Coreil rilevamento delle modifiche o il controllo della concorrenza, definire l'ambito del contesto per la durata del componente.

Nuove DbContext istanze

Il modo più rapido per creare una nuova DbContext istanza consiste nell'usare new per creare una nuova istanza. Esistono tuttavia scenari che richiedono la risoluzione di dipendenze aggiuntive:

Avviso

Non archiviare segreti dell'app, stringa di connessione, credenziali, password, numeri di identificazione personale (PIN), codice C#/.NET privato o chiavi/token privati nel codice lato client, che è sempre non sicuro. Negli ambienti di test/gestione temporanea e produzione, il codice lato Blazor server e le API Web devono usare flussi di autenticazione sicuri che evitano di mantenere le credenziali all'interno del codice del progetto o dei file di configurazione. Al di fuori dei test di sviluppo locali, è consigliabile evitare l'uso di variabili di ambiente per archiviare i dati sensibili, perché le variabili di ambiente non sono l'approccio più sicuro. Per i test di sviluppo locali, lo strumento Secret Manager è consigliato per proteggere i dati sensibili. Per altre informazioni, vedere Gestire in modo sicuro dati e credenziali sensibili.

L'approccio consigliato per creare un nuovo DbContext con le dipendenze consiste nell'usare una factory. EF Core 5.0 o versione successiva offre una factory predefinita per la creazione di nuovi contesti.

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

Nella factory precedente:

L'esempio seguente configura SQLite e abilita la registrazione dei dati. Il codice usa un metodo di estensione (AddDbContextFactory) per configurare la factory di database per l'inserimento delle dipendenze e fornire le opzioni predefinite:

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 factory viene inserita nei componenti e usata per creare nuove DbContext istanze.

home Nella pagina dell'app di esempio viene IDbContextFactory<ContactContext> inserito nel componente:

@inject IDbContextFactory<ContactContext> DbFactory

Un DbContext oggetto viene creato usando la factory (DbFactory) per eliminare un contatto nel DeleteContactAsync metodo :

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 è un oggetto inserito IContactFiltersed Wrapper è un riferimento al GridWrapper componente. Vedere il Home componente (Components/Pages/Home.razor) nell'app di esempio.

Nota

Filters è un oggetto inserito IContactFiltersed Wrapper è un riferimento al GridWrapper componente. Vedere il Index componente (Pages/Index.razor) nell'app di esempio.

Ambito della durata del componente

È possibile creare un oggetto DbContext esistente per la durata di un componente. In questo modo è possibile usarlo come unità di lavoro e sfruttare le funzionalità predefinite, ad esempio il rilevamento delle modifiche e la risoluzione della concorrenza.

È possibile usare la factory per creare un contesto e monitorarlo per la durata del componente. Prima di tutto, implementare IDisposable e inserire la factory come illustrato nel EditContact componente (Components/Pages/EditContact.razor):

È possibile usare la factory per creare un contesto e monitorarlo per la durata del componente. Prima di tutto, implementare IDisposable e inserire la factory come illustrato nel EditContact componente (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

L'app di esempio garantisce che il contesto venga eliminato quando il componente viene eliminato:

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

OnInitializedAsync Viene infine eseguito l'override per creare un nuovo contesto. Nell'app di esempio carica OnInitializedAsync il contatto nello stesso metodo:

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

Nell'esempio precedente:

  • Quando Busy è impostato su true, le operazioni asincrone possono iniziare. Quando Busy viene impostato di nuovo su false, le operazioni asincrone devono essere completate.
  • Inserire una logica aggiuntiva di gestione degli errori in un catch blocco.

Abilitare la registrazione dei dati sensibili

EnableSensitiveDataLogging include i dati dell'applicazione nei messaggi di eccezione e nella registrazione del framework. I dati registrati possono includere i valori assegnati alle proprietà delle istanze di entità e i valori dei parametri per i comandi inviati al database. La registrazione dei dati con EnableSensitiveDataLogging è un rischio per la sicurezza, in quanto può esporre password e altre informazioni personali (PII) quando registra le istruzioni SQL eseguite sul database.

È consigliabile abilitare EnableSensitiveDataLogging solo per lo sviluppo e il test:

#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

Risorse aggiuntive