Bagikan melalui


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

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Peringatan

Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Artikel ini menjelaskan cara menggunakan Entity Framework Core (EF Core) di aplikasi sisi Blazor server.

Sisi server Blazor adalah kerangka kerja aplikasi stateful. Aplikasi ini mempertahankan koneksi berkelanjutan ke server, dan status pengguna disimpan dalam memori server di sirkuit. Salah satu contoh status pengguna adalah data yang disimpan dalam instans layanan injeksi dependensi (DI) yang tercakup ke sirkuit. Model aplikasi unik yang Blazor menyediakan memerlukan pendekatan khusus untuk menggunakan Entity Framework Core.

Catatan

Artikel ini membahas EF Core di aplikasi sisi Blazor server. Blazor WebAssembly aplikasi berjalan di kotak pasir WebAssembly yang mencegah sebagian besar koneksi database langsung. Menjalankan EF Core di Blazor WebAssembly luar cakupan artikel ini.

Panduan ini berlaku untuk komponen yang mengadopsi penyajian sisi server interaktif (SSR interaktif) dalam Blazor Web App.

Panduan ini berlaku untuk Server proyek solusi atau aplikasi yang Blazor Server dihostingBlazor WebAssembly.

Alur autentikasi aman diperlukan untuk aplikasi produksi

Artikel ini menggunakan database lokal yang tidak memerlukan autentikasi pengguna. Aplikasi produksi harus menggunakan alur autentikasi paling aman yang tersedia. Untuk informasi selengkapnya tentang autentikasi untuk aplikasi pengujian dan produksi Blazor yang disebarkan, lihat artikel di BlazorKeamanan dan Identity simpul.

Untuk layanan Microsoft Azure, sebaiknya gunakan identitas terkelola. Identitas terkelola mengautentikasi dengan aman ke layanan Azure tanpa menyimpan kredensial dalam kode aplikasi. Untuk informasi selengkapnya, lihat sumber daya berikut:

Aplikasi sampel

Aplikasi sampel dibuat sebagai referensi untuk aplikasi sisi Blazor server yang menggunakan EF Core. Aplikasi sampel menyertakan kisi dengan operasi pengurutan dan pemfilteran, hapus, tambahkan, dan perbarui.

Sampel menunjukkan penggunaan EF Core untuk menangani konkurensi optimis. Namun, token konkurensi asli yang dihasilkan database tidak didukung untuk database SQLite, yang merupakan penyedia database untuk aplikasi sampel. Untuk menunjukkan konkurensi dengan aplikasi sampel, adopsi penyedia database berbeda yang mendukung token konkurensi yang dihasilkan database (misalnya, penyedia SQL Server).

Melihat atau mengunduh kode sampel (cara mengunduh): Pilih folder yang cocok dengan versi .NET yang Anda adopsi. Dalam folder versi, akses sampel bernama BlazorWebAppEFCore.

Melihat atau mengunduh kode sampel (cara mengunduh): Pilih folder yang cocok dengan versi .NET yang Anda adopsi. Dalam folder versi, akses sampel bernama BlazorServerEFCoreSample.

Sampel menggunakan database SQLite lokal sehingga dapat digunakan pada platform apa pun. Sampel juga mengonfigurasi pengelogan database untuk menampilkan kueri SQL yang dihasilkan. Ini dikonfigurasi dalam 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"
    }
  }
}

Komponen kisi, tambahkan, dan tampilan menggunakan pola "konteks per operasi", di mana konteks dibuat untuk setiap operasi. Komponen edit menggunakan pola "konteks per komponen", di mana konteks dibuat untuk setiap komponen.

Catatan

Beberapa contoh kode dalam topik ini memerlukan namespace layanan dan layanan yang tidak ditampilkan. Untuk memeriksa kode yang sepenuhnya berfungsi, termasuk direktif dan @inject yang diperlukan @using misalnyaRazor, lihat aplikasi sampel.

Membuat Blazor tutorial aplikasi database film

Untuk pengalaman tutorial membangun aplikasi yang digunakan EF Core untuk bekerja dengan database, lihat Membuat Blazor aplikasi database film (Gambaran Umum). Tutorial ini menunjukkan kepada Anda cara membuat Blazor Web App yang dapat menampilkan dan mengelola film dalam database film.

Akses Database

EF Core bergantung pada DbContext sebagai sarana untuk mengonfigurasi akses database dan bertindak sebagai unit kerja. EF CoreAddDbContext menyediakan ekstensi untuk aplikasi ASP.NET Core yang mendaftarkan konteks sebagai layanan terlingkup. Di aplikasi sisi Blazor server, pendaftaran layanan terlingkup dapat bermasalah karena instans dibagikan di seluruh komponen dalam sirkuit pengguna. DbContext tidak aman utas dan tidak dirancang untuk penggunaan bersamaan. Masa pakai yang ada tidak pantas karena alasan ini:

  • Status berbagi Singleton di semua pengguna aplikasi dan mengarah ke penggunaan bersamaan yang tidak tepat.
  • Cakupan (default) menimbulkan masalah serupa antara komponen untuk pengguna yang sama.
  • Hasil sementara dalam instans baru per permintaan; tetapi karena komponen dapat berumur panjang, ini menghasilkan konteks berumur lebih lama dari yang mungkin dimaksudkan.

