Expression Trees ile Dinamik LINQ

Çalışma zamanında dinamik filtreleme ve sıralama gerektiren senaryolarda expression tree'lerin nasıl oluşturulup LINQ sorgularına dönüştürüleceği anlatılır. Custom query provider yazımına giriş yapılır.

Expression Trees ile Dinamik LINQ

Expression Trees ile Dinamik LINQ: Çalışma Zamanında Sorgu İnşa Etmek

LINQ sorguları genelde derleme zamanında (compile-time) sabittir. Peki ya kullanıcı arayüzden "Ad'a göre filtrele", "Fiyata göre sırala" gibi kriterleri çalışma anında seçerse? İşte Expression Trees, C# kodunuzu veri olarak ele almanızı sağlayan ve bu dinamik ihtiyacı karşılayan en güçlü araçtır.

Bu yazıda, manuel expression oluşturmadan başlayıp, hazır kütüphaneleri ve nihayetinde custom bir sorgu sağlayıcısının (Query Provider) kapısını aralayacağız.


1. Expression Tree Nedir ve Neden Kullanılır?

Bir Expression<Func<T, bool>>, derleyici tarafından IL koduna değil, bir ağaç veri yapısına dönüştürülür. Bu ağaç; parametre, üye erişimi (MemberAccess), karşılaştırma (Equals, GreaterThan) ve mantıksal (AndAlso, OrElse) gibi düğümlerden oluşur.

  • Avantajı: Bu ağacı çalışma anında ziyaret edip (visit) değiştirebilir, yeni dallar ekleyebilir veya tamamen sıfırdan inşa edebilirsiniz.

  • Kullanım Alanı: IQueryable altyapısı (Entity Framework Core) tamamen bu prensiple çalışır. Siz Where yazdığınızda EF Core, bu ağacı ziyaret ederek SQL sorgusuna çevirir.


2. Sıfırdan Dinamik Filtre Oluşturmak (Manuel)

Diyelim ki elimizde Product sınıfı var ve kullanıcı "Name" veya "Price" alanlarından birinde filtreleme yapmak istiyor.

csharp

public class Product { public string Name { get; set; } public decimal Price { get; set; } }

// Dinamik sorgu üretici metot:
Expression<Func<Product, bool>> BuildFilter(string propertyName, string comparison, string value)
{
    // 1. Parametre: p => ... (p)
    var parameter = Expression.Parameter(typeof(Product), "p");
    
    // 2. Özelliğe erişim: p.Name veya p.Price
    var property = Expression.Property(parameter, propertyName);
    
    // 3. Sabit değer: value (Tip dönüşümüne dikkat!)
    var constant = Expression.Constant(Convert.ChangeType(value, property.Type));
    
    // 4. Karşılaştırma: p.Name == value veya p.Price > value
    Expression comparisonExpression = comparison switch
    {
        "Equals" => Expression.Equal(property, constant),
        "GreaterThan" => Expression.GreaterThan(property, constant),
        "Contains" => Expression.Call(property, "Contains", Type.EmptyTypes, constant),
        _ => throw new NotSupportedException()
    };
    
    // 5. Lambda: p => (p.Name == value)
    return Expression.Lambda<Func<Product, bool>>(comparisonExpression, parameter);
}

// Kullanım:
var filter = BuildFilter("Name", "Contains", "Laptop");
var result = context.Products.Where(filter).ToList();

⚠️ Kritik Uyarı: Contains metodu, IQueryable üzerinde çalışıyorsa SQL'de LIKE'a dönüşür; ancak IEnumerable (in-memory) üzerinde çalışıyorsa case-sensitivity sorunları yaşanabilir. Bu nedenle StringComparison parametresi eklemeyi unutmayın.


3. Sıralama (OrderBy) İçin Expression Ağacı

Sıralama biraz daha karmaşıktır çünkü OrderBy metodunun generic yapısı (IOrderedQueryable<T>) devreye girer.

csharp

