EFEF Core Handbook

İLERİ

Raw SQL & Transactions

EF Core'un LINQ çevirisi her senaryoya yetmez — karmaşık sorgular, performans-kritik SQL'ler veya stored procedure çağrıları için raw SQL kullanırsın.

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

Raw SQL Sorguları

// Parametreli sorgu (SQL Injection korumalı!)
var minPrice = 1000m;
var products = context.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")
    .ToList();

// Stored Procedure çağrısı
var categoryId = 1;
var products = context.Products
    .FromSqlRaw("EXEC GetProductsByCategory @CategoryId = {0}", categoryId)
    .ToList();

// Non-query (INSERT/UPDATE/DELETE)
var affected = context.Database
    .ExecuteSqlInterpolated($"UPDATE Products SET IsActive = 0 WHERE Stock = 0");

GÜVENLİK: Asla string concatenation kullanma!

// ❌ YANLIŞ — SQL Injection açığı!
var sql = $"SELECT * FROM Products WHERE Name = '{userInput}'";

// ✅ DOĞRU — Parametreli
context.Products.FromSqlInterpolated($"SELECT * FROM Products WHERE Name = {userInput}");

🆕 EF Core 10: Yeni bir Roslyn analyzer eklendi — FromSqlRaw içinde string concatenation yaparsanız derleme zamanında uyarı verir:

// ⚠️ EF10 Analyzer uyarısı: EF1002 - SQL injection risk
var users = context.Users.FromSqlRaw("SELECT * FROM Users WHERE [" + fieldName + "] IS NULL");
// Güvenli olduğundan eminseniz: #pragma warning disable EF1002

FromSqlRaw vs FromSqlInterpolated

Metot Parametre Stili Güvenlik Ne Zaman Kullan
FromSqlInterpolated $"... {variable}" Otomatik parametre Varsayılan tercih
FromSqlRaw "... {0}", arg Placeholder bazlı Dinamik SQL, sp_executesql
ExecuteSqlInterpolated $"... {variable}" Otomatik parametre Non-query (UPDATE/DELETE)
ExecuteSqlRaw "... {0}", arg Placeholder bazlı Dinamik DDL

ExecuteSqlRaw güvenlik notu: FromSqlRaw ile aynı riskleri taşır. String concatenation ile değişken ekleme = SQL Injection. Her zaman {0} placeholder veya ExecuteSqlInterpolated tercih et.

// FromSqlInterpolated — $ ile yazılan her değişken parametre olur
var city = "İstanbul";
var orders = context.Orders
    .FromSqlInterpolated($"SELECT * FROM Orders WHERE City = {city}")
    .Where(o => o.IsActive)   // ← LINQ zincirlenebilir!
    .OrderBy(o => o.OrderDate)
    .ToListAsync();

// FromSqlRaw — pozisyonel placeholder
var orders = context.Orders
    .FromSqlRaw("SELECT * FROM Orders WHERE City = {0} AND Total > {1}", city, 1000)
    .ToListAsync();

// ⚠️ FromSqlRaw'da string interpolation KULLANMA:
// ❌ context.FromSqlRaw($"...WHERE Name = '{input}'")  ← SQL INJECTION!

LINQ zincirleme: FromSql* sonrasına .Where(), .OrderBy(), .Include() eklenebilir — EF bunları alt sorgu (subquery) olarak sarar.

Unmapped Type Queries (EF Core 8+)

// SqlQuery<T> — DbSet'e bağlı olmayan sorgular (DTO, scalar, aggregate)
var stats = await context.Database
    .SqlQuery<CategoryStats>($"""
        SELECT c.Name AS CategoryName, COUNT(*) AS ProductCount, AVG(p.Price) AS AvgPrice
        FROM Categories c
        JOIN Products p ON p.CategoryId = c.Id
        GROUP BY c.Name
    """)
    .OrderByDescending(s => s.ProductCount)  // ← LINQ zincirlenebilir!
    .ToListAsync();

// Scalar değer
var count = await context.Database
    .SqlQuery<int>($"SELECT COUNT(*) AS [Value] FROM Products WHERE IsActive = 1")
    .SingleAsync();

// DTO tanımı (entity olmak zorunda değil, DbSet gerekmez)
public record CategoryStats(string CategoryName, int ProductCount, decimal AvgPrice);

SqlQuery kuralları:

  • Sütun adları property adlarıyla eşleşmeli (AS ile alias ver)
  • Scalar sorgularda sütun adı Value olmalı
  • LINQ zincirleme desteklenir (subquery olarak sarılır)

Transaction Yönetimi

// Basit — SaveChanges zaten transaction kullanır
context.Products.Add(product);
context.Orders.Add(order);
await context.SaveChangesAsync();  // Tek transaction'da ikisi de kaydedilir

// Manuel transaction (birden fazla SaveChanges gerekiyorsa)
using var transaction = await context.Database.BeginTransactionAsync();
try
{
    context.Accounts.Update(sender);
    await context.SaveChangesAsync();

    context.Accounts.Update(receiver);
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

SQL karşılığı:

BEGIN TRANSACTION;

UPDATE [Accounts] SET [Balance] = [Balance] - 1000 WHERE [Id] = 1;
UPDATE [Accounts] SET [Balance] = [Balance] + 1000 WHERE [Id] = 2;

COMMIT TRANSACTION;
-- Hata olursa: ROLLBACK TRANSACTION;
BEGIN;

UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;

COMMIT;
-- Hata olursa: ROLLBACK;
-- Not: PostgreSQL'de DDL (CREATE TABLE vs.) de transaction'a dahildir!
-- SQL Server'da DDL transaction'ı implicit commit edebilir.