Entity Framework Core: Optimasi Performa dan Praktik Terbaik di Tahun 2026

Panduan lengkap optimasi performa Entity Framework Core 10 pada .NET 10. Pelajari AsNoTracking, compiled queries, batch updates, split queries, dan operator LeftJoin.

Entity Framework Core Performance Optimization

Entity Framework Core 10, yang dirilis bersama .NET 10 LTS pada November 2025, menghadirkan operator LeftJoin, pencarian vektor, named query filters, dan peningkatan signifikan pada translasi SQL. Panduan ini membahas praktik terbaik EF Core yang secara langsung memengaruhi kecepatan query, alokasi memori, dan skalabilitas di lingkungan produksi.

EF Core 10 hanya berjalan di .NET 10

EF Core 10 merupakan rilis Long-Term Support, didukung hingga November 2028. Framework ini memerlukan SDK dan runtime .NET 10. Aplikasi yang masih menggunakan .NET 8 sebaiknya menargetkan EF Core 8 (LTS) hingga proses migrasi selesai.

Query Tracking: Kapan Harus Dinonaktifkan dan Mengapa

Setiap pemanggilan DbSet<T> secara default melampirkan entitas yang dikembalikan ke change tracker. Tracker ini memelihara snapshot dari nilai properti asli, menghitung perbedaan saat SaveChanges, dan menyelesaikan konflik identitas di seluruh navigasi. Overhead tersebut tidak diperlukan ketika data langsung mengalir ke respons API atau view model yang bersifat read-only.

ProductRepository.cscsharp
public async Task<List<ProductDto>> GetActiveByCategoryAsync(
    int categoryId, CancellationToken ct)
{
    return await _context.Products
        .AsNoTracking()              // lewati change tracker sepenuhnya
        .Where(p => p.CategoryId == categoryId && p.IsActive)
        .OrderBy(p => p.Name)
        .Select(p => new ProductDto   // proyeksi ke DTO di level database
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price
        })
        .ToListAsync(ct);
}

AsNoTracking menghilangkan overhead pembuatan snapshot dan resolusi identitas per entitas. Dikombinasikan dengan Select, hanya kolom yang dibutuhkan yang dikirim melalui jaringan. Pada tabel dengan 50.000 baris, pola ini biasanya mengurangi alokasi memori sebesar 40-60% dibandingkan query full-entity yang dilacak.

Untuk context yang tidak pernah memodifikasi data, atur default saat registrasi:

Program.cscsharp
builder.Services.AddDbContext<CatalogContext>(options =>
    options.UseSqlServer(connectionString)
           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

Split Queries untuk Menghindari Ledakan Kartesian

Memuat entitas dengan beberapa navigasi koleksi melalui Include menghasilkan satu pernyataan SQL dengan JOIN. Ketika dua atau lebih koleksi dimuat secara bersamaan, result set berkembang sebagai produk Kartesian dari koleksi-koleksi tersebut, menduplikasi data baris induk di setiap kombinasi.

OrderRepository.cscsharp
public async Task<Order?> GetWithDetailsAsync(int orderId, CancellationToken ct)
{
    return await _context.Orders
        .AsSplitQuery()              // satu SQL per Include
        .Include(o => o.Items)
            .ThenInclude(i => i.Product)
        .Include(o => o.Payments)
        .Include(o => o.ShippingEvents)
        .FirstOrDefaultAsync(o => o.Id == orderId, ct);
}

AsSplitQuery memecah pemuatan menjadi pernyataan SQL terpisah untuk setiap navigasi. Trade-off-nya: beberapa round-trip alih-alih satu, tetapi setiap result set tetap kecil dan menghindari masalah duplikasi baris. EF Core 10 juga memperbaiki inkonsistensi pengurutan yang sudah lama ada dalam split queries, memastikan pengurutan subquery sesuai dengan query utama.

Kapan menggunakan single query

Single query tetap lebih baik ketika memuat satu navigasi koleksi atau ketika latensi round-trip tinggi (pemanggilan database lintas-region). Lakukan benchmark kedua mode untuk pola akses spesifik sebelum mengambil keputusan.

Operasi Batch dengan ExecuteUpdate dan ExecuteDelete

Alur kerja EF tradisional memuat entitas, memodifikasi properti, lalu memanggil SaveChanges. Untuk operasi massal yang memengaruhi ribuan baris, pendekatan ini menghasilkan ribuan instance yang dilacak dan pernyataan UPDATE individual. EF Core 7 memperkenalkan ExecuteUpdateAsync dan ExecuteDeleteAsync untuk mendorong operasi menjadi satu perintah SQL. EF Core 10 menyederhanakan API lebih lanjut dengan menerima lambda biasa alih-alih expression tree.

PromotionService.cscsharp
public async Task ApplySeasonalDiscountAsync(
    int categoryId, decimal discountPercent, CancellationToken ct)
{
    var affected = await _context.Products
        .Where(p => p.CategoryId == categoryId && p.IsActive)
        .ExecuteUpdateAsync(s =>
        {
            s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100));
            s.SetProperty(p => p.LastModified, DateTime.UtcNow);
        }, ct);

    // affected = jumlah baris yang diperbarui
}

