EF Core (Entity Framework Core) bir LINQ sorgusunu mümkün olduğunca veritabanında çalıştırmaya çalışır ve SQL'e çeviremediği bir ifadeyi yalnızca sorgunun en son Select adımında (top-level projection) istemci tarafında, yani uygulamanızın içinde değerlendirir. Çevrilemeyen ifade Where, OrderBy gibi başka bir yerdeyse EF Core çalışma zamanında bir istisna fırlatır; "client evaluation hatası" diye aranan durum budur. Çözüm, ifadeyi çevrilebilir bir biçimde yeniden yazmak, sağlayıcının EF.Functions metotlarını ya da bir veritabanı fonksiyonu eşlemesini kullanmak veya istemci değerlendirmesini AsEnumerable gibi bir çağrıyla bilinçli olarak başlatmaktır.

Bu yazıda EF Core'un hangi kısmı nerede değerlendirdiğini, istemci değerlendirmesinin neden sorgunun ortasında engellendiğini, her çözüm yolunun ne zaman uygun olduğunu ve belgelerin uyardığı bellek sızıntısı durumunu anlatıyoruz. Karmaşık operatörlerin çevirisi Join ve GroupBy, temel sorgu yapısı EF Core LINQ sorguları yazısında ele alınıyor.

Sorgu nerede çalışır: sunucu ve istemci

Sorgu nerede çalışır: sunucu ve istemci

Belgelerdeki sorgu yaşam döngüsü üç adımda özetlenebilir. Önce EF Core LINQ sorgusunu işleyip veritabanı sağlayıcısının kullanabileceği bir gösterime dönüştürür; bu sonuç önbelleğe alınır, böylece aynı işlem her çalıştırmada tekrarlanmaz. Ardından sağlayıcı sorgunun hangi kısımlarının veritabanında değerlendirilebileceğini belirler ve bu kısımları veritabanına özgü dile, ilişkisel veritabanlarında SQL'e çevirir. Son olarak sorgu veritabanına gönderilir ve gelen değerlerden entity örnekleri oluşturulur.

Bu süreçte EF Core sorgunun bazı parçalarını parametreye dönüştürür; örneğin sorguda kullandığınız yerel bir değişkenin değeri istemcide hesaplanır ve SQL'e parametre olarak gider. Geri kalan kısım bu parametrelerle birlikte sağlayıcıya verilir. Sağlayıcının bir ifadeyi çevirebilmesi için o ifadenin anlamını bilmesi gerekir: string.ToLower gibi yerleşik bir metodun karşılığını bilir, ama sizin yazdığınız bir C# metodunun içinde ne olduğunu bilemez.

Sorgunun ne zaman veritabanına gittiği de bu konuda önemlidir. LINQ operatörlerini çağırmak yalnızca sorgunun bellekteki gösterimini oluşturur; sorgu, sonuçlar tüketildiğinde gönderilir. En yaygın tetikleyiciler sonuçları bir döngüde dolaşmak ve ToList, ToArray, Single, Count ya da bunların async karşılıklarıdır. Client evaluation hatası da derleme sırasında değil, sorgu bu noktada çevrilmeye çalışıldığında ortaya çıkar.

İzin verilen durum: son Select içinde istemci değerlendirmesi

EF Core, sorgunun en üst düzey projeksiyonunda kısmi istemci değerlendirmesini destekler. Belgelerdeki örnekte blog URL'lerini standart hale getiren bir yardımcı metot kullanılır. SQL Server sağlayıcısı bu metodun nasıl yazıldığını bilmediği için onu SQL'e çeviremez; ancak metot yalnızca son Select içinde olduğu için sorgunun geri kalanı veritabanında çalışır, URL'lerin bu metottan geçirilmesi istemcide yapılır.

csharp
var blogs = await context.Blogs
    .OrderByDescending(blog => blog.Rating)
    .Select(
        blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog.Url) })
    .ToListAsync();

public static string StandardizeUrl(string url)
{
    url = url.ToLower();

    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }

    return url;
}

Burada sıralama veritabanında yapılır, sunucudan yalnızca gereken kolonlar gelir ve her satır için StandardizeUrl uygulamanızda çalışır. Bu güvenlidir, çünkü istemcide yapılan iş veritabanından gelecek satır sayısını değiştirmez; satırlar zaten gelecekti, yalnızca biçimlendirme uygulamada yapılır.

