Entity Framework Core (EF Core) adalah Object-Relational Mapper (ORM) resmi dari Microsoft yang memungkinkan developer berinteraksi dengan database relasional menggunakan object dan method C#, tanpa perlu menulis query SQL mentah untuk sebagian besar operasi CRUD sehari-hari.

EF Core bekerja dengan konsep DbContext sebagai representasi sesi koneksi ke database, dan DbSet sebagai representasi tabel yang bisa di-query layaknya koleksi C# biasa menggunakan LINQ, yang kemudian diterjemahkan EF Core menjadi query SQL yang sesungguhnya di belakang layar.

Artikel ini membahas cara mengatur DbContext dan model dasar, melakukan operasi CRUD sederhana, serta memahami bagaimana EF Core menerjemahkan kode LINQ menjadi query SQL yang dijalankan terhadap database.

Mendefinisikan DbContext dan Model

DbContext merepresentasikan sesi koneksi dan DbSet mewakili tabel

public class Produk { public int Id { get; set; } public string Nama { get; set; } public decimal Harga { get; set; } } public class TokoDbContext : DbContext { public DbSet<Produk> Produk { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer("Server=localhost;Database=TokoOnline;"); }
Class Produk merepresentasikan satu tabel di database dengan setiap property menjadi satu kolom, sementara TokoDbContext menjadi pintu masuk utama untuk berinteraksi dengan database tersebut, dengan DbSet memungkinkan query terhadap tabel Produk layaknya mengakses koleksi List biasa.

Operasi Create dan Read

Add dan SaveChanges untuk create, LINQ untuk read

using var db = new TokoDbContext(); // Create db.Produk.Add(new Produk { Nama = "Laptop", Harga = 8000000 }); db.SaveChanges(); // Read var produkMahal = db.Produk.Where(p => p.Harga > 5000000).ToList();
Menambahkan data baru dilakukan dengan Add diikuti SaveChanges yang benar-benar mengeksekusi perubahan ke database (perubahan hanya tersimpan di memori sampai SaveChanges dipanggil), sementara membaca data cukup dengan menulis query LINQ biasa terhadap DbSet, yang akan diterjemahkan EF Core menjadi SQL SELECT yang sesuai.

Operasi Update dan Delete

EF Core melacak perubahan object secara otomatis

// Update var produk = db.Produk.First(p => p.Id == 1); produk.Harga = 7500000; db.SaveChanges(); // EF Core otomatis mendeteksi perubahan // Delete var produkHapus = db.Produk.First(p => p.Id == 2); db.Produk.Remove(produkHapus); db.SaveChanges();
EF Core secara otomatis melacak perubahan pada object yang sudah diambil dari database (change tracking), sehingga untuk update, cukup mengubah property object tersebut lalu memanggil SaveChanges tanpa perlu menulis statement UPDATE secara eksplisit sama sekali.

Melihat Query SQL yang Dihasilkan EF Core

LogTo menampilkan SQL asli yang dihasilkan EF Core

builder.Services.AddDbContext<TokoDbContext>(options => options.UseSqlServer(connString) .LogTo(Console.WriteLine, LogLevel.Information)); // Setiap query LINQ yang dijalankan akan mencetak // SQL yang sebenarnya dieksekusi ke console
Mengaktifkan logging dengan LogTo sangat membantu memahami bagaimana query LINQ yang ditulis diterjemahkan menjadi SQL sesungguhnya, berguna untuk debugging performa atau memastikan query yang dihasilkan sudah efisien sebelum aplikasi di-deploy ke production.

Tracking vs No-Tracking Query pada EF Core

AsNoTracking() menghilangkan overhead change tracking untuk query read-only

// Query dengan tracking (default) - EF Core memantau perubahan setiap entity var produk = await _db.Products.FirstOrDefaultAsync(p => p.Id == 1); produk.Stock -= 1; await _db.SaveChangesAsync(); // EF tahu persis field mana yang berubah // Query untuk kebutuhan read-only (lebih ringan, tanpa change tracker) var daftarProduk = await _db.Products .AsNoTracking() .Where(p => p.IsActive) .ToListAsync(); // daftarProduk tidak bisa langsung di-SaveChanges, karena tidak dipantau EF Core

Secara default, setiap entity yang diambil lewat DbContext akan dipantau oleh change tracker EF Core agar perubahan pada propertinya bisa terdeteksi otomatis ketika SaveChangesAsync dipanggil. Mekanisme ini sangat berguna untuk operasi update, tetapi menimbulkan overhead memori dan CPU yang tidak perlu ketika data hanya akan ditampilkan ke user tanpa pernah diubah, seperti pada endpoint listing atau laporan.

Memanggil AsNoTracking() sebelum eksekusi query memberi tahu EF Core untuk tidak menyertakan entity hasil query tersebut ke dalam change tracker, sehingga proses query menjadi lebih cepat dan konsumsi memori lebih rendah, terutama saat mengambil ratusan atau ribuan baris sekaligus. Sebagai patokan praktis, setiap endpoint GET yang murni menampilkan data sebaiknya selalu menggunakan AsNoTracking() secara konsisten.

Untuk aplikasi yang sebagian besar operasinya bersifat baca, EF Core juga menyediakan opsi mengubah perilaku default tracking di level DbContext lewat ChangeTracker.QueryTrackingBehavior, sehingga tidak perlu menambahkan AsNoTracking() di setiap query satu per satu, dan hanya query yang benar-benar butuh update yang secara eksplisit memanggil AsTracking().

Raw SQL Query dengan FromSqlRaw dan ExecuteSqlRaw

Placeholder {0} dan {1} diterjemahkan menjadi parameterized query, bukan string concat

// Query raw SQL yang hasilnya dipetakan ke entity EF Core var laporan = await _db.Orders .FromSqlRaw(@"SELECT * FROM Orders WHERE CreatedAt >= {0} AND Status = {1}", startDate, "Completed") .ToListAsync(); // Parameter {0} dan {1} otomatis dijadikan SqlParameter, aman dari SQL Injection // Eksekusi perintah yang tidak mengembalikan entity (UPDATE/DELETE massal) int rowsAffected = await _db.Database.ExecuteSqlRawAsync( "UPDATE Products SET IsActive = 0 WHERE Stock = {0}", 0); // rowsAffected -> jumlah baris yang ter-update, misal 14

LINQ to Entities pada EF Core mampu menangani sebagian besar kebutuhan query, tetapi ada kasus tertentu seperti laporan dengan agregasi kompleks, penggunaan hint index, atau pemanggilan stored procedure lama yang lebih efisien ditulis langsung sebagai SQL mentah. FromSqlRaw memungkinkan eksekusi SQL mentah yang hasilnya tetap dipetakan ke entity class EF Core, sehingga hasil query tetap bisa diproses lebih lanjut sebagai objek C# seperti hasil LINQ biasa. Kolom hasil query SQL juga harus mencakup semua properti yang dipetakan pada entity tersebut, atau EF Core akan melempar error saat mencoba membentuk object dari hasil baris yang dikembalikan.

Penulisan parameter wajib memakai placeholder seperti {0} dan {1} yang diisi lewat argumen method, bukan string interpolation langsung seperti $"WHERE Status = '{status}'", karena pola placeholder ini diterjemahkan EF Core menjadi parameterized query yang aman dari serangan SQL Injection. Menggabungkan nilai user secara manual ke dalam string SQL tetap berisiko tinggi walau dibungkus FromSqlRaw sekalipun. Hasil dari FromSqlRaw juga masih bisa dirangkai lebih lanjut dengan operator LINQ seperti Where atau OrderBy selama tidak memakai Select yang mengubah bentuk entity hasilnya.

ExecuteSqlRawAsync dipakai untuk perintah yang tidak mengembalikan baris data seperti UPDATE atau DELETE massal yang sulit diekspresikan secara efisien lewat LINQ biasa, dan hasilnya berupa jumlah baris yang terpengaruh, bukan koleksi entity. Kedua method ini sebaiknya dipakai secara selektif untuk kasus yang benar-benar membutuhkan kontrol SQL eksplisit, karena query raw kehilangan sebagian keuntungan EF Core seperti type safety penuh dan kemudahan refactoring otomatis saat nama kolom berubah. Perubahan lewat ExecuteSqlRawAsync juga langsung dieksekusi ke database tanpa melewati change tracker, sehingga entity yang sudah dimuat sebelumnya di memori tidak otomatis ikut diperbarui.

Global Query Filter untuk Soft Delete yang Konsisten di Semua Query

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Produk>() .HasQueryFilter(p => !p.IsDeleted); } var produkAktif = await _context.Produk.ToListAsync(); // Baris dengan IsDeleted = true otomatis tidak ikut terambil // tanpa perlu menambahkan .Where(p => !p.IsDeleted) di setiap query

Soft delete sering diimplementasikan dengan menambah kolom IsDeleted dan mengandalkan setiap developer menambahkan filter Where secara manual pada setiap query yang mengambil data entity tersebut. Pendekatan ini cukup rawan terlewat, terutama saat ada query baru yang ditulis oleh anggota tim yang belum tahu konvensi tersebut, sehingga data yang seharusnya tersembunyi dari pengguna tetap saja ikut muncul di hasil query.

HasQueryFilter mendaftarkan aturan penyaringan itu sekali saja di level model sehingga berlaku otomatis untuk semua query LINQ terhadap entity tersebut, termasuk query baru yang ditulis belakangan di masa depan. Untuk kasus tertentu yang memang membutuhkan akses ke data yang sudah terhapus, misalnya fitur audit atau pemulihan data, filter ini bisa dilewati sementara dengan memanggil IgnoreQueryFilters pada query spesifik yang membutuhkannya.

Kesalahan Umum yang Sering Terjadi

Lupa Memanggil SaveChanges Setelah Add atau Remove

Perubahan yang dilakukan dengan Add, Remove, atau mengubah property object hanya tersimpan di memori sampai SaveChanges benar-benar dipanggil; lupa memanggilnya akan membuat perubahan tersebut tidak pernah tersimpan ke database.

