Mert Özen Her Satırda Daha İleri
Backend LINQ — #01

LINQ Nedir? C# ile Query Syntax ve Method Syntax

Mert Özen 7 Kas 2026 11 dk 2 görüntülenme
LINQ Nedir? C# ile Query Syntax ve Method Syntax

LINQ'in döngülerle yazdığımız sorgulara getirdiği değişiklik, onu mümkün kılan C# 3.0 özellikleri, query syntax'ın derleyicide method çağrısına nasıl çevrildiği, hangi yazımı ne zaman seçmek gerektiği ve ilk tuzaklar.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Elimizde beş kitaplık küçük bir katalog var. Aşağıdaki sorgu SQL'e benziyor ama saf C#. Ekrana hangi kitaplar, hangi sırayla yazılır? Bir de şunu düşün: from ile başlayan bu expression aslında ne, derleyici onu neye çeviriyor?

var books = new List<Book>
{
    new("978-0132350884", "Clean Code", "Robert C. Martin", 37.90m, 12),
    new("978-0135957059", "The Pragmatic Programmer", "David Thomas", 42.50m, 0),
    new("978-0321125217", "Domain-Driven Design", "Eric Evans", 54.99m, 3),
    new("978-0201633610", "Design Patterns", "Erich Gamma", 49.95m, 7),
    new("978-1617294532", "C# in Depth", "Jon Skeet", 44.99m, 5),
};

IEnumerable<string> titles =
    from book in books
    where book.Stock > 0 && book.Price < 50m
    orderby book.Price descending
    select book.Title;

foreach (string title in titles)
    Console.WriteLine(title);

record Book(string Isbn, string Title, string Author, decimal Price, int Stock);

Döngüyle Aynı İş

Çıktı şu:

Design Patterns
C# in Depth
Clean Code

Stokta olan ve 50'den ucuz kitaplar kalıyor, en pahalıdan en ucuza diziliyor. The Pragmatic Programmer stokta olmadığı için, Domain-Driven Design de 54.99 olduğu için eleniyor. Sorgu ne istediğimizi söylüyor: "stokta olan ve ucuz kitapları al, fiyata göre azalan sırala, başlıklarını ver". Nasıl yapılacağı hakkında tek kelime yok.

LINQ'ten önce aynı işi şöyle yazardık:

var selected = new List<Book>();
foreach (Book book in books)
{
    if (book.Stock > 0 && book.Price < 50m)
        selected.Add(book);
}

selected.Sort((x, y) => y.Price.CompareTo(x.Price));

var titles = new List<string>();
foreach (Book book in selected)
    titles.Add(book.Title);

Console.WriteLine(string.Join(" | ", titles));
Design Patterns | C# in Depth | Clean Code

Sonuç aynı, ama kodun büyük kısmı "nasıl" ile meşgul: ara liste aç, dolaş, koşula bak, ekle, bir karşılaştırma fonksiyonu yaz, bir kez daha dolaş, başlıkları topla. Okuyan kişi niyeti bu adımlardan geri çıkarmak zorunda. Her yeni rapor için bu pattern'ı tekrar yazarsın ve her seferinde küçük bir hata fırsatı doğar: karşılaştırmada x ile y'nin yerini karıştırmak, ara listeyi temizlemeyi unutmak gibi.

LINQ (Language Integrated Query), sorgulamayı dilin bir parçası yapar. Restoranda mutfağa girip yemeği kendin pişirmek yerine garsona siparişini söylemek gibi düşün: "ne" istediğini söylersin, "nasıl" kısmını library halleder. Bu serideki her yazıda bu "ne" kelime dağarcığını, yani LINQ operatörlerini tek tek açacağız.

Aynı Sorgu, İki Yazım

LINQ'i iki şekilde yazabilirsin. Tahmin sorusundaki from ... where ... select biçimine query syntax denir. Aynı sorgunun method syntax karşılığı, extension method'ları zincirlemektir:

IEnumerable<string> querySyntax =
    from book in books
    where book.Stock > 0 && book.Price < 50m
    orderby book.Price descending
    select book.Title;

IEnumerable<string> methodSyntax = books
    .Where(book => book.Stock > 0 && book.Price < 50m)
    .OrderByDescending(book => book.Price)
    .Select(book => book.Title);

Console.WriteLine(string.Join(" | ", methodSyntax));
Console.WriteLine(querySyntax.SequenceEqual(methodSyntax));
Design Patterns | C# in Depth | Clean Code
True

SequenceEqual iki sequence'ı element element karşılaştırır ve True dönüyor: iki yazım aynı sonucu üretiyor. Bu bir tesadüf değil. Derleyici query syntax'ı derlemenin ilk adımında method çağrılarına çevirir. where bir Where çağrısına, orderby ... descending bir OrderByDescending çağrısına, select de bir Select çağrısına dönüşür. Ortaya çıkan IL iki yazımda da aynı method'ları çağırır; aralarında performans farkı yoktur.