Engellenen durum: Where içinde çevrilemeyen metot

Aynı yardımcı metot bir Where filtresinde kullanıldığında durum değişir. Filtre veritabanında uygulanamazsa onu istemcide uygulamak için bütün verinin belleğe çekilmesi gerekir. Filtreye ve sunucudaki veri miktarına bağlı olarak bu çok kötü performansa yol açabilir. Bu yüzden EF Core bu tür istemci değerlendirmesini engeller ve çalışma zamanında istisna fırlatır.

csharp
var blogs = await context.Blogs
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToListAsync();

Belgelere göre EF Core 3.0 öncesi sürümler istemci değerlendirmesine sorgunun her yerinde izin veriyordu ve bu sorgu çalışırdı; fark edilmeyen performans sorunlarına yol açtığı için yalnızca bir uyarı loglanıyordu. Günümüzdeki davranış bilinçli bir tasarım kararıdır: sessizce bütün tabloyu belleğe çekmek yerine sizi sorunu açıkça çözmeye zorlar. Sorunu çözmenin ilk adımı, sorgunun hangi parçasının çevrilemediğini bulmaktır; genellikle bu, sizin yazdığınız bir metot ya da sağlayıcının tanımadığı bir operatördür.

Çözüm 1: ifadeyi çevrilebilir biçimde yeniden yazmak

Çözüm 1: ifadeyi çevrilebilir biçimde yeniden yazmak

Çoğu durumda en iyi çözüm, aynı mantığı sağlayıcının tanıdığı yerleşik metotlarla yazmaktır. EF Core sağlayıcıları ilkel tipler üzerindeki pek çok yerleşik fonksiyon için hazır eşlemeler sunar. Örneğin SQL Server sağlayıcısının eşleme tablosuna göre ToLower() LOWER, Length LEN, StartsWith ve Contains ise LIKE ifadelerine çevrilir. Belgeler, çevirilerin genellikle C# tarafındaki sonuçla aynı sonucu verecek şekilde tasarlandığını, ancak bazı durumlarda C# anlamını birebir taşımak yerine en uygun çevirinin seçildiğini de not eder.

Yukarıdaki engellenen sorguya geri dönelim. StandardizeUrl metodu URL'yi küçük harfe çevirir ve başında yoksa http:// ekler. Eklenen önek "dotnet" kelimesini içermediği için, "standart URL dotnet içeriyor mu" sorusu "küçük harfe çevrilmiş URL dotnet içeriyor mu" sorusuyla aynı sonucu verir. Bu yüzden filtreyi çevrilebilir yerleşik metotlarla yeniden yazabilirsiniz:

csharp
var blogs = await context.Blogs
    .Where(blog => blog.Url.ToLower().Contains("dotnet"))
    .ToListAsync();

Bu sorgu artık bütünüyle veritabanında çalışır ve uygulamaya yalnızca eşleşen satırlar gelir. Bu tür bir yeniden yazımda mantığın gerçekten eşdeğer olduğunu kendiniz doğrulamanız gerekir; her yardımcı metot bu kadar kolay dönüştürülemez. Büyük ve küçük harf duyarlılığının veritabanının collation ayarına bağlı olduğunu da unutmayın; bu konu EF Core LINQ sorguları yazısında ele alınıyor.

Bazı C# metotlarının veritabanında birebir fonksiyon karşılığı olmadığını da bilmek gerekir. Belgelerdeki örneklerde string.IsNullOrEmpty bir fonksiyona değil, bir null kontrolüne ve boş metinle karşılaştırmaya çevrilir. StringComparison parametreli string.Equals aşırı yüklemesinin ise veritabanı karşılığı yoktur, çünkü bu karşılaştırma biçimi veritabanında kolayca temsil edilemez. Tersi de mümkündür: C#'taki ?? operatörünün bir metodu yoktur ama veritabanında COALESCE fonksiyonuna çevrilir.

Çözüm 2: EF.Functions ile sağlayıcıya özgü fonksiyonlar

Her veritabanı fonksiyonunun bir C# karşılığı yoktur. Bu boşluğu doldurmak için sağlayıcılar, LINQ sorgularında kullanılmak üzere EF.Functions üzerinde uzantı metotları tanımlar. SQL Server ve SQLite sağlayıcılarının eşleme tablolarında örneğin EF.Functions.Like (LIKE) ve EF.Functions.Collate (COLLATE) yer alır; SQL Server tablosunda ayrıca EF.Functions.DateDiffDay gibi tarih fonksiyonları, SQLite tablosunda EF.Functions.Glob bulunur.