Rekomendasi berikut dirancang untuk memberikan pendekatan yang konsisten untuk digunakan EF Core di aplikasi sisi Blazor server.

  • Pertimbangkan untuk menggunakan satu konteks per operasi. Konteks dirancang untuk instans overhead yang cepat dan rendah:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Gunakan bendera untuk mencegah beberapa operasi bersamaan:

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

    Tempatkan operasi setelah Loading = true; baris di try blok.

    Keamanan utas tidak menjadi perhatian, jadi memuat logika tidak memerlukan penguncian rekaman database. Logika pemuatan digunakan untuk menonaktifkan kontrol UI sehingga pengguna tidak secara tidak sengaja memilih tombol atau memperbarui bidang saat data diambil.

  • Jika ada kemungkinan bahwa beberapa utas dapat mengakses blok kode yang sama, menyuntikkan pabrik dan membuat instans baru per operasi. Jika tidak, menyuntikkan dan menggunakan konteks biasanya cukup.

  • Untuk operasi berumur lebih lama yang memanfaatkan EF Corekontrol pelacakan perubahan atau konkurensi, cakupan konteks hingga masa pakai komponen.

Instans baru DbContext

Cara tercepat untuk membuat instans baru DbContext adalah dengan menggunakan new untuk membuat instans baru. Namun, ada skenario yang memerlukan penyelesaian dependensi tambahan:

Peringatan

Jangan menyimpan rahasia aplikasi, string koneksi, kredensial, kata sandi, nomor identifikasi pribadi (PIN), kode C#/.NET privat, atau kunci/token privat dalam kode sisi klien, yang selalu tidak aman. Di lingkungan pengujian/penahapan dan produksi, kode sisi Blazor server dan API web harus menggunakan alur autentikasi aman yang menghindari mempertahankan kredensial dalam kode proyek atau file konfigurasi. Di luar pengujian pengembangan lokal, sebaiknya hindari penggunaan variabel lingkungan untuk menyimpan data sensitif, karena variabel lingkungan bukanlah pendekatan yang paling aman. Untuk pengujian pengembangan lokal, alat Secret Manager direkomendasikan untuk mengamankan data sensitif. Untuk informasi selengkapnya, lihat Mempertahankan data dan kredensial sensitif dengan aman.

Pendekatan yang direkomendasikan untuk membuat baru DbContext dengan dependensi adalah dengan menggunakan pabrik. EF Core 5.0 atau yang lebih baru menyediakan pabrik bawaan untuk membuat konteks baru.

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

Di pabrik sebelumnya:

Contoh berikut mengonfigurasi SQLite dan mengaktifkan pengelogan data. Kode menggunakan metode ekstensi (AddDbContextFactory) untuk mengonfigurasi pabrik database untuk DI dan menyediakan opsi default:

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

Pabrik disuntikkan ke dalam komponen dan digunakan untuk membuat instans baru DbContext .

home Di halaman aplikasi sampel, IDbContextFactory<ContactContext> disuntikkan ke dalam komponen:

@inject IDbContextFactory<ContactContext> DbFactory

DbContext dibuat menggunakan pabrik (DbFactory) untuk menghapus kontak dalam DeleteContactAsync metode :

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

Catatan

Filters adalah disuntikkan IContactFilters, dan Wrapper merupakan referensi komponen ke GridWrapper komponen. Home Lihat komponen (Components/Pages/Home.razor) di aplikasi sampel.

Catatan

Filters adalah disuntikkan IContactFilters, dan Wrapper merupakan referensi komponen ke GridWrapper komponen. Index Lihat komponen (Pages/Index.razor) di aplikasi sampel.

Cakupan ke masa pakai komponen

Anda mungkin ingin membuat DbContext yang ada selama masa pakai komponen. Ini memungkinkan Anda untuk menggunakannya sebagai unit kerja dan memanfaatkan fitur bawaan, seperti pelacakan perubahan dan resolusi konkurensi.

Anda dapat menggunakan pabrik untuk membuat konteks dan melacaknya selama masa pakai komponen. Pertama, terapkan IDisposable dan injeksi pabrik seperti yang ditunjukkan dalam EditContact komponen (Components/Pages/EditContact.razor):

Anda dapat menggunakan pabrik untuk membuat konteks dan melacaknya selama masa pakai komponen. Pertama, terapkan IDisposable dan injeksi pabrik seperti yang ditunjukkan dalam EditContact komponen (Pages/EditContact.razor):

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Aplikasi sampel memastikan konteks dibuang saat komponen dibuang:

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

Akhirnya, OnInitializedAsync ditimpa untuk membuat konteks baru. Di aplikasi sampel, OnInitializedAsync memuat kontak dalam metode yang sama:

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

Dalam contoh sebelumnya:

  • Ketika Busy diatur ke true, operasi asinkron dapat dimulai. Ketika Busy diatur kembali ke false, operasi asinkron harus selesai.
  • Tempatkan logika penanganan kesalahan tambahan dalam catch blok.

Mengaktifkan pengelogan data sensitif

EnableSensitiveDataLogging menyertakan data aplikasi dalam pesan pengecualian dan pengelogan kerangka kerja. Data yang dicatat dapat menyertakan nilai yang ditetapkan ke properti instans entitas dan nilai parameter untuk perintah yang dikirim ke database. Data pengelogan dengan EnableSensitiveDataLogging adalah risiko keamanan, karena dapat mengekspos kata sandi dan Informasi Pengidentifikasi Pribadi (PII) lainnya ketika mencatat pernyataan SQL yang dijalankan terhadap database.

Sebaiknya hanya mengaktifkan EnableSensitiveDataLogging untuk pengembangan dan pengujian:

#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

Sumber Daya Tambahan: