# 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. - Published: 2026-04-15 - Updated: 2026-04-15 - Author: SharpSkill - Tags: dotnet, entity-framework, performance, best-practices - Reading time: 10 min --- 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` 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. ```csharp // ProductRepository.cs public async Task> 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: ```csharp // Program.cs builder.Services.AddDbContext(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. ```csharp // OrderRepository.cs public async Task 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. ```csharp // PromotionService.cs 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: ```csharp // CleanupService.cs public async Task PurgeExpiredSessionsAsync(CancellationToken ct) { await _context.Sessions .Where(s => s.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(ct); } ``` ## 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. ```csharp // ReportService.cs public async Task> 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. ```csharp // AppDbContext.cs protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasQueryFilter("SoftDelete", i => !i.IsDeleted) .HasQueryFilter("Tenant", i => i.TenantId == _tenantId); } ``` Endpoint admin dapat menonaktifkan filter soft-delete sambil tetap menjaga isolasi tenant: ```csharp // InvoiceRepository.cs public async Task> 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. ```csharp // Program.cs builder.Services.AddDbContext(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: ``` 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. ```csharp // AppDbContext.cs - OnModelCreating modelBuilder.Entity(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: ```csharp // Program.cs (development only) builder.Services.AddDbContext(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. ```csharp // CompiledQueries.cs public static class UserQueries { // dikompilasi sekali, digunakan kembali pada setiap pemanggilan public static readonly Func> 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); ``` ## 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](/technologies/dotnet/interview-questions/ef-core-advanced) membahas pola-pola ini dalam konteks wawancara, dan [panduan Clean Architecture dengan .NET](/blog/dotnet/clean-architecture-dotnet-practical-guide) mendemonstrasikan cara menyusun lapisan repository dan service yang membungkus query-query ini. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/dotnet/ef-core-performance-best-practices