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

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

In diesem Artikel wird erläutert, wie Sie Entity Framework Core (EF Core) in serverseitigen Blazor-Apps verwenden.

Serverseitiges Blazor ist ein zustandsbehaftetes App-Framework. Die App unterhält eine kontinuierliche Verbindung mit dem Server, und der Zustand des Benutzers wird im Arbeitsspeicher des Servers in einer Verbindung gespeichert. Ein Beispiel für den Benutzerzustand sind Daten, die in Dienstinstanzen mit -Abhängigkeitsinjektion (DI) gespeichert sind, die auf die Verbindung beschränkt sind. Das eindeutige Anwendungsmodell, das Blazor bereitstellt, erfordert eine besondere Vorgehensweise bei der Verwendung von Entity Framework Core.

Hinweis

In diesem Artikel wird EF Core in serverseitigen Blazor-Apps behandelt. Blazor WebAssembly-Apps werden in einer WebAssembly-Sandbox ausgeführt, die die meisten direkten Datenbankverbindungen verhindert. Die Ausführung von EF Core in Blazor WebAssembly sprengt jedoch den Rahmen dieses Artikels.

Dieser Leitfaden gilt für Komponenten, die interaktives serverseitiges Rendering (interactive SSR) in einer Blazor-Web App übernehmen.

Dieser Leitfaden gilt für das Server Projekt einer gehosteten Blazor WebAssembly Lösung oder einer Blazor Server App.

Beispiel-App

Die Beispiel-App wurde als Referenz für serverseitige Blazor-Apps erstellt, die EF Core verwenden. Die Beispiel-App enthält ein Raster mit Sortier- und Filter-, Lösch-, Hinzufügungs- und Aktualisierungsvorgängen. Das Beispiel veranschaulicht die Verwendung von EF Core zum Verarbeiten vollständiger Parallelität.

Anzeigen oder Herunterladen von Beispielcode (Downloadanleitung): Wählen Sie den Ordner aus, der der Version von .NET entspricht, die Sie übernehmen. Greifen Sie im Ordner „version“ auf das Beispiel mit dem Namen BlazorWebAppEFCore zu.

Anzeigen oder Herunterladen von Beispielcode (Downloadanleitung): Wählen Sie den Ordner aus, der der Version von .NET entspricht, die Sie übernehmen. Greifen Sie im Ordner „version“ auf das Beispiel mit dem Namen BlazorServerEFCoreSample zu.

Das Beispiel verwendet eine lokale SQLite-Datenbank, sodass es auf jeder Plattform verwendet werden kann. Das Beispiel konfiguriert auch die Datenbankprotokollierung, um die generierten SQL-Abfragen anzuzeigen. Dies wird in appsettings.Development.json konfiguriert:

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

Die Raster-, Hinzufügungs- und Anzeigekomponenten verwenden das Muster „Kontext pro Vorgang“, bei dem ein Kontext für jeden Vorgang erstellt wird. Die Bearbeitungskomponente verwendet das Muster „Kontext pro Komponente“, bei dem ein Kontext für jede Komponente erstellt wird.

Hinweis

Einige der Codebeispiele in diesem Thema erfordern Namespaces und Dienste, die nicht gezeigt werden. Um den voll funktionsfähigen Code einschließlich der erforderlichen @using- und @inject-Anweisungen für Razor-Beispiele zu untersuchen, ziehen Sie die Beispiel-App zurate.

Datenbankzugriff

EF Core stützt sich auf einen DbContext als Mittel zur Konfiguration des Datenbankzugriffs und um als Arbeitseinheit zu fungieren. EF Core stellt die AddDbContext-Erweiterung für ASP.NET Core-Apps bereit, die den Kontext standardmäßig als bereichsbezogenen Dienst registriert. In serverseitigen Blazor-Apps können bereichsbezogene Dienstregistrierungen problematisch sein, da die Instanz innerhalb der Verbindung des Benutzers bzw. der Benutzerin von allen Komponenten gemeinsam genutzt wird. DbContext ist nicht threadsicher und nicht für gleichzeitige Verwendung vorgesehen. Die vorhandenen Lebensdauern sind aus den folgenden Gründen ungeeignet:

  • Singleton verwendet den Zustand für alle Benutzer der App und führt zu einer ungeeigneten gleichzeitigen Verwendung.
  • Scoped (Bereichsbezogen) (die Standardeinstellung) stellt ein ähnliches Problem zwischen Komponenten für denselben Benutzer dar.
  • Transient (Vorübergehend) führt zu einer neuen Instanz pro Anforderung. Da Komponenten jedoch langlebig sein können, führt dies zu einem längerlebigen Kontext als möglicherweise beabsichtigt.

