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

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Tento článek vysvětluje, jak používat Entity Framework Core (EF Core) v serverových Blazor aplikacích.

Na straně Blazor serveru je stavová aplikační architektura. Aplikace udržuje trvalé připojení k serveru a stav uživatele se uchovává v paměti serveru v okruhu. Jedním z příkladů stavu uživatele jsou data uložená v instancích služby injektáže závislostí (DI), které jsou vymezeny na okruh. Jedinečný aplikační model, který Blazor poskytuje, vyžaduje speciální přístup k používání Entity Framework Core.

Poznámka:

Tento článek se zabývá EF Core aplikacemi na straně Blazor serveru. Blazor WebAssembly aplikace běží v sandboxu WebAssembly, který brání většině přímých databázových připojení. Blazor WebAssembly Spuštění EF Core je nad rámec tohoto článku.

Tyto pokyny platí pro komponenty, které přijímají interaktivní vykreslování na straně serveru (interaktivní SSR) ve Blazor webové aplikaci.

Tyto pokyny platí pro Server projekt hostovaného Blazor WebAssembly řešení nebo Blazor Server aplikace.

Ukázková aplikace

Ukázková aplikace byla vytvořena jako referenční informace pro aplikace na straně Blazor serveru, které používají EF Core. Ukázková aplikace obsahuje mřížku s operacemi řazení a filtrování, odstraňování, přidávání a aktualizací. Ukázka ukazuje použití EF Core ke zpracování optimistické souběžnosti.

Zobrazení nebo stažení vzorového kódu (postup stažení): Vyberte složku, která odpovídá verzi .NET, kterou přijímáte. Ve složce verze přejděte k ukázce s názvem BlazorWebAppEFCore.

Zobrazení nebo stažení vzorového kódu (postup stažení): Vyberte složku, která odpovídá verzi .NET, kterou přijímáte. Ve složce verze přejděte k ukázce s názvem BlazorServerEFCoreSample.

Ukázka používá místní databázi SQLite , aby ji bylo možné použít na libovolné platformě. Ukázka také nakonfiguruje protokolování databáze tak, aby zobrazovala dotazy SQL, které se generují. Toto nastavení je nakonfigurováno v 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"
    }
  }
}

Součástí mřížky, přidání a zobrazení se používá vzor "kontext podle operace", kde se pro každou operaci vytvoří kontext. Komponenta pro úpravy používá vzor context-per-component, kde se pro každou komponentu vytvoří kontext.

Poznámka:

Některé příklady kódu v tomto tématu vyžadují obory názvů a služby, které se nezobrazují. Pokud chcete zkontrolovat plně funkční kód, včetně požadovaných @using a @inject direktiv pro Razor příklady, podívejte se na ukázkovou aplikaci.

Přístup k databázi

EF Corespoléhá na způsob DbContextkonfigurace přístupu k databázi a funguje jako jednotka práce. EF CoreAddDbContext poskytuje rozšíření pro aplikace ASP.NET Core, které ve výchozím nastavení registrují kontext jako službu s vymezeným oborem. V aplikacích na straně Blazor serveru můžou být registrace vymezených služeb problematické, protože instance se sdílí mezi komponentami v okruhu uživatele. DbContext není bezpečné vlákno a není navržené pro souběžné použití. Stávající životnosti jsou nevhodné z těchto důvodů:

  • Singleton sdílí stav napříč všemi uživateli aplikace a vede k nevhodnému souběžnému použití.
  • Obor (výchozí) představuje podobný problém mezi komponentami pro stejného uživatele.
  • Přechodné výsledky v nové instanci na požadavek, ale vzhledem k tomu, že komponenty mohou být dlouhodobé, výsledkem je delší kontext, než může být zamýšleno.

