LINQ (Language Integrated Query), koleksiyonlar ve sorgu sağlayıcıları üzerinde filtreleme, dönüştürme, gruplama ve sıralama işlemlerini C# dil yapılarıyla ifade etmeyi sağlar; C# 3.0 ve .NET Framework 3.5 ile birlikte kullanıma girmiştir. Sözdizimi aynı görünse de IEnumerable<T> üzerinde bellekte çalışan sorgular ile IQueryable<T> üzerinden başka bir dile (örneğin SQL’e) çevrilen sorgular farklı yürütme kurallarına sahiptir.
Aşağıda iki yazım biçimi, ertelenmiş yürütme, sağlayıcılar arasındaki farklar, EF Core’un çeviri sınırları ve LINQ’in elle yazılmış SQL ile karşılaştırması örneklerle ele alınıyor.
LINQ Hangi Sorunu Çözer?
İç içe döngüler, koşullar ve geçici listelerle yazılan bir veri dönüşümü LINQ ile daha bildirimsel ifade edilebilir. SQL yazdıysanız Where, OrderBy ve GroupBy gibi işlemler tanıdık gelecektir. LINQ yalnız veritabanı sorguları için değildir; diziler, listeler, sözlükler, XML ve IEnumerable<T> uygulayan her tip üzerinde çalışır.
LINQ’in standart sorgu operatörleri, fonksiyonel programlamadaki map, filter ve reduce işlemlerinin C# karşılığıdır: Select, Where ve Aggregate. Bu kısalık tek başına performans garantisi değildir; sorgunun ne zaman çalıştığını ve sağlayıcının ifadeyi nasıl çevirdiğini bilmek gerekir.
Sorgu Sözdizimi ve Metot Sözdizimi
LINQ iki biçimde yazılabilir. Sorgu sözdizimi (query syntax) SQL’e benzer; metot sözdizimi (method syntax) uzantı metotlarını zincirler. Yazının geri kalanındaki örnekler aşağıdaki products listesini kullanır:
using System;
using System.Collections.Generic;
using System.Linq;
var products = new List<Product>
{
new(1, "Laptop", 1000m, "Electronics"),
new(2, "Mouse", 20m, "Electronics"),
new(3, "Book", 15m, "Education"),
new(4, "Notebook", 12m, "Education"),
};
// Metot sözdizimi: filtrele, sırala, yansıt
var expensiveItems = products
.Where(p => p.Price > 50m)
.OrderByDescending(p => p.Price)
.Select(p => new { p.Name, p.Price });
// Sorgu sözdizimi: aynı sonuç
var expensiveItemsQuery =
from p in products
where p.Price > 50m
orderby p.Price descending
select new { p.Name, p.Price };
foreach (var item in expensiveItemsQuery)
{
Console.WriteLine(item); // { Name = Laptop, Price = 1000 }
}
public record Product(int Id, string Name, decimal Price, string Category);
C# derleyicisi sorgu sözdizimini metot çağrılarına çevirir; iki biçim anlam olarak aynıdır. Seçimde ölçüt okunurluk ve kapsamdır. Count, Sum, Max, First, Take, Skip ve Distinct gibi operatörlerin C# sorgu sözdiziminde anahtar kelimesi yoktur; bunlar metot çağrısıyla yazılır ya da sorgu parantez içine alınıp sonuna eklenir. Buna karşılık birden çok from, join veya let içeren sorgular sorgu sözdiziminde daha okunaklı olabilir. Art arda iki from yan tümcesi derleyici tarafından SelectMany çağrısına çevrilir; SelectMany, monad’lardaki flatMap işleminin LINQ’teki karşılığıdır.
Ertelenmiş Yürütme (Deferred Execution)
Where veya Select çağrısı sorguyu çalıştırmaz, yalnız tanımlar. Sorgu, sonuç gerçekten dolaşıldığında (foreach, ToList(), Count(), First() gibi) çalışır ve her dolaşmada yeniden çalışır:
var numbers = new List<int> { 1, 2, 3 };
var evens = numbers.Where(n => n % 2 == 0); // sorgu tanımlandı, çalışmadı
numbers.Add(4);
Console.WriteLine(string.Join(", ", evens)); // 2, 4
var snapshot = evens.ToList(); // sorgu şimdi çalışır, sonuç kopyalanır
numbers.Add(6);
Console.WriteLine(string.Join(", ", evens)); // 2, 4, 6 (yeniden çalıştı)
Console.WriteLine(string.Join(", ", snapshot)); // 2, 4
Bu davranış gereksiz işi önleyebilir, ama aynı sorgunun birden çok kez dolaşılması bellekte tekrarlanan hesaba, IQueryable sorgularında ise veritabanına tekrarlanan isteklere yol açar. Sonucu bir kez hesaplayıp tekrar kullanmak istediğinizde ToList() veya ToArray() ile somutlaştırın.
LINQ Sağlayıcıları: Aynı Sözdizimi, Farklı Yürütme Kuralları
- LINQ to Objects: Bellekteki koleksiyonlar (
List<T>, diziler,Dictionary<TKey, TValue>). Lambda ifadeleri sıradan C# kodu olarak çalışır. - EF Core (LINQ to Entities):
IQueryable<T>sorgularını ifade ağacı (expression tree) olarak alır ve veritabanı sağlayıcısı aracılığıyla SQL’e çevirir. EF Core sağlayıcısı olan veritabanlarıyla çalışır (SQL Server, PostgreSQL, SQLite, MySQL ve diğerleri). - LINQ to SQL: .NET Framework 3.5 ile gelen, SQL Server için tasarlanmış eski bir ORM’dir ve .NET Framework’ün bileşenidir. Güncel .NET (.NET 5 ve sonrası) projelerinde ilişkisel veritabanı erişimi için EF Core kullanılır.
- LINQ to XML:
System.Xml.Linqad alanındakiXDocumentveXElementtipleriyle XML sorgulama. - JSON: Newtonsoft.Json kütüphanesi “LINQ to JSON” adıyla
JObjectveJArraytiplerini sunar. .NET’in yerleşikSystem.Text.Jsonkütüphanesinde (.NET 6+)JsonNodeDOM’u bulunur;JsonArray,IList<JsonNode?>uyguladığı için LINQ to Objects ile sorgulanabilir. - Parallel LINQ (PLINQ):
AsParallel()ile sorguyu birden çok çekirdeğe dağıtır. Küçük veri kümelerinde paralelleştirmenin ek yükü kazancı aşabilir.
Sözdizimi aynı kalsa da yürütme kuralları sağlayıcıya göre değişir. EF Core 3.0’dan beri, son Select çağrısı (üst düzey projeksiyon) dışında SQL’e çevrilemeyen bir ifade gördüğünde sorguyu sessizce istemcide çalıştırmaz, çalışma zamanında hata fırlatır. Aşağıdaki basitleştirilmiş örnekte db, Products tablosunu içeren bir EF Core DbContext örneğidir:
static bool IsCheap(Product p) => p.Price < 50m;
// Derlenir, ama IsCheap SQL'e çevrilemediği için çalışırken hata verir
var cheap = db.Products.Where(p => IsCheap(p)).ToList();
// Çevrilebilir sürüm: koşulu doğrudan ifade olarak yazın
var cheapOk = db.Products.Where(p => p.Price < 50m).ToList();
Filtreyi bilerek istemcide çalıştırmak gerekiyorsa önce AsEnumerable() veya ToList() çağrılır. Bu durumda filtrelenmemiş bütün satırlar belleğe çekilir; küçük tablolar dışında tercih edilmemelidir.
Metot Zincirleme
LINQ metotları zincirlenebilir; her adım bir öncekinin sonucunu alır:
// Kategoriye, sonra fiyata göre sıralanmış ikinci sayfa (sayfa boyutu 2)
var page = products
.Where(p => p.Price >= 10m)
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price)
.ThenBy(p => p.Id) // eşitlikte sırayı belirleyici kılar
.Skip(2)
.Take(2)
.Select(p => p.Name)
.ToList(); // sorgu burada çalışır
Console.WriteLine(string.Join(", ", page)); // Laptop, Mouse
Skip ve Take ile sayfalama yaparken sıralamanın kararlı ve belirleyici olması gerekir; aksi hâlde veritabanı aynı kaydı iki farklı sayfada döndürebilir. Distinct sonucu ise belgelerde sırasız (unordered) dizi olarak tanımlanır. Tekrarları ayıklamanız gerekiyorsa Distinct’i sıralamadan önce çağırın.
Anonim Tipler ve Gruplama
Select içinde sınıf tanımlamadan yeni bir şekil üretebilirsiniz:
var byCategory = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
Total = g.Sum(p => p.Price)
});
foreach (var row in byCategory)
{
Console.WriteLine(row);
}
// { Category = Electronics, Count = 2, Total = 1020 }
// { Category = Education, Count = 2, Total = 27 }
Anonim tipler yalnız tanımlandıkları metot içinde güçlü tiplidir; sonucu metottan döndürmek gerekiyorsa record veya tuple kullanın. GroupBy, SQL’deki GROUP BY’a benzer, ama LINQ to Objects her grubun bütün elemanlarını taşır; SQL ise her grup için tek satır döndürür. SQL tarafında seçilen her sütunun gruplamaya bağlı olması gerekir; MySQL’deki ONLY_FULL_GROUP_BY hatası bu kuraldan doğar.
LINQ ve SQL Dizeleri
LINQ, SQL’den esinlenir ama elle yazılmış SQL dizelerine göre farklı güvenceler verir:
Tür güvenliği: LINQ ifadeleri derleme zamanında tür denetiminden geçer; bir SQL dizesindeki yazım hatası ancak çalışma zamanında ortaya çıkar. IQueryable sorgularında ise ifadenin SQL’e çevrilememesi de çalışma zamanında anlaşılır.
IntelliSense: IDE özellik ve metot adlarını tamamlar.
Yeniden adlandırma: Bir özelliği IDE’nin yeniden adlandırma (refactoring) aracıyla değiştirdiğinizde LINQ ifadeleri de güncellenir; SQL dizeleri güncellenmez. Veritabanındaki sütun adı için ayrıca migration gerekir.
Hata ayıklama: LINQ to Objects sorgularında lambda içine breakpoint konabilir. SQL’e çevrilen bir sorgu ise adım adım izlenemez; üretilen SQL’i EF Core 5.0’dan beri ToQueryString() ile ya da EF Core loglarıyla görebilirsiniz.
Gerçek Dünyada Kullanım
- Veri işleme: CSV veya Excel dosyalarından okunan kayıtları filtreleme ve gruplama
- API yanıtları: JSON yanıtlarını uygulamanın iç modellerine dönüştürme
- Raporlama: İş kurallarına göre özet ve rapor üretme
- Test: Test verisini sorgulama ve doğrulama (assertion) yazma
- Yapılandırma: Yapılandırma dosyalarını okuyup doğrulama
Sonuç
LINQ, C#‘ta koleksiyon dönüşümlerini kısa ve tür güvenli biçimde yazmanın standart yoludur; SQL bilenler için öğrenme eğrisi düşüktür. Bellekteki koleksiyonlarda döngülerin yerini rahatça alır. Veritabanı sorgularında ise sözdizimi aynı kalsa da ertelenmiş yürütmeyi, sağlayıcının neyi SQL’e çevirebildiğini ve üretilen SQL’i göz önünde tutmak gerekir. Performansın kritik olduğu sık çalışan döngülerde LINQ zincirinin ek yükünü ölçüp elle yazılmış döngüyle karşılaştırmak yerinde olur.