Where, OrderByDescending ve Select, System.Linq namespace'indeki Enumerable static class'ında tanımlı extension method'lardır ve IEnumerable<T> üzerinde çalışırlar. Array, List<T>, Dictionary<TKey, TValue>, HashSet<T> ya da kendi yazdığın bir iterator; IEnumerable<T> implemente eden her şey LINQ ile sorgulanabilir. Dictionary'yi sorguladığında dolaştığın element'ler KeyValuePair<TKey, TValue> olur. .NET 6 console template'indeki ImplicitUsings ayarı System.Linq'i global olarak eklediği için dosyanın başına using yazmana gerek kalmaz.

LINQ'i Mümkün Kılan C# 3.0 Parçaları

LINQ, Kasım 2007'de C# 3.0 ve .NET Framework 3.5 ile geldi. Tek bir özellik değil, aynı sürümde birlikte gelen birkaç dil özelliğinin toplamıdır: extension method, lambda expression, var ile tip çıkarımı, anonymous type, object ve collection initializer, expression tree ve query expression. Bunların her biri tek başına da işe yarar, ama asıl anlamlarını LINQ'te bulurlar.

LINQ'in sihirli olmadığını görmenin en iyi yolu, onun küçük bir parçasını kendin yazmak:

// Kendi yazdığımız extension method; LINQ'in Where'i ile aynı fikir
foreach (Book book in books.Filter(b => b.Stock == 0))
    Console.WriteLine(book.Title);

// Anonymous type: tipin adını derleyici koyar
var summary = books
    .Select(b => new { b.Title, InStock = b.Stock > 0 })
    .Take(2);

foreach (var item in summary)
    Console.WriteLine(item);

static class SequenceExtensions
{
    public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        foreach (T item in source)
        {
            if (predicate(item))
                yield return item;
        }
    }
}
The Pragmatic Programmer
{ Title = Clean Code, InStock = True }
{ Title = The Pragmatic Programmer, InStock = False }

Filter, this keyword'üyle işaretlenmiş ilk parametresi sayesinde bir extension method. books.Filter(...) yazdığında derleyici bunu SequenceExtensions.Filter(books, ...) çağrısına çevirir. b => b.Stock == 0 bir lambda: derleyici onu Func<Book, bool> tipinde bir delegate'e dönüştürür. Gövdedeki yield return ise element'leri birer birer üretir. Koleksiyon serisindeki iterator yazısını okuduysan bu yapı tanıdık gelecek. .NET'in kendi Where'i argüman kontrolü ve birkaç optimizasyon dışında aynı fikirle çalışır.

İkinci kısımdaki new { b.Title, InStock = b.Stock > 0 } bir anonymous type. Adını derleyici koyar, property'leri read-only'dir ve ToString() çıktısı da derleyicinin ürettiği bu { Title = ..., InStock = ... } biçimidir. Tipin bir adı olmadığı için değişkeni var ile tanımlamak zorundasın; var'ın C# 3.0'a LINQ yüzünden eklenmesinin sebebi de bu. Projection yazısında anonymous type'lara ayrıca döneceğiz.

Query Syntax Derleyicide Neye Dönüşür

Query syntax'ın method çağrısına çevrildiğini söyledik. Bu çeviri tamamen isim bazlıdır: derleyici Where ve Select adında, uygun signature'lı method'lar arar. Bunlar IEnumerable<T> üzerinde olmak zorunda bile değil:

var shelf = new Shelf(books);

// Shelf bir IEnumerable değil; yine de query syntax derleniyor
var expensive =
    from b in shelf
    where b.Price > 45m
    select b.Title;

foreach (string title in expensive)
    Console.WriteLine(title);

class Shelf
{
    private readonly List<Book> _books;

    public Shelf(IEnumerable<Book> books) => _books = new List<Book>(books);

    public Shelf Where(Func<Book, bool> predicate)
    {
        Console.WriteLine("Shelf.Where called");
        var kept = new List<Book>();
        foreach (Book book in _books)
        {
            if (predicate(book))
                kept.Add(book);
        }
        return new Shelf(kept);
    }

    public List<TResult> Select<TResult>(Func<Book, TResult> selector)
    {
        Console.WriteLine("Shelf.Select called");
        var results = new List<TResult>();
        foreach (Book book in _books)
            results.Add(selector(book));
        return results;
    }
}
Shelf.Where called
Shelf.Select called
Domain-Driven Design
Design Patterns