csharp
var blogs = await context.Blogs
    .Where(b => EF.Functions.Like(b.Url, "%dotnet%"))
    .ToListAsync();

Bu metotlar belirli veritabanı fonksiyonlarına sıkı sıkıya bağlı oldukları için sağlayıcıya özgüdür; bir sağlayıcıda çalışan metot büyük olasılıkla başka bir sağlayıcıda çalışmaz. Belgelerin vurguladığı bir başka nokta da şudur: bu metotların amacı çevrilen sorguda bir veritabanı fonksiyonunu çağırmak olduğu için, onları istemcide değerlendirmeye çalışmak bir istisnayla sonuçlanır. Yani EF.Functions çağrısını AsEnumerable sonrasındaki bir Where içine taşıyamazsınız. Kullandığınız sağlayıcının tam listesi için fonksiyon eşleme sayfasına bakın; sağlayıcılar arasındaki farklar veritabanı sağlayıcıları yazısında özetleniyor.

Çözüm 3: kendi veritabanı fonksiyonunuzu eşlemek

Mantık zaten veritabanında bir kullanıcı tanımlı fonksiyon (UDF) olarak varsa ya da onu oraya taşımak mantıklıysa, bir CLR metodunu bu fonksiyona eşleyebilirsiniz. EF Core sorguyu SQL'e çevirirken eşlenen CLR metodu yerine veritabanı fonksiyonunu çağırır. Belgelerdeki örnekte, bir blogdaki en az bir yorum almış yazıları sayan CommentedPostCountForBlog adlı bir SQL fonksiyonu bağlam üzerindeki bir metoda eşlenir:

csharp
public int ActivePostCountForBlog(int blogId)
    => throw new NotSupportedException();

// OnModelCreating icinde:
modelBuilder.HasDbFunction(() => ActivePostCountForBlog(default))
    .HasName("CommentedPostCountForBlog")
    .HasSchema("dbo");

Metodun gövdesi önemli değildir; argümanları çevrilebildiği sürece istemcide hiç çağrılmaz, EF Core yalnızca imzayla ilgilenir. HasDbFunction çağrısındaki default değerleri yalnızca metodu tanımlamak içindir, veritabanına gönderilmez. Varsayılan olarak EF Core, CLR metodunu varsayılan şemadaki aynı adlı fonksiyona eşler; ad veya şema farklıysa HasName ve HasSchema kullanılır. Bu eşlemeden sonra context.ActivePostCountForBlog(b.BlogId) > 1 koşulu belgelerdeki örnek çıktıda WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1 olarak görünür.

Aynı sayfa birkaç seçenek daha anlatır. Bir metodu SQL Server'ın ISDATE gibi yerleşik bir fonksiyonuna eşlemek için IsBuiltIn kullanılır; bu önemlidir, çünkü SQL Server kullanıcı tanımlı fonksiyonların şema adıyla çağrılmasını ister, yerleşik fonksiyonlar ise şemasız çağrılır. Bağlam üzerinde tanımlı statik metotlar DbFunctionAttribute ile de eşlenebilir ve bu metotlar otomatik keşfedilir. IQueryable döndüren ve gövdesinde FromExpression çağıran bir metot ise tablo değerli bir fonksiyona (TVF) eşlenir ve sorgularda bir DbSet gibi kullanılabilir. HasTranslation ile bir metodu doğrudan bir SQL ifade ağacına çevirmek de mümkündür; ancak belgeler bunun düşük seviyeli bir API olduğunu, yanlış meta verinin geçersiz SQL veya yanlış sonuç üretebileceğini ve mümkünse normal fonksiyon eşlemesinin tercih edilmesi gerektiğini vurgular.

Fonksiyonun kendisini veritabanında oluşturmak migration'ın işidir; boş bir migration'a ham SQL ekleme yöntemi migration özelleştirme ve ham SQL yazısında anlatılıyor. Sorgunun tamamını SQL ile yazmanın daha uygun olduğu durumlar için FromSql ile ham SQL sorguları yazısına bakabilirsiniz.

Çözüm 4: istemci değerlendirmesini bilerek başlatmak