Die folgenden Empfehlungen sind darauf ausgelegt, einen konsistenten Ansatz zur Verwendung von EF Core in serverseitigen Blazor-Apps zu schaffen.

  • Erwägen Sie standardmäßig die Verwendung eines Kontexts pro Vorgang. Der Kontext ist für schnelle Instanziierung mit geringem Mehraufwand konzipiert:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Verwenden Sie ein Flag, um mehrere gleichzeitige Vorgänge zu verhindern:

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

    Platzieren Sie Vorgänge nach der Zeile Loading = true; im try-Block.

    Ladelogik erfordert keine Sperrung von Datenbankdatensätzen, da die Threadsicherheit kein Problem darstellt. Die Ladelogik wird verwendet, um Benutzeroberflächensteuerelemente zu deaktivieren, sodass Benutzer nicht versehentlich Schaltflächen auswählen oder Felder aktualisieren, während Daten abgerufen werden.

  • Wenn es möglich ist, dass mehrere Threads auf denselben Codeblock zugreifen könnten, fügen Sie eine Factory ein, und erstellen Sie eine neue Instanz pro Vorgang. Andernfalls reicht das Einfügen und das Verwenden des Kontexts in der Regel aus.

  • Bei langlebigen Vorgängen, die die EF Core-Vorteile der Änderungsnachverfolgung oder Parallelitätssteuerung nutzen, passen Sie den Kontext an die Lebensdauer der Komponente an.

Neue DbContext-Instanzen

Die schnellste Möglichkeit zum Erstellen einer neuen DbContext-Instanz ist die Verwendung von new, um eine neue Instanz zu erstellen. Es gibt jedoch Szenarien, in denen möglicherweise zusätzliche Abhängigkeiten aufgelöst werden müssen:

Der empfohlene Lösung zum Erstellen eines neuen DbContext mit Abhängigkeiten besteht in der Verwendung einer Factory. EF Core 5.0 oder höher bietet eine integrierte Factory zum Erstellen neuer Kontexte.

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

In der vorhergehenden Factory gilt Folgendes:

Im folgenden Beispiel wird SQLite konfiguriert und Datenprotokollierung aktiviert. Der Code verwendet eine Erweiterungsmethode (AddDbContextFactory) zum Konfigurieren der Datenbankfactory für DI und bietet Standardoptionen:

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

Die Factory wird in Komponenten eingefügt und zum Erstellen neuer DbContext-Instanzen verwendet.

Auf der Willkommensseite der Beispiel-App wird IDbContextFactory<ContactContext> in die Komponente eingefügt:

@inject IDbContextFactory<ContactContext> DbFactory

Ein DbContext wird mithilfe der Factory (DbFactory) erstellt, um einen Kontakt in der DeleteContactAsync-Methode zu löschen:

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

Hinweis

Filters ist ein injiziertes IContactFilters-Element und Wrapper ist ein Komponentenverweis auf die GridWrapper-Komponente. Sehen Sie sich dazu die Home-Komponente (Components/Pages/Home.razor) in der Beispiel-App an.

Hinweis

Filters ist ein injiziertes IContactFilters-Element und Wrapper ist ein Komponentenverweis auf die GridWrapper-Komponente. Sehen Sie sich dazu die Index-Komponente (Pages/Index.razor) in der Beispiel-App an.

Gültigkeitsbereich für die Komponentenlebensdauer

Möglicherweise möchten Sie einen DbContext erstellen, der für die Lebensdauer einer Komponente vorhanden ist. Dies ermöglicht Ihnen die Verwendung als Arbeitseinheit und nutzt integrierte Features, z. B. Änderungsnachverfolgung und Parallelitätsauflösung.

Sie können die Factory verwenden, um einen Kontext zu erstellen und diesen für die gesamte Lebensdauer der Komponente nachzuverfolgen. Implementieren Sie zunächst IDisposable, und fügen Sie die Factory dann wie gezeigt in die EditContact-Komponente (Components/Pages/EditContact.razor) ein:

Sie können die Factory verwenden, um einen Kontext zu erstellen und diesen für die gesamte Lebensdauer der Komponente nachzuverfolgen. Implementieren Sie zunächst IDisposable, und fügen Sie die Factory dann wie gezeigt in die EditContact-Komponente (Pages/EditContact.razor) ein:

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Die Beispiel-App stellt sicher, dass der Kontext verworfen wird, wenn die Komponente verworfen wird:

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

Schließlich wird OnInitializedAsync überschrieben, um einen neuen Kontext zu erstellen. In der Beispiel-App lädt OnInitializedAsync den Kontakt in derselben Methode:

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

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

Im vorherigen Beispiel:

  • Wenn Busy auf true festgelegt ist, können asynchrone Vorgänge beginnen. Wenn Busy auf false zurückgesetzt wird, sollten asynchrone Vorgänge abgeschlossen sein.
  • Fügen Sie zusätzliche Fehlerbehandlungslogik in einen catch-Block ein.

Aktivieren der Protokollierung von vertraulicher Daten

EnableSensitiveDataLogging schließt Anwendungsdaten in Ausnahmemeldungen und Frameworkprotokolle ein. Die protokollierten Daten können die den Eigenschaften von Entitätsinstanzen zugewiesenen Werte enthalten sowie Parameterwerte für Befehle, die an die Datenbank gesendet werden. Das Protokollieren von Daten mit EnableSensitiveDataLogging stellt ein Sicherheitsrisiko dar, da damit möglicherweise Kennwörter und andere personenbezogene Informationen (PII) verfügbar gemacht werden, wenn SQL-Anweisungen für die Datenbank protokolliert werden.

Es wird empfohlen, EnableSensitiveDataLogging ausschließlich für Entwicklung und Tests zu aktivieren:

#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

Zusätzliche Ressourcen