Shelf ne IEnumerable<T> implemente ediyor ne de LINQ'ten bir şey kullanıyor. Yine de query syntax derlendi, çünkü derleyici from b in shelf where ... select ... expression'ını kelimesi kelimesine shelf.Where(b => b.Price > 45m).Select(b => b.Title) hâline getirdi ve bu çağrılar Shelf'in kendi method'larına bağlandı. Query syntax bir yazım kısaltmasıdır; anlamını, çağrılan method'lar belirler.

Çıktıda bir ayrıntıya dikkat et: "Shelf.Where called" ve "Shelf.Select called" satırları, foreach başlamadan, sorgu tanımlandığı anda yazıldı. Bizim method'larımız işi hemen yapıp liste döndürüyor. LINQ'in kendi operatörleri ise böyle davranmaz; sorguyu tarif eden bir object döndürüp işi dolaşma anına bırakırlar. Bir sonraki yazının konusu tam olarak bu fark.

Hangisini Ne Zaman Yazmalı

Ekiplerin çoğu günlük kodda method syntax kullanır. Her operatörün query karşılığı yoktur; query syntax yalnızca from, where, select, orderby, group, join, let ve into gibi birkaç keyword'ü tanır. Count, Take, Distinct, First, Sum gibi operatörlerin keyword'ü yoktur, onları method syntax ile çağırırsın. Query syntax'ın parladığı yerler ise ara değişken gerektiren ya da birden fazla kaynağı birleştiren sorgulardır:

using System.Globalization;

// Ondalık ayırıcı her makinede aynı olsun
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

// let ara bir değeri bir kez hesaplayıp isim verir
List<string> discounted =
    (from b in books
     let salePrice = b.Price * 0.9m
     where salePrice < 45m
     orderby salePrice
     select $"{b.Title}: {salePrice:0.00}")
    .Take(3)
    .ToList();

discounted.ForEach(Console.WriteLine);

// Count ve Distinct'in query keyword'ü yok, method syntax ile devam edilir
int inStock = (from b in books where b.Stock > 0 select b).Count();
int authors = books.Select(b => b.Author).Distinct().Count();
Console.WriteLine($"In stock: {inStock}, authors: {authors}");
Clean Code: 34.11
The Pragmatic Programmer: 38.25
C# in Depth: 40.49
In stock: 4, authors: 5

let, indirimli fiyatı bir kez hesaplayıp ona isim veriyor; where, orderby ve select bu ismi tekrar hesaplamadan kullanıyor. Aynı sorguyu method syntax ile yazmak istersen, ara değeri taşımak için önce bir anonymous type'a Select etmen gerekir; derleyici let'i zaten böyle çevirir. Sorgunun sonunda parantezle kapatıp .Take(3).ToList() ile devam ettik: iki yazım aynı expression'da rahatça karışabilir.

Pratik bir kural: tek kaynaklı, birkaç adımlı sorgularda method syntax daha kısa ve araç desteği (IntelliSense, refactoring) daha iyi. let, join ya da iç içe from gerektiren, SQL'e benzeyen sorgularda query syntax daha okunaklı olabilir. Tek kural tutarlılık: aynı kod tabanında birinin query, diğerinin method syntax ile aynı tür sorguyu yazması okuyanı yorar.

Tek Syntax, Farklı Veri Kaynakları

LINQ'in adındaki "Language Integrated" kısmı şunu vaat ediyor: veri bellekte, bir XML belgesinde ya da veritabanında olsun, sorguyu aynı dille yazarsın. Bu yazıda kullandığımız bellek içi collection'lar için olanına LINQ to Objects denir. Aynı syntax'la XML de sorgulanır:

using System.Xml.Linq;

