ASP.NET Core Blazor z programem Entity Framework Core (EF Core)

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

W tym artykule wyjaśniono, jak używać programu Entity Framework Core (EF Core) w aplikacjach po stronie Blazor serwera.

Po stronie Blazor serwera jest stanowa struktura aplikacji. Aplikacja utrzymuje bieżące połączenie z serwerem, a stan użytkownika jest przechowywany w pamięci serwera w obwodzie. Przykładem stanu użytkownika są dane przechowywane w wystąpieniach usługi wstrzykiwania zależności (DI), które są ograniczone do obwodu. Unikatowy model aplikacji, który udostępnia, Blazor wymaga specjalnego podejścia do korzystania z platformy Entity Framework Core.

Uwaga

Ten artykuł dotyczy EF Core aplikacji po stronie Blazor serwera. Blazor WebAssembly aplikacje są uruchamiane w piaskownicy zestawu WebAssembly, która uniemożliwia korzystanie z większości bezpośrednich połączeń z bazą danych. Uruchamianie EF Core w programie Blazor WebAssembly wykracza poza zakres tego artykułu.

Te wskazówki dotyczą składników, które przyjmują interakcyjne renderowanie po stronie serwera (interakcyjne SSR) w Blazor aplikacji internetowej.

Te wskazówki dotyczą Server projektu hostowanego Blazor WebAssembly rozwiązania lub Blazor Server aplikacji.

Przykładowa aplikacja

Przykładowa aplikacja została utworzona jako odwołanie do aplikacji po stronie Blazor serwera, które używają polecenia EF Core. Przykładowa aplikacja zawiera siatkę z operacjami sortowania i filtrowania, usuwania, dodawania i aktualizowania. W przykładzie pokazano użycie funkcji do obsługi optymistycznej EF Core współbieżności.

Wyświetl lub pobierz przykładowy kod (jak pobrać): wybierz folder zgodny z wdrażaną wersją platformy .NET. W folderze wersji uzyskaj dostęp do przykładu o nazwie BlazorWebAppEFCore.

Wyświetl lub pobierz przykładowy kod (jak pobrać): wybierz folder zgodny z wdrażaną wersją platformy .NET. W folderze wersji uzyskaj dostęp do przykładu o nazwie BlazorServerEFCoreSample.

W przykładzie użyto lokalnej bazy danych SQLite , aby można było jej używać na dowolnej platformie. Przykład umożliwia również skonfigurowanie rejestrowania bazy danych w celu wyświetlenia wygenerowanych zapytań SQL. Jest to skonfigurowane w programie 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": "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"
    }
  }
}

Składniki siatki, dodawania i wyświetlania używają wzorca "kontekst na operację", w którym jest tworzony kontekst dla każdej operacji. Składnik edycji używa wzorca "context-per-component", w którym kontekst jest tworzony dla każdego składnika.

Uwaga

Niektóre przykłady kodu w tym temacie wymagają przestrzeni nazw i usług, które nie są wyświetlane. Aby sprawdzić w pełni działający kod, w tym wymagane @using dyrektywy i @inject przykłady Razor , zobacz przykładową aplikację.

Dostęp do bazy danych

EF Core opiera się na metodzie DbContextkonfigurowania dostępu do bazy danych i działania jako jednostki pracy. EF CoreAddDbContext Udostępnia rozszerzenie dla aplikacji ASP.NET Core, które domyślnie rejestrują kontekst jako usługę o określonym zakresie. W aplikacjach po stronie Blazor serwera rejestracje usług w zakresie mogą być problematyczne, ponieważ wystąpienie jest współużytkowane między składnikami w obwodzie użytkownika. DbContext nie jest bezpieczny wątkiem i nie jest przeznaczony do współbieżnego użycia. Istniejące okresy istnienia są nieodpowiednie z następujących powodów:

  • Stan pojedynczego udziału dla wszystkich użytkowników aplikacji i prowadzi do niewłaściwego współbieżnego użycia.
  • Zakres (wartość domyślna) stanowi podobny problem między składnikami dla tego samego użytkownika.
  • Przejściowe wyniki w nowym wystąpieniu na żądanie, ale ponieważ składniki mogą być długotrwałe, powoduje to dłuższy kontekst niż może być zamierzony.