Bazı durumlarda istemci değerlendirmesine bilinçli olarak geçmek isteyebilirsiniz. Belgeler iki tipik durum sayar: veri miktarı küçüktür ve istemcide değerlendirme büyük bir performans cezası getirmez, ya da kullandığınız LINQ operatörünün sunucu tarafında çevirisi yoktur. Bu durumda sorgunun çevrilebilir kısmından sonra AsEnumerable veya ToList (async için AsAsyncEnumerable veya ToListAsync) çağırarak geri kalanını istemcide çalıştırırsınız:

csharp
var blogs = context.Blogs
    .AsAsyncEnumerable()
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToListAsync();

Bu iki yöntem arasında bir fark vardır. AsEnumerable sonuçları akış halinde (streaming) işler, yani satırlar teker teker gelir ve bütün sonuç kümesi aynı anda bellekte tutulmaz. ToList ise bir liste oluşturarak sonuçları tamponlar (buffering) ve ek bellek kullanır. Öte yandan sonuçları birden fazla kez dolaşacaksanız onları bir listede tutmak, veritabanına tek bir sorgu gitmesini sağladığı için daha avantajlıdır. Hangisinin uygun olduğu kullanım biçiminize bağlıdır.

Burada en önemli nokta istemci değerlendirmesinin yerini doğru seçmektir. Yukarıdaki örnekte filtrenin tamamı istemcide çalıştığı için bütün bloglar veritabanından gelir; küçük bir tablo için kabul edilebilir, büyük bir tablo için değildir. Mümkünse önce çevrilebilen filtreleri AsAsyncEnumerable öncesine koyun ki sunucudan gelen satır sayısı azalsın, yalnızca çevrilemeyen kısmı sonraya bırakın. AsAsyncEnumerable sonrasında daha fazla operatör kullanmak isterseniz belgeler, async enumerable'lar için operatörler tanımlayan System.Interactive.Async kütüphanesini işaret eder; async kullanımının genel kuralları EF Core async yazısında anlatılıyor.

Seçenekleri karşılaştırma

Seçenekleri karşılaştırma: Yerleşik metotlarla yeniden yazmak, EF.Functions, HasDbFunction veya DbFunction, FromSql ile ham SQL
YöntemNerede çalışırNe zaman uygun
Yerleşik metotlarla yeniden yazmakVeritabanıMantık eşdeğer biçimde ifade edilebiliyorsa
EF.FunctionsVeritabanıSağlayıcının özel bir fonksiyonu gerekiyorsa
HasDbFunction veya DbFunctionVeritabanıMantık bir UDF olarak veritabanında yaşıyorsa
FromSql ile ham SQLVeritabanıLINQ ile ifade edilemeyen bir sorgu için
AsEnumerable veya ToList sonrasıİstemciVeri küçükse veya operatörün çevirisi yoksa
Son Select içinde istemci metoduİstemci (yalnızca projeksiyon)Biçimlendirme gibi satır sayısını etkilemeyen işler

İstemci projeksiyonunda olası bellek sızıntısı

Son Select içinde istemci değerlendirmesi serbest olsa da belgeler burada ince bir tuzağa dikkat çeker. Sorgu çevirisi ve derlemesi pahalı olduğu için EF Core derlenmiş sorgu planını önbelleğe alır. Önbellekteki temsilci (delegate), projeksiyondaki istemci kodunu kullanabilir. EF Core istemcide değerlendirilen parçalar için parametreler üretir ve aynı planı yalnızca parametre değerlerini değiştirerek yeniden kullanır. Ancak ifade ağacındaki bazı sabitler parametreye dönüştürülemez. Önbellekteki temsilci böyle sabitler içeriyorsa bu nesneler hâlâ referans aldığı için çöp toplayıcı tarafından temizlenemez. Böyle bir nesne bir DbContext ya da başka servisler içeriyorsa uygulamanın bellek kullanımı zamanla büyüyebilir.

Bu yüzden EF Core, geçerli sağlayıcıyla eşlenemeyen türden sabitlerle karşılaştığında istisna fırlatır. Belgeler yaygın nedenleri ve çözümlerini şöyle sıralar:

  • Örnek (instance) metodu kullanmak: İstemci projeksiyonunda bir örnek metodu çağırdığınızda ifade ağacı o örneği sabit olarak içerir. Metot örnekten veri kullanmıyorsa onu static yapın; kullanıyorsa gereken veriyi metoda argüman olarak verin.
  • Metoda sabit argüman geçmek: Bu genellikle istemci metoduna argüman olarak this verildiğinde olur. Argümanı sağlayıcının eşleyebileceği birden fazla skaler argümana bölün.
  • Diğer sabitler: Başka bir durumda sabitin gerçekten gerekli olup olmadığını değerlendirin. Gerekliyse değeri yerel bir değişkende saklayıp sorguda o değişkeni kullanın; EF Core yerel değişkeni parametreye dönüştürür.