Pernyataan ini diterjemahkan menjadi satu UPDATE ... SET ... WHERE. Tidak ada entitas yang dimuat ke memori. Untuk pembaruan 10.000 baris, waktu eksekusi turun dari hitungan detik (pendekatan tracked) menjadi milidetik.

Pola yang sama berlaku untuk penghapusan:

CleanupService.cscsharp
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
    await _context.Sessions
        .Where(s => s.ExpiresAt < DateTime.UtcNow)
        .ExecuteDeleteAsync(ct);
}

Siap menguasai wawancara .NET Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Operator LeftJoin di EF Core 10

Sebelum EF Core 10, melakukan LEFT JOIN memerlukan kombinasi GroupJoin, SelectMany, dan DefaultIfEmpty dalam pola tertentu yang kebanyakan pengembang harus mencari referensinya setiap kali. EF Core 10 menambahkan LeftJoin sebagai operator LINQ kelas satu.

ReportService.cscsharp
public async Task<List<EmployeeReportDto>> GetEmployeeDepartmentReportAsync(
    CancellationToken ct)
{
    return await _context.Employees
        .LeftJoin(
            _context.Departments,
            employee => employee.DepartmentId,
            department => department.Id,
            (employee, department) => new EmployeeReportDto
            {
                FullName = employee.FirstName + " " + employee.LastName,
                Department = department.Name ?? "Unassigned",
                HiredAt = employee.HiredAt
            })
        .OrderBy(r => r.FullName)
        .ToListAsync(ct);
}

SQL yang dihasilkan menggunakan klausa LEFT JOIN standar. Operator RightJoin juga tersedia. Kedua operator ini menghilangkan rantai tiga metode yang verbose yang sebelumnya diperlukan.

Named Query Filters untuk Pemfilteran Multi-Concern

Global query filters telah dibatasi pada satu filter per tipe entitas sejak EF Core 2.0. Aplikasi yang mengimplementasikan soft-delete dan multi-tenancy harus menggabungkan kondisi menjadi satu ekspresi dan tidak dapat menonaktifkannya secara selektif. EF Core 10 memperkenalkan named query filters.

AppDbContext.cscsharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Invoice>()
        .HasQueryFilter("SoftDelete", i => !i.IsDeleted)
        .HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}

Endpoint admin dapat menonaktifkan filter soft-delete sambil tetap menjaga isolasi tenant:

InvoiceRepository.cscsharp
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
    return await _context.Invoices
        .IgnoreQueryFilters(["SoftDelete"])  // filter tenant tetap aktif
        .ToListAsync(ct);
}

Hal ini menghilangkan kebutuhan metode ekstensi IQueryable kustom yang secara manual menambahkan kondisi filter.

Ketahanan Koneksi dan Konfigurasi Pooling

Kegagalan database sementara (gangguan jaringan, failover Azure SQL, kehabisan connection pool) menyebabkan exception yang sering membuat pipeline request crash. EF Core menyediakan logika retry bawaan, tetapi konfigurasi default memerlukan pengaturan eksplisit.

Program.cscsharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(10),
            errorNumbersToAdd: null);    // retry pada semua error sementara
        sqlOptions.CommandTimeout(30);   // timeout perintah 30 detik
    }));

Parameter pooling connection string juga berpengaruh:

text
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;

Min Pool Size menjaga koneksi yang siap pakai untuk lonjakan traffic. Max Pool Size membatasi total koneksi terbuka untuk mencegah overload database. Nilai default 100 cocok untuk sebagian besar aplikasi web, tetapi layanan dengan throughput tinggi mungkin perlu penyesuaian berdasarkan volume query konkuren aktual.

Logika retry dan transaksi

Retry otomatis tidak berfungsi di dalam transaksi yang diinisiasi pengguna. Bungkus seluruh transaksi dalam strategi retry manual menggunakan context.Database.CreateExecutionStrategy().ExecuteAsync(...) untuk menangani kegagalan sementara di beberapa operasi.

Strategi Indexing dan Analisis Query

Migrasi EF Core dapat mendefinisikan indeks secara deklaratif, tetapi memilih kolom mana yang akan diindeks memerlukan pemahaman pola query.

AppDbContext.cs - OnModelCreatingcsharp
modelBuilder.Entity<Order>(entity =>
{
    // indeks komposit untuk pola query yang sering digunakan
    entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt })
          .HasDatabaseName("IX_Order_Customer_Status_Date");

    // indeks terfilter untuk pesanan aktif saja
    entity.HasIndex(o => o.Status)
          .HasFilter("[Status] <> 'Cancelled'")
          .HasDatabaseName("IX_Order_ActiveStatus");
});

