Dela via


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

Anmärkning

Det här är inte den senaste versionen av den här artikeln. Den aktuella versionen finns i den .NET 9-versionen av den här artikeln.

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i .NET och .NET Core Support Policy. Den aktuella versionen finns i den .NET 9-versionen av den här artikeln.

Viktig

Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps kommersiellt. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

Den aktuella utgåvan finns i .NET 9-versionen av den här artikeln .

Den här artikeln beskriver hur du använder Entity Framework Core (EF Core) i Blazor appar på serversidan.

Blazor på serversidan är ett tillståndskänsligt appramverk. Appen upprätthåller en pågående anslutning till servern och användarens tillstånd lagras i serverns minne i en krets. Ett exempel på användartillstånd är data som lagras i beroendeinmatning (DI) tjänstinstanser som är begränsade till kretsen. Den unika programmodell som Blazor tillhandahåller kräver en särskild metod för att använda Entity Framework Core.

Anteckning

Den här artikeln tar upp EF Core i Blazor appar på serversidan. Blazor WebAssembly appar körs i en WebAssembly-sandbox-miljö som förhindrar de flesta direkta databasanslutningar. Att köra EF Core i Blazor WebAssembly ligger utanför den här artikelns omfång.

Den här vägledningen gäller för komponenter som använder interaktiv återgivning på serversidan (interaktiv SSR) i en Blazor Web App.

Den här vägledningen gäller för Server projekt för en värdbaserad Blazor WebAssembly lösning eller en Blazor Server app.

Säkert autentiseringsflöde som krävs för produktionsappar

Den här artikeln använder en lokal databas som inte kräver användarautentisering. Produktionsappar bör använda det säkraste tillgängliga autentiseringsflödet. Mer information om autentisering för distribuerade test- och produktionsappar Blazor finns i artiklarna i noden BlazorSäkerhet och Identity nod.

För Microsoft Azure-tjänster rekommenderar vi att du använder hanterade identiteter. Hanterade identiteter autentiserar säkert till Azure-tjänster utan att lagra autentiseringsuppgifter i appkod. Mer information finns i följande resurser:

Exempelapp

Exempelappen skapades som en referens för Blazor appar på serversidan som använder EF Core. Exempelappen innehåller ett rutnät med sorterings- och filtreringsåtgärder, borttagning, tillägg och uppdatering.

Visa eller ladda ned exempelkod (hur du laddar ned): Välj den mapp som matchar den version av .NET som du använder. I versionsmappen kommer du åt exemplet med namnet BlazorWebAppEFCore.

Visa eller ladda ned exempelkod (hur du laddar ned): Välj den mapp som matchar den version av .NET som du använder. I versionsmappen kommer du åt exemplet med namnet BlazorServerEFCoreSample.

Använda exemplet med SQLite

Exemplet använder en lokal SQLite- databas så att den kan användas på valfri plattform.

Exemplet visar användningen av EF Core för att hantera optimistisk samtidighet. Men inbyggda databasgenererade samtidighetstoken stöds inte för SQLite-databaser, som är databasprovidern för exempelappen. Om du vill demonstrera samtidighet med exempelappen använder du en annan databasprovider som stöder databasgenererade samtidighetstoken (till exempel SQL Server-providern). Du kan använda SQL Server för exempelappen genom att följa anvisningarna i nästa avsnitt Använd exemplet med SQL Server och optimistisk samtidighet.

Använd exemplet med SQL Server och optimistisk samtidighet

Exemplet visar användning av EF Core för att hantera optimistisk samtidighet, men bara för en databasleverantör som använder databasgenererade inbyggda samtidighetstoken, vilket är en funktion som stöds för SQL Server. För att demonstrera samtidighet med exempelappen kan exempelappen konverteras från SQLite-providern för att använda SQL Server-providern med en ny SQL Server-databas som skapats med hjälp av .NET-byggnadsställningar.

Använd följande vägledning för att använda SQL Server för exempelappen med hjälp av Visual Studio.

Öppna filen Program (Program.cs) och kommentera ut raderna som lägger till databasens kontextfabrik med SQLite-providern:

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

Spara filen Program.cs.

Högerklicka på projektet i Solution Explorer och välj Lägg till>Nytt ställningsobjekt.

