EF Core vs Dapper — Ne Zaman Hangisi?
EF Core vs Dapper — Ne Zaman Hangisi?
Yanlış Soru
"EF Core mı, Dapper mı daha iyi?" sorusu yanlış sorudur.
Doğru soru şudur: Bu proje için hangisi daha uygun?
İkisi de olgun, production-grade araçlardır. İkisinin de güçlü olduğu senaryolar farklıdır. Birini körü körüne seçmek — "ORM kullanmak tembelliktir" veya "Dapper eskidir" gibi ideolojik gerekçelerle — projenize zarar verir.
Bu yazı, o kararı bilinçli vermenizi sağlayacak çerçeveyi sunuyor.
Kısa Tanıtım
EF Core Nedir?
Entity Framework Core, Microsoft'un .NET için geliştirdiği tam özellikli ORM'dir (Object-Relational Mapper). C# nesnelerinizi veritabanı tablolarıyla eşleştirir. LINQ sorguları SQL'e çevirir. Migration'larla şema değişikliklerini yönetir.
// EF Core ile veri çekme
var orders = await context.Orders
.Include(o => o.Customer)
.Where(o => o.Status == OrderStatus.Active && o.TotalAmount > 1000)
.OrderByDescending(o => o.CreatedAt)
.Take(50)
.ToListAsync();
Dapper Nedir?
Dapper, Stack Overflow ekibinin geliştirdiği micro-ORM'dir. Ham SQL yazarsınız, Dapper sonuçları C# nesnelerine map eder. Başka bir şey yapmaz — ve bu kasıtlıdır.
// Dapper ile aynı veri
var orders = await connection.QueryAsync<OrderDto>(@"
SELECT TOP 50
o.OrderId, o.TotalAmount, o.CreatedAt,
c.Name AS CustomerName
FROM Orders o
INNER JOIN Customers c ON c.CustomerId = o.CustomerId
WHERE o.Status = 'Active'
AND o.TotalAmount > 1000
ORDER BY o.CreatedAt DESC",
connection: connection);
Performans Karşılaştırması
Dapper'ın EF Core'dan daha hızlı olduğu genel bir kabuldür. Bu doğrudur — ama bağlamı olmadan anlamsızdır.
Benchmark Gerçeği
Basit bir SELECT 1000 satır senaryosunda tipik sonuçlar:
| Araç | Süre | Bellek |
|---|---|---|
| Ham ADO.NET | ~15ms | Düşük |
| Dapper | ~18ms | Düşük |
| EF Core (No-tracking) | ~25ms | Orta |
| EF Core (Tracking) | ~35ms | Yüksek |
Ancak bu rakamlar bağımsız değerlendirilemez.
Sorgunuzun toplam süresi 200ms ise, EF Core'un 10ms fazla alması toplam süreyi %5 etkiler. Asıl darboğaz neredeyse her zaman veritabanı tarafındaki yanlış index, kötü yazılmış sorgu veya N+1 problemidir — ORM seçimi değil.
EF Core'u Hızlandırmak
EF Core, doğru kullanıldığında Dapper'a yakın performans gösterir:
// AsNoTracking — read-only sorgularda her zaman kullanın
var products = await context.Products
.AsNoTracking()
.Where(p => p.CategoryId == categoryId)
.ToListAsync();
// Projection — sadece ihtiyaç duyduğunuz kolonları çekin
var dtos = await context.Orders
.AsNoTracking()
.Where(o => o.CustomerId == customerId)
.Select(o => new OrderSummaryDto
{
OrderId = o.OrderId,
TotalAmount = o.TotalAmount,
CreatedAt = o.CreatedAt
})
.ToListAsync();
// Compiled Query — tekrarlanan sorgularda LINQ derleme overhead'ini ortadan kaldırır
private static readonly Func<AppDbContext, int, Task<List<Product>>> GetByCategory =
EF.CompileAsyncQuery((AppDbContext ctx, int categoryId) =>
ctx.Products.Where(p => p.CategoryId == categoryId));
EF Core'un Güçlü Olduğu Durumlar
1. CRUD Ağırlıklı Uygulamalar
Oluştur, oku, güncelle, sil. İş kuralları var, ilişkiler var, ama sorgular karmaşık değil.
// Create
context.Products.Add(new Product(name, price, categoryId));
await context.SaveChangesAsync();
// Update — entity'yi çek, değiştir, kaydet
var product = await context.Products.FindAsync(productId);
product.UpdatePrice(newPrice);
await context.SaveChangesAsync();
// Delete — soft delete interceptor ile otomatik
context.Products.Remove(product);
await context.SaveChangesAsync();
Bunu Dapper ile yapmak mümkündür ama her işlem için ayrı SQL yazmak gerekir. Yüzlerce entity'si olan bir projede bu ciddi bir bakım yüküdür.
2. Migration ve Şema Yönetimi
Takımda birden fazla geliştirici varsa ve şema sürekli değişiyorsa, EF Core migration'ları paha biçilmezdir.
dotnet ef migrations add AddProductStockColumn
dotnet ef database update
Her şema değişikliği için elle SQL script yazmak ve versiyonlamak zorunda kalmamak büyük bir avantajdır.
3. Domain-Driven Design ve Rich Domain Model
EF Core, private setter'lar, owned entity'ler ve value object'lerle çalışabilir. Dapper bunu doğrudan desteklemez.
public class Order
{
private readonly List<OrderItem> _items = new();
public Guid Id { get; private set; }
public Money TotalAmount { get; private set; } // Value Object
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity)
{
_items.Add(new OrderItem(product.Id, product.Price, quantity));
RecalculateTotal();
}
}
// EF Core konfigürasyonu
modelBuilder.Entity<Order>(builder =>
{
builder.OwnsOne(o => o.TotalAmount, money =>
{
money.Property(m => m.Amount).HasColumnName("TotalAmount");
money.Property(m => m.Currency).HasColumnName("Currency");
});
builder.HasMany(o => o.Items)
.WithOne()
.HasForeignKey("OrderId");
});
4. Interceptor ve Global Filter Desteği
Audit log, soft delete, multi-tenancy gibi cross-cutting concern'ler EF Core'da merkezi olarak yönetilebilir.
// Tüm sorgulara otomatik soft delete filtresi
modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);
// SaveChanges interceptor ile audit log
public class AuditInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData, InterceptionResult<int> result)
{
var entries = eventData.Context.ChangeTracker.Entries()
.Where(e => e.State is EntityState.Added or EntityState.Modified);
foreach (var entry in entries)
{
if (entry.Entity is IAuditable auditable)
{
auditable.UpdatedAt = DateTime.UtcNow;
auditable.UpdatedBy = _currentUser.Id;
}
}
return base.SavingChanges(eventData, result);
}
}
Dapper'ın Güçlü Olduğu Durumlar
1. Karmaşık Raporlama Sorguları
Window fonksiyonları, CTE'ler, pivot sorgular — bunları LINQ ile ifade etmek ya imkânsızdır ya da okunaksız SQL üretir.
var report = await connection.QueryAsync<SalesReportDto>(@"
WITH MonthlySales AS (
SELECT
YEAR(OrderDate) AS SalesYear,
MONTH(OrderDate) AS SalesMonth,
SUM(TotalAmount) AS Revenue,
COUNT(*) AS OrderCount
FROM Orders
WHERE OrderDate >= @StartDate
GROUP BY YEAR(OrderDate), MONTH(OrderDate)
)
SELECT
SalesYear,
SalesMonth,
Revenue,
OrderCount,
SUM(Revenue) OVER (PARTITION BY SalesYear ORDER BY SalesMonth) AS YTDRevenue,
LAG(Revenue, 1) OVER (ORDER BY SalesYear, SalesMonth) AS PreviousMonthRevenue,
ROUND((Revenue - LAG(Revenue, 1) OVER (ORDER BY SalesYear, SalesMonth))
/ NULLIF(LAG(Revenue, 1) OVER (ORDER BY SalesYear, SalesMonth), 0) * 100, 2)
AS GrowthRate
FROM MonthlySales
ORDER BY SalesYear, SalesMonth",
new { StartDate = startDate });
Bunu EF Core LINQ'uyla yazmayı deneyin — ya çeviri hatası alırsınız ya da client-side evaluation tuzağına düşersiniz.
2. Yüksek Hacimli, Performans Kritik Noktalar
Saniyede binlerce işlem yapılan bir endpoint, her milisaniyenin önemli olduğu bir batch job — burada Dapper'ın düşük overhead'i fark yaratır.
// Büyük toplu insert — Dapper ile SqlBulkCopy kombinasyonu
public async Task BulkInsertAsync(IEnumerable<PriceUpdate> updates)
{
using var bulkCopy = new SqlBulkCopy(connection);
bulkCopy.DestinationTableName = "PriceUpdates";
var table = new DataTable();
// ... DataTable doldurma
await bulkCopy.WriteToServerAsync(table);
}
// Ya da Dapper ile toplu parametre
await connection.ExecuteAsync(
"UPDATE Products SET Price = @Price WHERE ProductId = @ProductId",
updates.Select(u => new { u.Price, u.ProductId }));
3. Mevcut Veritabanı — ORM Eşleme Zorluğu
Legacy bir veritabanıyla çalışıyorsanız — tutarsız isimlendirme, çoklu primary key'ler, saklı prosedürler, view'lar — EF Core konfigürasyonu kabusa dönebilir. Dapper burada çok daha esnek davranır.
// Saklı prosedür çağrısı — Dapper ile son derece temiz
var result = await connection.QueryAsync<CustomerSummary>(
"sp_GetCustomerSummary",
new { CustomerId = customerId, IncludeInactive = false },
commandType: CommandType.StoredProcedure);
// Birden fazla result set
using var multi = await connection.QueryMultipleAsync(
"sp_GetOrderWithDetails",
new { OrderId = orderId },
commandType: CommandType.StoredProcedure);
var order = await multi.ReadSingleAsync<Order>();
var items = await multi.ReadAsync<OrderItem>();
var payments = await multi.ReadAsync<Payment>();
4. Takımda Güçlü SQL Bilgisi Var
DBA'lar veya SQL'e hakim geliştiricilerden oluşan bir takım, ORM soyutlaması olmadan doğrudan SQL yazarak daha verimli çalışabilir. Üretilen SQL'i tahmin etmeye çalışmak yerine tam kontrol tercih edilebilir.
İkisini Birlikte Kullanmak — Hibrit Yaklaşım
Bu seçimin siyah-beyaz olmak zorunda olmadığını anlamak önemlidir. Aynı projede EF Core ve Dapper birlikte kullanılabilir — ve çoğu zaman bu en iyi yaklaşımdır.
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
private readonly IDbConnection _connection;
public OrderRepository(AppDbContext context, IDbConnection connection)
{
_context = context;
_connection = connection;
}
// Yazma işlemleri — EF Core
public async Task<Order> CreateAsync(Order order)
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return order;
}
// Basit okuma — EF Core
public async Task<Order?> GetByIdAsync(Guid id)
{
return await _context.Orders
.Include(o => o.Items)
.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == id);
}
// Karmaşık raporlama — Dapper
public async Task<IEnumerable<OrderDashboardDto>> GetDashboardDataAsync(
DateTime startDate, DateTime endDate)
{
return await _connection.QueryAsync<OrderDashboardDto>(@"
SELECT
o.CustomerId,
c.Name AS CustomerName,
COUNT(o.OrderId) AS TotalOrders,
SUM(o.TotalAmount) AS TotalRevenue,
AVG(o.TotalAmount) AS AvgOrderValue,
MAX(o.CreatedAt) AS LastOrderDate
FROM Orders o
INNER JOIN Customers c ON c.CustomerId = o.CustomerId
WHERE o.CreatedAt BETWEEN @StartDate AND @EndDate
GROUP BY o.CustomerId, c.Name
ORDER BY TotalRevenue DESC",
new { StartDate = startDate, EndDate = endDate });
}
}
DI konfigürasyonu:
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddScoped<IDbConnection>(_ =>
new SqlConnection(connectionString));
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
Karar Çerçevesi
Seçim yaparken şu soruları kendinize sorun:
EF Core'u tercih edin, eğer:
✅ Proje yeni başlıyor ve domain modeli karmaşık
✅ Takımda SQL bilgisi değişken, C# bilgisi güçlü
✅ Şema sık değişiyor, migration yönetimi kritik
✅ CRUD işlemleri baskın, raporlama ikincil
✅ DDD uyguluyor, rich domain model istiyorsunuz
✅ Audit log, soft delete gibi cross-cutting concern'ler var
Dapper'ı tercih edin, eğer:
✅ Legacy veritabanı, ORM eşleme maliyeti yüksek
✅ Raporlama ağırlıklı, karmaşık analitik sorgular baskın
✅ Performans kritik noktalarda her milisaniye önemli
✅ Takımda güçlü SQL bilgisi var, DBA'larla çalışılıyor
✅ Saklı prosedür yoğun bir mimari mevcut
✅ Mikro servis, küçük scope, sınırlı domain
İkisini birlikte kullanın, eğer:
✅ Domain yazma işlemleri EF Core ile yönetilmek isteniyor
✅ Raporlama ve dashboard sorguları ayrı bir concern
✅ Proje büyüdükçe performans kritik noktalar ortaya çıkıyor
✅ Takımın bir kısmı ORM'i, diğeri SQL'i tercih ediyor
Sık Yapılan Hatalar
1. EF Core'u kör kullanmak — N+1 problemi
// ❌ Her order için ayrı sorgu — N+1
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name); // Her satırda ayrı sorgu!
}
// ✅ Eager loading ile tek sorgu
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();
2. Dapper'da SQL injection — parametreli sorgu kullanmamak
// ❌ SQL injection açığı
var query = $"SELECT * FROM Users WHERE Username = '{username}'";
var user = await connection.QueryFirstOrDefaultAsync<User>(query);
// ✅ Her zaman parametreli sorgu
var user = await connection.QueryFirstOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Username = @Username",
new { Username = username });
3. EF Core'da gereksiz tracking
// ❌ Read-only sorguda tracking overhead'i
var products = await context.Products.ToListAsync();
// ✅ AsNoTracking ile düşük bellek ve hız
var products = await context.Products.AsNoTracking().ToListAsync();
Temel Çıkarımlar
- EF Core vs Dapper tartışması ideolojik değil, pragmatik olmalıdır
- EF Core CRUD, migration ve domain model için güçlüdür
- Dapper karmaşık sorgular, legacy DB ve yüksek performans için güçlüdür
- İkisini aynı projede birlikte kullanmak hem mümkündür hem de çoğu zaman en doğru karardır
- Gerçek performans darboğazı ORM seçimi değil, index stratejisi ve sorgu tasarımıdır
- Takımın yetkinlik profili teknik tercih kadar önemlidir
Araç tartışması yapmak yerine problemi anlayın. Doğru araç, probleminizi en az karmaşıklıkla çözendir.