Následující doporučení jsou navržená tak, aby poskytovala konzistentní přístup k používání EF Core v aplikacích na straně Blazor serveru.

  • Ve výchozím nastavení zvažte použití jednoho kontextu pro každou operaci. Kontext je navržený pro rychlé vytváření instancí s nízkou režií:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Pokud chcete zabránit více souběžným operacím, použijte příznak:

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

    Umístěte operace za Loading = true; řádek v try bloku.

    Logika načítání nevyžaduje uzamčení záznamů databáze, protože zabezpečení vláken není problém. Logika načítání se používá k zakázání ovládacích prvků uživatelského rozhraní, aby uživatelé neúmyslně nevybírejli tlačítka nebo aktualizovali pole při načítání dat.

  • Pokud existuje nějaká šance, že více vláken může přistupovat ke stejnému bloku kódu, vkněte továrnu a vytvořte novou instanci na operaci. V opačném případě je vkládání a používání kontextu obvykle dostatečné.

  • U delších EF Coreoperací, které využívají sledování změn nebo řízení souběžnosti, rozsah kontextu na dobu životnosti komponenty.

Nové DbContext instance

Nejrychlejší způsob, jak vytvořit novou DbContext instanci, je použití new k vytvoření nové instance. Existují však scénáře, které vyžadují překlad dalších závislostí:

Doporučeným přístupem k vytvoření nové DbContext se závislostmi je použití továrny. EF Core 5.0 nebo novější poskytuje integrovanou továrnu pro vytváření nových kontextů.

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

V předchozí továrně:

Následující příklad nakonfiguruje SQLite a povolí protokolování dat. Kód používá metodu rozšíření (AddDbContextFactory) ke konfiguraci databázové továrny pro DI a poskytnutí výchozích možností:

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

Továrna se vloží do komponent a použije se k vytváření nových DbContext instancí.

Na domovské stránce ukázkové aplikace IDbContextFactory<ContactContext> se vloží do komponenty:

@inject IDbContextFactory<ContactContext> DbFactory

Vytvoří DbContext se pomocí továrny (DbFactory) k odstranění kontaktu v DeleteContactAsync metodě:

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

Poznámka:

Filters je vložený IContactFiltersa Wrapper je součástí odkaz na komponentu GridWrapper . Podívejte se na komponentu Home (Components/Pages/Home.razor) v ukázkové aplikaci.

Poznámka:

Filters je vložený IContactFiltersa Wrapper je součástí odkaz na komponentu GridWrapper . Podívejte se na komponentu Index (Pages/Index.razor) v ukázkové aplikaci.

Rozsah životnosti komponenty

Možná budete chtít vytvořit DbContext , která existuje po celou dobu životnosti komponenty. Můžete ho používat jako jednotku práce a využívat integrované funkce, jako je sledování změn a řešení souběžnosti.

Pomocí továrny můžete vytvořit kontext a sledovat ho po celou dobu životnosti komponenty. Nejprve implementujte IDisposable a vkládat továrnu, jak je znázorněno v komponentě EditContact (Components/Pages/EditContact.razor):

Pomocí továrny můžete vytvořit kontext a sledovat ho po celou dobu životnosti komponenty. Nejprve implementujte IDisposable a vkládat továrnu, jak je znázorněno v komponentě EditContact (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Ukázková aplikace zajistí, že se kontext odstraní, když je komponenta uvolněná:

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

Nakonec se přepíše, OnInitializedAsync aby se vytvořil nový kontext. V ukázkové aplikaci OnInitializedAsync načte kontakt ve stejné metodě:

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

V předchozím příkladu:

  • Pokud Busy je nastavena na true, asynchronní operace mohou začínat. Po Busy nastavení zpět na falseasynchronní operace by měly být dokončeny.
  • Do bloku umístěte další logiku catch zpracování chyb.

Povolení protokolování citlivých dat

EnableSensitiveDataLogging zahrnuje data aplikací ve zprávách výjimek a protokolování architektury. Protokolovaná data můžou zahrnovat hodnoty přiřazené vlastnostem instancí entit a hodnot parametrů pro příkazy odeslané do databáze. Protokolování dat s bezpečnostními rizikyEnableSensitiveDataLogging, protože může vystavit hesla a další identifikovatelné osobní údaje (PII), když protokoluje příkazy SQL spuštěné v databázi.

Doporučujeme povolit EnableSensitiveDataLogging pouze vývoj a testování:

#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

Další materiály