I dialogrutan Lägg till nytt genererat objekt väljer du Installerat>Gemensamt>Blazor>Razor Komponent>Razor Komponenter som använder Entity Framework (CRUD). Välj knappen Lägg till.

I dialogrutan Lägg till Razor-komponenter med hjälp av Entity Framework (CRUD) använder du följande inställningar:

  • Template: Använd standardvalet (CRUD).
  • Modellklass: Välj den Contact modellen i listrutan.
  • DbContext-klass: Välj plustecknet (+) och välj Lägg till med hjälp av standardkontextklassnamnet som genereras av byggnadsställningen.
  • Database Provider: Använd standardvalet (SQL Server).

Välj Lägg till för att skapa modellen och skapa SQL Server-databasen.

När scaffolding-åtgärden är klar tar du bort den genererade kontextklassen från mappen Data (Data/{PROJECT NAME}Context.cs, där platshållaren {PROJECT NAME} är projektets namn/namnområde).

Ta bort mappen ContactPages från mappen Components/Pages, som innehåller QuickGrid--baserade sidor för kontakthantering. För en fullständig demonstration av QuickGrid-baserade sidor för att hantera data använder du självstudiekursen Skapa en Blazor filmdatabasapp (översikt).

Öppna filen Program (Program.cs) och leta reda på raden som scaffolding lade till för att skapa en databasens kontextfabrik med hjälp av SQL Server-leverantören. Ändra kontexten från den genererade kontextklassen (togs bort tidigare) till appens befintliga ContactContext-klass:

- builder.Services.AddDbContextFactory<BlazorWebAppEFCoreContext>(options =>
+ builder.Services.AddDbContextFactory<ContactContext>(options =>

Nu använder appen SQL Server-providern och en SQL Server-databas som skapats för Contact-modellklassen. Optimistisk samtidighet fungerar med inbyggda databasgenererade samtidighetstoken som redan har implementerats i exempelappens ContactContext-klass.

Databasloggning

Exemplet konfigurerar även databasloggning för att visa DE SQL-frågor som genereras. Detta konfigureras i 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"
    }
  }
}

Komponenterna grid, add och view använder mönstret "context-per-operation", där en kontext skapas för varje åtgärd. Redigeringskomponenten använder mönstret "context-per-component", där en kontext skapas för varje komponent.

Not

Vissa av kodexemplen i det här avsnittet kräver namnområden och tjänster som inte visas. För att inspektera den fullt fungerande koden, inklusive obligatoriska @using- och @inject-direktiv för Razor-exempel, se exempelappen.

Guida till att bygga en Blazor filmdatabasapp

En självstudiekurs om hur du skapar en app som använder EF Core för att arbeta med en databas finns i Skapa en Blazor filmdatabasapp (översikt). Handledningen visar hur du skapar en Blazor Web App som kan visa och hantera filmer i en filmdatabas.

Databasåtkomst

EF Core förlitar sig på en DbContext som ett sätt att konfigurera databasåtkomst och fungera som en arbetsenhet. EF Core tillhandahåller AddDbContext-tillägget för ASP.NET Core-appar som registrerar kontexten som en begränsad tjänst. I Blazor appar på serversidan kan begränsade tjänstregistreringar vara problematiska eftersom instansen delas mellan komponenter i användarens krets. DbContext är inte trådsäkert och är inte utformat för samtidig användning. De befintliga livslängderna är olämpliga av följande skäl:

  • Singleton delar tillstånd för alla användare av appen och leder till olämplig samtidig användning.
  • Scoped (standard) utgör ett liknande problem mellan komponenter för samma användare.
  • Tillfälliga resulterar i en ny instans per begäran. men eftersom komponenter kan vara långlivade resulterar detta i en kontext med längre livslängd än vad som kan vara avsett.

Följande rekommendationer är utformade för att ge en konsekvent metod för att använda EF Core i Blazor appar på serversidan.

  • Överväg att använda en kontext per åtgärd. Kontexten är utformad för snabb instansiering med låg belastning.

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Använd en flagga för att förhindra flera samtidiga åtgärder:

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

    Placera åtgärder efter raden Loading = true; i try-blocket.

    Trådsäkerhet är inte ett problem, så inläsningslogik kräver inte låsning av databasposter. Inläsningslogik används för att inaktivera användargränssnittskontroller så att användarna inte oavsiktligt väljer knappar eller uppdaterar fält när data hämtas.

  • Om det finns en risk att flera trådar får åtkomst till samma kodblock, injicera en fabrik och skapa en ny instans per åtgärd. Annars räcker det vanligtvis att mata in och använda kontexten.

  • För åtgärder med längre livslängd som utnyttjar EF Coreändringsspårning eller samtidighetskontrollomfångskontexten till komponentens livslängd.