IQueryable<T> OrderByDynamic<T>(IQueryable<T> source, string propertyName, bool ascending)
{
    var parameter = Expression.Parameter(typeof(T), "x");
    var property = Expression.Property(parameter, propertyName);
    var lambda = Expression.Lambda(property, parameter);
    
    var methodName = ascending ? "OrderBy" : "OrderByDescending";
    var method = typeof(Queryable).GetMethods()
        .First(m => m.Name == methodName && m.GetParameters().Length == 2)
        .MakeGenericMethod(typeof(T), property.Type);
    
    return (IQueryable<T>)method.Invoke(null, new object[] { source, lambda });
}

4. Hazır Çözüm: System.Linq.Dynamic.Core (Tavsiye Edilir!)

Yukarıdaki manuel işlemler çok eğlenceli ama üretim kodunda tekerleği yeniden icat etmeyin. NuGet: System.Linq.Dynamic.Core bu işi sizin için zaten yapıyor.

csharp

// Dinamik filtre ve sıralama tek satırda:
var result = context.Products
    .Where("Name.Contains(@0) and Price > @1", "Laptop", 5000)
    .OrderBy("Price descending")
    .Select("new(Name, Price)")
    .ToList();

Bu kütüphane, string tabanlı sorguları arka planda Expression Tree'ye çevirir. Ancak güvenlik açısından dikkatli olun: Kullanıcıdan gelen string ifadeleri (örneğin "Name.Contains") doğrudan çalıştırmak SQL Injection benzeri bir risk taşımaz çünkü parametreler (@0, @1) güvenlidir. Ancak kötü niyetli bir kullanıcı sistemde olmayan bir property ismi gönderirse runtime hatası alırsınız; bunu try-catch ile yönetin.


5. Custom Query Provider Yazımına Giriş (İleri Seviye)

Eğer EF Core veya normal LINQ to Objects dışında, örneğin Web API'den gelen bir REST servisine LINQ sorgusu göndermek istiyorsanız, kendi IQueryProvider'ınızı yazmanız gerekir.

  • Temel Mantık: IQueryable<T> arayüzünü implemente eden bir sınıf oluşturursunuz. Bu sınıfın Provider özelliği, sizin özel IQueryProvider'ınızı döndürür.

  • Execute Metodu: Execute metodu, Expression Tree'yi ziyaret eder (Visitor pattern ile) ve bu ağacı hedef dile (örneğin JSON query string veya OData URI) çevirir.

  • Örnek Senaryo: Bir MongoDB veya Elasticsearch veri kaynağına LINQ yazmak istiyorsanız, custom provider yazmak kaçınılmazdır. (Ancak unutmayın: Bu, yüzlerce satır kod ve derin bir mimari bilgi gerektirir. Çoğu durumda System.Linq.Dynamic.Core yeterlidir.)


6. Performans ve Expression Cache

Her dinamik sorgu oluşturduğunuzda, EF Core sorguyu yeniden derler (Compile). Saniyede binlerce farklı filtre kombinasyonu geliyorsa bu performansı vurur. Çözüm:

  • CompiledQuery ile birleştirin: Sadece değişen değerler (value) için CompiledQuery kullanabilirsiniz, ama property adı değişiyorsa compiled query çalışmaz.

  • Cache: Manuel oluşturduğunuz Expression<Func<T, bool>>'leri bir ConcurrentDictionary'de cache'leyin (key olarak propertyName + comparison + value type kullanarak). Bu sayede aynı filtre türü tekrar derlenmez.

Sonuç:

Expression Trees, C# dilinin en derin ve en zorlu konularından biridir ancak verdiği güç muazzamdır.

  • Başlangıç seviyesi: System.Linq.Dynamic.Core kullanarak işinizi görün.

  • Orta seviye: Manuel Expression.Equal ve Expression.OrElse ile basit filtre kombinasyonları yazın.

  • Uzman seviyesi: Expression Visitor yazarak sorguyu ziyaret edin ve custom bir tercüman (translator) oluşturun.

Unutmayın, Expression Tree'lerle oynarken reflection kullanmak zorunda kalırsınız, bu da performansı görece düşürür. Bu yüzden mümkünse yukarıdaki Dynamic LINQ kütüphanesi gibi optimize edilmiş araçları tercih edin.

Tüm yazılar