Yukarıdaki StandardizeUrl örneğinin static olarak tanımlanması bu açıdan tesadüf değildir: metot örnek verisi kullanmadığı için statik olması hem bu sorunu önler hem de niyeti açık eder.

EF Core 3.0 öncesinden gelen kodlar

Eski bir projeyi taşıyorsanız EF Core 3.0 öncesinde yazılmış ve istemci değerlendirmesine dayanan sorgularla karşılaşabilirsiniz. O sürümlerde istemci değerlendirmesi sorgunun her yerinde mümkündü ve bir uyarı loglanırdı. İsteğe bağlı olarak bu uyarıyı istisnaya çevirmek mümkündü; belgelerde bunun için ConfigureWarnings ile RelationalEventId.QueryClientEvaluationWarning olayının Throw edildiği bir yapılandırma örneği yer alır. Güncel sürümlerde bu davranış zaten varsayılan olduğu için böyle bir yapılandırmaya gerek yoktur; taşıma sırasında bu sorguları yukarıdaki çözümlerden biriyle yeniden yazmanız gerekir.

Sık yapılan hatalar

  • Hatayı ToList ile geçiştirmek: Sorgunun başına ToList ekleyip filtreyi bellekte çalıştırmak hatayı yok eder ama bütün tabloyu uygulamaya taşır. Önce sunucuda çalışabilecek filtreleri uygulayın.
  • Yeniden yazımın eşdeğerliğini kontrol etmemek: Bir yardımcı metodu yerleşik metotlarla yeniden yazarken sonucun gerçekten aynı olduğunu doğrulayın; büyük ve küçük harf karşılaştırması gibi ayrıntılar veritabanı ayarına bağlıdır.
  • EF.Functions'ı istemcide çağırmak: Bu metotlar yalnızca çevrilen sorgu içinde anlamlıdır; istemcide değerlendirmeye çalışmak istisna fırlatır.
  • Sağlayıcı değiştirince şaşırmak: Çeviri desteği sağlayıcıya göre değişir. SQLite'ta geliştirip SQL Server'da çalıştırdığınız (ya da tersi) bir sorgu farklı sonuç verebilir.
  • Projeksiyonda örnek metodu kullanmak: İfade ağacına örneğin kendisi sabit olarak girer; bu da bellek sızıntısı riski ve istisna demektir. Statik metot kullanın veya gerekli değeri argüman olarak verin.

Sık Sorulan Sorular

EF Core neden bazı sorgularda hata veriyor da bazılarında vermiyor?

Çevrilemeyen ifade son Select içindeyse EF Core onu istemcide değerlendirir ve hata vermez. Aynı ifade Where, OrderBy gibi başka bir yerdeyse bunu yapmak bütün veriyi belleğe çekmeyi gerektireceğinden EF Core istisna fırlatır.

AsEnumerable mı ToList mi kullanmalıyım?

AsEnumerable sonuçları akış halinde işler ve bütün kümeyi bellekte tutmaz; ToList sonuçları bir listeye tamponlar. Sonuçları bir kez dolaşacaksanız akış, birden fazla kez dolaşacaksanız liste daha uygundur.

Kendi C# metodumu SQL'e çevirtmenin yolu var mı?

Metodun C# gövdesi çevrilmez. Ancak aynı mantığı veritabanında bir fonksiyon olarak tanımlayıp HasDbFunction veya DbFunctionAttribute ile bir CLR metoduna eşleyebilirsiniz; ileri düzey durumlarda HasTranslation ile bir SQL ifade ağacı da üretebilirsiniz.

Üretilen SQL'i nasıl görebilirim?

LogTo ile basit loglamayı açabilir ya da uygulamanızın Microsoft.Extensions.Logging yapılandırmasını kullanabilirsiniz. Ayrıntılar EF Core SQL loglama yazısında.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: EF Core öğrenme rehberi · EF Core LINQ sorguları · Join ve GroupBy · FromSql ve ham SQL · SQL loglama