Nya DbContext instanser

Det snabbaste sättet att skapa en ny DbContext instans är att använda new för att skapa en ny instans. Det finns dock scenarier som kräver att ytterligare beroenden löses.

Varning

Lagra inte apphemligheter, anslutningssträngar, autentiseringsuppgifter, lösenord, personliga identifieringsnummer (PIN),privat C#/.NET-kod eller privata nycklar/token i kod på klientsidan, vilket är alltid osäker. I test-/mellanlagrings- och produktionsmiljöer bör Blazor kod på serversidan och webb-API:er använda säkra autentiseringsflöden som undviker att underhålla autentiseringsuppgifter i projektkod eller konfigurationsfiler. Förutom testning av lokal utveckling rekommenderar vi att du undviker användning av miljövariabler för att lagra känsliga data, eftersom miljövariabler inte är den säkraste metoden. För testning av lokal utveckling rekommenderas verktyget Secret Manager för att skydda känsliga data. Mer information finns i På ett säkert sätt underhålla känsliga data och autentiseringsuppgifter.

Den rekommenderade metoden för att skapa en ny DbContext med beroenden är att använda en fabrik. EF Core 5.0 eller senare erbjuder en inbyggd fabrik för att skapa nya kontexter.

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

I föregående fabrik:

I följande exempel konfigureras SQLite- och aktiverar dataloggning. Koden använder en tilläggsmetod (AddDbContextFactory) för att konfigurera databasfabriken för DI och ange standardalternativ:

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

Fabriken matas in i komponenter och används för att skapa nya DbContext instanser.

På home sidan i exempelappen matas IDbContextFactory<ContactContext> in i komponenten:

@inject IDbContextFactory<ContactContext> DbFactory

En DbContext skapas med hjälp av fabriken (DbFactory) för att ta bort en kontakt i metoden 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;

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

Not

Filters är en inmatad IContactFiltersoch Wrapper är en komponentreferens till komponenten GridWrapper. Se Home komponenten (Components/Pages/Home.razor) i exempelappen.

Anteckning

Filters är en inmatad IContactFiltersoch Wrapper är en komponentreferens till komponenten GridWrapper. Se Index komponenten (Pages/Index.razor) i exempelappen.

Räckvidd för komponentens livslängd

Du kanske vill skapa en DbContext som finns under en komponents livslängd. På så sätt kan du använda den som en arbetsenhet och dra nytta av inbyggda funktioner, till exempel ändringsspårning och samtidighetsmatchning.

Du kan använda fabriken för att skapa en kontext och spåra den under komponentens livslängd. Börja med att implementera IDisposable och injicera fabriken som visas i EditContact-komponenten (Components/Pages/EditContact.razor):

Du kan använda fabriken för att skapa en kontext och spåra den under komponentens livslängd. Börja med att implementera IDisposable och injicera fabriken som visas i EditContact-komponenten (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Exempelappen ser till att kontexten tas bort när komponenten tas bort:

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

Slutligen åsidosättas OnInitializedAsync för att skapa en ny kontext. I exempelappen läser OnInitializedAsync in kontakten med samma 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;
    }
}
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();
}

I föregående exempel:

  • När Busy är inställt på truekan asynkrona åtgärder börja. När Busy återställs till falsebör de asynkrona åtgärderna vara slutförda.
  • Placera ytterligare felhanteringslogik i ett catch block.

Aktivera loggning av känsliga data

EnableSensitiveDataLogging innehåller programdata i undantagsmeddelanden och ramverksloggning. De loggade data kan innehålla värden som tilldelats egenskaper för entitetsinstanser och parametervärden för kommandon som skickas till databasen. Loggning av data med EnableSensitiveDataLogging är en säkerhetsriskeftersom det kan exponera lösenord och andra personligt identifierbar information (PII) när SQL-instruktioner som körs mot databasen loggas.

Vi rekommenderar att du bara aktiverar EnableSensitiveDataLogging för utveckling och testning:

#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

Ytterligare resurser