Poniższe zalecenia zostały zaprojektowane w celu zapewnienia spójnego podejścia do korzystania z EF Core aplikacji po stronie Blazor serwera.

  • Domyślnie rozważ użycie jednego kontekstu na operację. Kontekst jest przeznaczony do szybkiego tworzenia wystąpienia niskiego obciążenia:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Użyj flagi, aby zapobiec wielu równoczesnym operacjom:

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

    Umieść operacje po Loading = true; wierszu w try bloku.

    Logika ładowania nie wymaga blokowania rekordów bazy danych, ponieważ bezpieczeństwo wątku nie jest problemem. Logika ładowania służy do wyłączania kontrolek interfejsu użytkownika, dzięki czemu użytkownicy nie mogą przypadkowo wybierać przycisków ani aktualizować pól podczas pobierania danych.

  • Jeśli istnieje prawdopodobieństwo, że wiele wątków może uzyskać dostęp do tego samego bloku kodu, wstrzyknąć fabrykę i utworzyć nowe wystąpienie na operację. W przeciwnym razie wstrzykiwanie i używanie kontekstu jest zwykle wystarczające.

  • W przypadku długotrwałych operacji, które korzystają ze EF Coreśledzenia zmian lub kontroli współbieżności, określ zakres kontekstu na okres istnienia składnika.

Nowe DbContext wystąpienia

Najszybszym sposobem utworzenia nowego DbContext wystąpienia jest utworzenie new nowego wystąpienia. Istnieją jednak scenariusze wymagające rozwiązania dodatkowych zależności:

Zalecanym podejściem do utworzenia nowego DbContext elementu z zależnościami jest użycie fabryki. EF Core Wersja 5.0 lub nowsza udostępnia wbudowaną fabrykę do tworzenia nowych kontekstów.

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

W poprzedniej fabryce:

W poniższym przykładzie skonfigurowaliśmy bibliotekę SQLite i włączono rejestrowanie danych. Kod używa metody rozszerzenia (AddDbContextFactory), aby skonfigurować fabrykę baz danych dla di i udostępnić opcje domyślne:

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

Fabryka jest wstrzykiwana do składników i używana do tworzenia nowych DbContext wystąpień.

Na stronie głównej przykładowej aplikacji IDbContextFactory<ContactContext> jest wstrzykiwany do składnika:

@inject IDbContextFactory<ContactContext> DbFactory

Obiekt DbContext jest tworzony przy użyciu fabryki (DbFactory) w celu usunięcia kontaktu w metodzie 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;

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

Uwaga

Filters jest wstrzykniętym IContactFilterselementem i Wrapper jest odwołaniem doGridWrapper składnika. Home Zobacz składnik (Components/Pages/Home.razor) w przykładowej aplikacji.

Uwaga

Filters jest wstrzykniętym IContactFilterselementem i Wrapper jest odwołaniem doGridWrapper składnika. Index Zobacz składnik (Pages/Index.razor) w przykładowej aplikacji.

Zakres do okresu istnienia składnika

Możesz utworzyć element DbContext , który istnieje przez okres istnienia składnika. Dzięki temu można używać go jako jednostki pracy i korzystać z wbudowanych funkcji, takich jak śledzenie zmian i rozpoznawanie współbieżności.

Za pomocą fabryki można utworzyć kontekst i śledzić go przez cały okres istnienia składnika. Najpierw zaimplementuj IDisposable i wstrzyknąć fabrykę, jak pokazano w składniku EditContact (Components/Pages/EditContact.razor):

Za pomocą fabryki można utworzyć kontekst i śledzić go przez cały okres istnienia składnika. Najpierw zaimplementuj IDisposable i wstrzyknąć fabrykę, jak pokazano w składniku EditContact (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Przykładowa aplikacja gwarantuje, że kontekst zostanie usunięty po usunięciu składnika:

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

Na koniec zostanie zastąpiony, OnInitializedAsync aby utworzyć nowy kontekst. W przykładowej aplikacji OnInitializedAsync ładuje kontakt w tej samej metodzie:

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

W powyższym przykładzie:

  • Gdy Busy jest ustawiona wartość true, mogą rozpocząć się operacje asynchroniczne. Po Busy ustawieniu elementu z powrotem na falseoperacje asynchroniczne powinny zostać zakończone.
  • Umieść dodatkową logikę catch obsługi błędów w bloku.

Włączanie rejestrowania poufnych danych

EnableSensitiveDataLogging zawiera dane aplikacji w komunikatach o wyjątkach i rejestrowaniu struktury. Zarejestrowane dane mogą zawierać wartości przypisane do właściwości wystąpień jednostki i wartości parametrów dla poleceń wysyłanych do bazy danych. Rejestrowanie danych EnableSensitiveDataLogging za pomocą elementu jest zagrożeniem bezpieczeństwa, ponieważ może uwidaczniać hasła i inne dane osobowe podczas rejestrowania instrukcji SQL wykonywanych względem bazy danych.

Zalecamy włączenie EnableSensitiveDataLogging tylko na potrzeby programowania i testowania:

#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

Dodatkowe zasoby