XElement catalog = XElement.Parse(@"
<catalog>
  <book isbn='978-0132350884' price='37.90'>Clean Code</book>
  <book isbn='978-0321125217' price='54.99'>Domain-Driven Design</book>
  <book isbn='978-1617294532' price='44.99'>C# in Depth</book>
</catalog>");

IEnumerable<string> overForty =
    from book in catalog.Elements("book")
    where (decimal)book.Attribute("price")! > 40m
    select $"{book.Value} ({book.Attribute("isbn")!.Value})";

foreach (string line in overForty)
    Console.WriteLine(line);
Domain-Driven Design (978-0321125217)
C# in Depth (978-1617294532)

System.Xml.Linq'teki XElement.Elements("book") bir IEnumerable<XElement> döndürüyor, gerisi bildiğimiz LINQ. Attribute'u decimal'e çevirmek için XAttribute'un explicit dönüşüm operatörünü kullandık.

Veritabanı tarafında durum biraz farklı. EF Core'da dbContext.Books.Where(b => b.Price > 40m) yazdığında Where çağrısı IEnumerable<T> değil IQueryable<T> üzerinde yapılır. Bu durumda lambda çalıştırılacak bir kod olarak değil, incelenebilir bir expression tree olarak taşınır ve provider onu SQL'e çevirir. Syntax aynı, ama sorguyu kimin ve nerede çalıştırdığı tamamen değişir. Bu ayrımı bir sonraki yazıda ayrıntılı açacağız; EF Core serisinde de bu çevirinin pratikte nasıl göründüğünü görmüştük.

Sık Düşülen Tuzaklar

using System.Linq'i unutmak. Yeni template'lerde ImplicitUsings açık olduğu için bu sorunla pek karşılaşmazsın, ama ayarı kapalı eski bir projede ya da bir library'de şunu yazarsan:

using System;
using System.Collections.Generic;

var books = new List<Book>
{
    new("978-0132350884", "Clean Code", 37.90m),
    new("978-0321125217", "Domain-Driven Design", 54.99m),
};

var cheap = books.Where(b => b.Price < 40m);
Console.WriteLine(cheap.Count());

record Book(string Isbn, string Title, decimal Price);

derleyici List<Book>'da Where diye bir şey bulamaz:

Program.cs(10,19): error CS1061: 'List<Book>' does not contain a definition for 'Where' and no accessible extension method 'Where' accepting a first argument of type 'List<Book>' could be found (are you missing a using directive or an assembly reference?)

Mesaj, sebebi de söylüyor: Where, List<T>'nin kendi method'u değil, bir extension method ve extension method'lar ancak namespace'leri using ile görünür olduğunda bulunur. Dosyanın başına using System.Linq; eklemek yeter.

Query syntax'ı yarıda bırakmak. Bir query expression select ya da group ile bitmek zorundadır. SQL alışkanlığıyla where'de durursan:

var books = new List<Book>
{
    new("978-0132350884", "Clean Code", 37.90m),
    new("978-0321125217", "Domain-Driven Design", 54.99m),
};

var cheap = from b in books
            where b.Price < 40m;

record Book(string Isbn, string Title, decimal Price);
Program.cs(8,32): error CS0742: A query body must end with a select clause or a group clause

LINQ operatörünün kaynağı değiştirdiğini sanmak. LINQ sorgusu her zaman yeni bir sequence üretir; kaynağa dokunmaz:

var books = new List<Book>
{
    new("978-0321125217", "Domain-Driven Design", 54.99m),
    new("978-0132350884", "Clean Code", 37.90m),
};

books.OrderBy(b => b.Price);
Console.WriteLine(books[0].Title);

List<Book> sorted = books.OrderBy(b => b.Price).ToList();
Console.WriteLine(sorted[0].Title);
Console.WriteLine(books[0].Title);

record Book(string Isbn, string Title, decimal Price);
Domain-Driven Design
Clean Code
Domain-Driven Design

İlk OrderBy çağrısının sonucu hiçbir yere atanmadığı için boşa gitti ve books aynı sırada kaldı. Derleyici bu satır için uyarı vermez. Sıralı sonucu kullanmak istiyorsan onu bir değişkene ata; listeyi yerinde sıralamak istiyorsan List<T>.Sort kullan. Aynı karışıklık List<T>.ForEach için de geçerli: o bir List<T> method'u, LINQ operatörü değil ve IEnumerable<T> üzerinde yoktur.

Her şeyi tek bir dev sorguya sıkıştırmak. On satırlık bir zincir okunmuyorsa, ara adımlara isim ver: var inStock = books.Where(...), var cheapest = inStock.OrderBy(...). LINQ operatörleri ara sonuç hesaplamadığı için bu bölme maliyetsizdir; nedenini bir sonraki yazıda göreceğiz.

Küçük Bir Deneme

★☆☆ Tahmin sorusundaki kataloğa yeni bir kitap ekle ve sorguyu "yazarı E ile başlayan kitapların başlıkları, alfabetik" olarak değiştir. Önce query syntax ile, sonra method syntax ile yaz ve SequenceEqual ile iki sonucun aynı olduğunu doğrula.

★★☆ let kullanan sorguyu method syntax ile yeniden yaz. İndirimli fiyatı taşımak için bir anonymous type'a Select etmen gerekecek. Hangi yazımı daha okunaklı buldun?

★★★ Shelf class'ına bir OrderBy<TKey>(Func<Book, TKey> keySelector) method'u ekle ve from b in shelf orderby b.Price select b.Title sorgusunu derlenir hâle getir. İpucu: orderby clause'u derleyicinin aradığı OrderBy çağrısına dönüşür, Shelf döndürürsen zincir Select ile devam edebilir.

Bu yazıda Shelf'in method'larının sorgu tanımlandığı anda çalıştığını, LINQ'inkilerin ise beklediğini söyledik. Bir sonraki yazıda bu beklemenin, yani deferred execution'ın ne demek olduğuna, sorgunun tam olarak hangi anda çalıştığına ve IEnumerable<T> ile IQueryable<T> arasındaki farka bakacağız.