Indeks terfilter mengurangi ukuran indeks dengan mengecualikan baris yang tidak pernah ditargetkan oleh query. Untuk tabel dengan 80% pesanan yang dibatalkan, indeks terfilter pada status aktif bisa 5 kali lebih kecil dan lebih cepat untuk dipindai.

Untuk menganalisis SQL yang dihasilkan, aktifkan logging di development:

Program.cs (development only)csharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information)
           .EnableSensitiveDataLogging());

Salin SQL yang tercatat ke SQL Server Management Studio dan jalankan dengan SET STATISTICS IO ON untuk memeriksa logical reads, atau gunakan EXPLAIN ANALYZE di PostgreSQL. Saran indeks yang hilang dari query plan sering mengungkapkan peluang optimasi dengan dampak tertinggi.

Compiled Queries untuk Hot Paths

Expression tree LINQ diurai dan diterjemahkan ke SQL pada setiap eksekusi. Untuk query yang berjalan ribuan kali per menit (misalnya pencarian autentikasi, validasi sesi), biaya translasi ini terakumulasi. Compiled queries meng-cache translasi saat aplikasi dimulai.

CompiledQueries.cscsharp
public static class UserQueries
{
    // dikompilasi sekali, digunakan kembali pada setiap pemanggilan
    public static readonly Func<AppDbContext, string, CancellationToken, Task<UserSession?>>
        GetActiveSession = EF.CompileAsyncQuery(
            (AppDbContext ctx, string token, CancellationToken ct) =>
                ctx.UserSessions
                    .AsNoTracking()
                    .FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow));
}

// Penggunaan di middleware
var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct);

Compiled queries melewati fase parsing expression tree sepenuhnya. Perbedaan performa hanya terukur pada jalur frekuensi tinggi (1.000+ panggilan/menit). Untuk endpoint CRUD standar, cache query bawaan di EF Core sudah menangani penggunaan ulang translasi.

Translasi Parameterized Collection di EF Core 10

Query yang memfilter berdasarkan daftar ID merupakan salah satu pola paling umum dalam akses data. EF Core 10 mengubah strategi translasi default untuk parameterized collections. Alih-alih meng-encode daftar sebagai array JSON (EF Core 8-9) atau menyisipkan konstanta secara inline (EF Core 7 dan sebelumnya), EF Core 10 menerjemahkan setiap nilai sebagai parameter SQL terpisah.

csharp
// Sebelumnya (EF Core 8-9): parameter array JSON
// @__ids_0='[1,2,3]'
// SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0))

// EF Core 10: parameter individual dengan padding
// SELECT ... WHERE Id IN (@ids1, @ids2, @ids3)

var orderIds = new[] { 101, 205, 389 };
var orders = await _context.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToListAsync(ct);

Pendekatan baru ini memberikan informasi kardinalitas kepada query planner database sambil tetap menghindari bloat cache plan melalui parameter padding. Untuk kasus di mana pendekatan JSON berkinerja lebih baik (koleksi yang sangat besar), override perilaku per query:

csharp
var orders = await _context.Orders
    .Where(o => EF.Parameter(orderIds).Contains(o.Id))  // paksa mode JSON
    .ToListAsync(ct);

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Kesimpulan

  • Gunakan AsNoTracking() dan proyeksi Select pada setiap query read-only untuk menghilangkan overhead change tracker
  • Terapkan AsSplitQuery() saat memuat beberapa navigasi koleksi untuk menghindari ledakan Kartesian
  • Ganti pola tracked load-modify-save dengan ExecuteUpdateAsync dan ExecuteDeleteAsync untuk operasi massal
  • Adopsi operator LeftJoin dari EF Core 10 untuk menggantikan rantai GroupJoin/SelectMany/DefaultIfEmpty yang verbose
  • Konfigurasi named query filters untuk mengelola soft-delete dan multi-tenancy secara independen
  • Atur EnableRetryOnFailure dan sesuaikan ukuran connection pool untuk ketahanan produksi
  • Definisikan indeks komposit dan terfilter berdasarkan pola query aktual, bukan dugaan
  • Cadangkan compiled queries untuk jalur yang benar-benar panas yang melebihi 1.000 eksekusi per menit
  • Biarkan EF Core 10 menangani translasi parameterized collection secara default, dan override hanya ketika benchmark membenarkannya

Bacaan lebih lanjut: modul EF Core Advanced membahas pola-pola ini dalam konteks wawancara, dan panduan Clean Architecture dengan .NET mendemonstrasikan cara menyusun lapisan repository dan service yang membungkus query-query ini.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#dotnet
#entity-framework
#performance
#best-practices

Bagikan

Artikel terkait