Membuat DbContext Baru untuk Setiap Operasi Kecil

Membuat instance DbContext baru secara manual berulang kali alih-alih memanfaatkan dependency injection dengan lifetime Scoped bisa menyebabkan masalah koneksi database yang tidak dikelola dengan efisien.

Mengambil Seluruh Data ke Memori Sebelum Memfilter

Memanggil ToList() terlalu dini sebelum menerapkan Where akan membuat EF Core mengambil seluruh baris tabel ke memori terlebih dahulu baru memfilternya di sisi aplikasi, alih-alih menerjemahkan filter tersebut menjadi klausa WHERE pada query SQL yang jauh lebih efisien.

Kapan Pendekatan Ini Tidak Cocok Dipakai

EF Core memberikan produktivitas tinggi karena developer bisa bekerja dengan object C# dan LINQ tanpa menulis SQL manual, sangat mempercepat development untuk operasi CRUD standar.

Namun untuk query yang sangat kompleks atau membutuhkan optimasi ekstrem, SQL mentah atau stored procedure terkadang masih memberikan kontrol dan performa yang lebih baik dibanding query LINQ yang diterjemahkan otomatis, sehingga EF Core tetap menyediakan opsi raw SQL untuk kasus-kasus khusus seperti ini.

Ringkasan

Entity Framework Core menyederhanakan interaksi dengan database relasional lewat DbContext dan DbSet yang bisa di-query dengan LINQ, menghilangkan kebutuhan menulis SQL manual untuk operasi CRUD sehari-hari. Pahami change tracking dan kapan memanggil SaveChanges, serta manfaatkan logging untuk memahami SQL yang sebenarnya dihasilkan di balik layar.