EF Core'da LINQ'ten SQL'e Çeviri Kataloğu: String, DateTime, Math, Collection, EF.Functions ve HasDbFunction
EF Core, SQL Server'da string method'larını LIKE, UPPER, SUBSTRING ve STRING_AGG'e, tarihleri DATEADD/DATEPART/DATEDIFF'e, collection'ları IN ve OPENJSON'a çevirir; karşılığı olmayan SQL özellikleri EF.Functions, kendi function'ların HasDbFunction ile LINQ'e bağlanır. Kolonu function'a sokan çeviriler (ToLower, DATEDIFF) index'i kullanılamaz hâle getirir.
Sürüm notu: Bu yazıdaki kodlar EF Core (Entity Framework Core) 10 ve SQL Server 2025 ile yazılıp denenmiştir; EF Core 11 notları EF Core 11.0.0 ile denenmiştir.
Önce Tahmin Et
Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) arama kutusuna "dune" yazan kullanıcıya "Dune"u bulmak istiyorsun. Büyük-küçük harf sorun olmasın diye şunu yazıyorsun:
var term = "dune";
var titles = db.Books
.Where(b => b.Title.ToLower().Contains(term.ToLower()))
.Select(b => b.Title)
.ToList();
Books.Title üzerinde bir index var. Bu sorgu SQL Server'a nasıl gider, index işe yarar mı? İkinci soru: ToLower'a hiç gerek var mıydı? İlk sorunun cevabı aşağıdaki çeviri kataloğunda, ikincisi sargability bölümünde.
09. yazıda LINQ'in (Language Integrated Query; C# içinden sorgu yazma özelliği) SQL'e nasıl çevrildiğini SQLite üzerinde görmüştük. Bu yazıda SQL Server'da, gerçek bir Bookstore modeliyle hangi C# ifadesinin hangi SQL'e döndüğünün kataloğunu çıkarıyoruz. Örnekler samples/Bookstore.QueryLab console uygulamasında; her sorgu önce ToQueryString() ile SQL'ini, sonra sonucunu yazıyor. Veritabanını kurmak ve SQL function'larını oluşturmak için:
dotnet run --project samples/Bookstore.QueryLab -- setup
dotnet run --project samples/Bookstore.QueryLab -- strings
String Function'ları ve LIKE
Açılıştaki sorgunun ve yakın akrabalarının SQL'i:
DECLARE @term_contains nvarchar(200) = N'%dune%';
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE @term_contains ESCAPE N'\'
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'The%'
DECLARE @ToLower_contains nvarchar(200) = N'%dune%';
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE LOWER([b].[Title]) LIKE @ToLower_contains ESCAPE N'\'
İlki Title.Contains(term), ikincisi Title.StartsWith("The"), üçüncüsü açılıştaki sorgu. Değişkenden gelen değer parametre oldu ve EF Core, % ile _ karakterlerini kaçırmak için ESCAPE N'\' ekledi. Kullanıcı arama kutusuna 50% yazarsa parametre N'%50\%%' olarak gidiyor; kullanıcının yazdığı % kaçırıldığı için joker değil, düz metin olarak aranıyor. Sabit yazdığın "The" ise SQL'e doğrudan gömüldü. term.ToLower() bellekte çalıştı (parametre değeri zaten küçük harf), ama Title.ToLower() kolona LOWER olarak uygulandı. Bunun bedelini birazdan ölçeceğiz.
Diğer string method'ları da bire bir SQL karşılığı buluyor:
db.Books.OrderBy(b => b.Id).Take(2).Select(b => new
{
b.Title,
Upper = b.Title.ToUpper(),
First3 = b.Title.Substring(0, 3),
Space = b.Title.IndexOf(" "),
b.Title.Length,
Trimmed = b.Title.Trim()
});
SELECT TOP(@p) [b].[Title], UPPER([b].[Title]) AS [Upper], SUBSTRING([b].[Title], 0 + 1, 3) AS [First3], CHARINDEX(N' ', [b].[Title]) - 1 AS [Space], CAST(LEN([b].[Title]) AS int) AS [Length], LTRIM(RTRIM([b].[Title])) AS [Trimmed]
FROM [Books] AS [b]
ORDER BY [b].[Id]
{ Title = Dune, Upper = DUNE, First3 = Dun, Space = -1, Length = 4, Trimmed = Dune }
C# index'leri 0'dan, SQL 1'den başlıyor; EF Core bu farkı 0 + 1 ve - 1 ile kapatıyor. Boşluk olmayan "Dune"da IndexOf C#'taki gibi -1 döndü.
Gruplanmış veride string.Join bir aggregate'e çevriliyor:
db.Books
.GroupBy(b => b.Author.Name)
.OrderBy(g => g.Key)
.Select(g => new { Author = g.Key, Titles = string.Join(", ", g.OrderBy(b => b.Title).Select(b => b.Title)) });
SELECT [a].[Name] AS [Author], COALESCE(STRING_AGG([b].[Title], N', ') WITHIN GROUP (ORDER BY [b].[Title]), N'') AS [Titles]
FROM [Books] AS [b]
INNER JOIN [Authors] AS [a] ON [b].[AuthorId] = [a].[Id]
GROUP BY [a].[Name]
ORDER BY [a].[Name]
{ Author = Jane Austen, Titles = Emma, Pride and Prejudice }
Microsoft'un function mapping tablosu sıralı STRING_AGG ... WITHIN GROUP çevirisini EF Core 11'e yazıyor, ama ben bu SQL'i EF Core 10.0.15'te aldım. Aynı sonucu GroupBy yerine navigation üzerinden (a.Books.OrderBy(...).Select(...)) istediğimde ise EF Core STRING_AGG üretmedi; LEFT JOIN ile bütün satırları çekip birleştirmeyi bellekte yaptı. Aynı niyet, iki farklı SQL: ToQueryString()'e bakmadan hangisinin olduğunu bilemezsin.
Collation ve Türkçe İ
Veritabanımın collation'ı SQL_Latin1_General_CP1_CI_AS, yani büyük-küçük harfe duyarsız. Katalogda "İnce Memed" var; kullanıcı "ince" yazıyor:
db.Books.Where(b => b.Title.StartsWith("ince"));
db.Books.Where(b => EF.Functions.Collate(b.Title, "Turkish_CI_AS").StartsWith("ince"));
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'ince%'
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Title] COLLATE Turkish_CI_AS LIKE N'ince%'
İnce Memed
İlk sorgu boş döndü. Latin collation'ında "İ" harfi "i"nin büyüğü değil; Türkçe collation'da öyle. SQL Temelleri serisinin "SQL Veri Tipleri ve CREATE TABLE" yazısında bu tuzağın SQL tarafını görmüştük. EF.Functions.Collate sorgu bazında collation verir, ama kolona COLLATE uygulamak da index'in sırasını bozar. Türkçe arama ana senaryoysa kolonu baştan Türkçe collation ile tanımlamak daha doğru; bunu bir sonraki yazıda UseCollation ile yapacağız.
Tarih ve Saat
var now = DateTime.UtcNow;
db.Orders.OrderBy(o => o.Id).Take(2).Select(o => new
{
o.Id,
Day = o.CreatedAt.Date,
o.CreatedAt.Year,
o.CreatedAt.Month,
DueDate = o.CreatedAt.AddDays(14),
Age = EF.Functions.DateDiffDay(o.CreatedAt, now)
});
SELECT TOP(@p) [o].[Id], CONVERT(date, [o].[CreatedAt]) AS [Day], DATEPART(year, [o].[CreatedAt]) AS [Year], DATEPART(month, [o].[CreatedAt]) AS [Month], DATEADD(day, CAST(14.0E0 AS int), [o].[CreatedAt]) AS [DueDate], DATEDIFF(day, [o].[CreatedAt], @now) AS [Age]
FROM [Orders] AS [o]
ORDER BY [o].[Id]
{ Id = 1, Day = 11/12/2026 12:00:00 AM, Year = 2026, Month = 11, DueDate = 11/26/2026 9:57:20 PM, Age = 2 }
.Date CONVERT(date, ...), Year ve Month DATEPART, AddDays DATEADD oldu. AddDays bir double aldığı için 14 önce 14.0E0'a, sonra int'e cast edildi; zararsız ama SQL'i okurken şaşırtabilir. EF.Functions.DateDiffDay doğrudan DATEDIFF(day, ...); C#'ta karşılığı olmayan, yalnız sorgu içinde çalışan bir method. DateOnly da aynı şekilde çevriliyor: b.PublishedOn.Year < 1900 bir DATEPART, AddYears(100) bir DATEADD oldu.
Dikkat edilecek bir ayrım var. Değeri now değişkenine alıp kullandığında @now parametresi oldu ve saat uygulama sunucusundan geldi. Lambda'nın içinde doğrudan DateTime.UtcNow yazarsan EF Core onu GETUTCDATE()'e çevirir ve saat veritabanı sunucusundan gelir. İki sunucunun saati arasındaki fark, "son 30 gün" gibi sınırlarda farklı sonuçlar demek.
"Son 30 günde en çok satanlar" sorgusu bunların hepsini bir araya getiriyor:
var since = DateTime.UtcNow.AddDays(-30);
var top = db.OrderItems
.Where(i => i.Order.CreatedAt >= since)
.GroupBy(i => i.Book.Title)
.Select(g => new { Title = g.Key, Units = g.Sum(i => i.Quantity) })
.OrderByDescending(x => x.Units)
.Take(3);
SELECT TOP(@p) [b].[Title], COALESCE(SUM([o].[Quantity]), 0) AS [Units]
FROM [OrderItems] AS [o]
INNER JOIN [Orders] AS [o0] ON [o].[OrderId] = [o0].[Id]
INNER JOIN [Books] AS [b] ON [o].[BookId] = [b].[Id]
WHERE [o0].[CreatedAt] >= @since
GROUP BY [b].[Title]
ORDER BY COALESCE(SUM([o].[Quantity]), 0) DESC
{ Title = Dune, Units = 3 }
{ Title = The Hobbit, Units = 3 }
{ Title = İnce Memed, Units = 2 }
Aynı filtreyi EF.Functions.DateDiffDay(i.Order.CreatedAt, DateTime.UtcNow) <= 30 diye de yazabilirdin; sonuç aynı geldi, SQL'de WHERE DATEDIFF(day, [o0].[CreatedAt], GETUTCDATE()) <= 30 oldu. İlki kolonu çıplak bırakıyor, ikincisi kolonu bir function'ın içine alıyor. Aradaki farkı sargability bölümünde ölçeceğiz.
EF Core 11 notu: EF Core 11 EF.Functions.DateTrunc'ı getiriyor, SQL Server 2022'deki DATETRUNC'a çevriliyor. Siparişleri haftalara bölmek için:
db.Orders
.GroupBy(o => EF.Functions.DateTrunc("week", o.CreatedAt))
.Select(g => new { Week = g.Key, Orders = g.Count() })
.OrderBy(x => x.Week);
SELECT [o0].[Key] AS [Week], COUNT(*) AS [Orders]
FROM (
SELECT DATETRUNC(week, [o].[CreatedAt]) AS [Key]
FROM [Orders] AS [o]
) AS [o0]
GROUP BY [o0].[Key]
ORDER BY [o0].[Key]
Aynı sürümde MaxBy/MinBy da çevriliyor: await db.Books.MaxByAsync(b => b.Price) en pahalı kitabı (Dune, 18.50) tek sorguda getirdi.
Math, Guid ve Collection'lar
db.Books.Where(b => b.Price > 12).Select(b => new
{
b.Title,
Discounted = Math.Round(b.Price * 0.85m, 2),
Floor = Math.Floor(b.Price),
Diff = Math.Abs(b.Price - 12.50m)
});
db.Books.OrderBy(b => Guid.NewGuid()).Select(b => b.Title).Take(1);
SELECT [b].[Title], ROUND([b].[Price] * 0.85, 2) AS [Discounted], FLOOR([b].[Price]) AS [Floor], ABS([b].[Price] - 12.5) AS [Diff]
FROM [Books] AS [b]
WHERE [b].[Price] > 12.0
{ Title = Dune, Discounted = 15.7300, Floor = 18, Diff = 6.00 }
SELECT TOP(@p) [b].[Title]
FROM [Books] AS [b]
ORDER BY NEWID()
Math.Round ROUND, Guid.NewGuid() NEWID() oldu; "rastgele bir kitap öner" böyle yazılır. 15.7300'daki fazla sıfırlar SQL Server'ın decimal çarpımında ölçeği büyütmesinden geliyor; değer doğru, gösterirken biçimlendir.
Collection tarafında iki farklı durum var. Bellekteki bir array'in Contains'i, EF Core 10 yazısında gördüğümüz gibi ayrı parametrelere açılan bir IN oluyor. Kolonun kendisi bir collection ise, yani 26. yazıdaki primitive collection'larda, EF Core JSON kolonunu OPENJSON ile tabloya açıyor. Book.Tags bir List<string>:
int[] ids = [1, 3, 5];
db.Books.Where(b => ids.Contains(b.Id));
db.Books.Where(b => b.Tags.Contains("classic"));
DECLARE @ids1 int = 1;
DECLARE @ids2 int = 3;
DECLARE @ids3 int = 5;
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Id] IN (@ids1, @ids2, @ids3)
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE N'classic' IN (
SELECT [t].[value]
FROM OPENJSON([b].[Tags]) WITH ([value] nvarchar(max) '$') AS [t]
)
EF Core 11 notu: UseCompatibilityLevel(170) ile SQL Server 2025'i hedeflediğinde EF Core 11 aynı Contains'i WHERE JSON_CONTAINS([b].[Tags], N'classic') = 1'e çeviriyor. Bunu EF Core 10'un oluşturduğu tabloya karşı denediğimde SQL Server reddetti: "Argument data type nvarchar(max) is invalid for argument 1 of json_contains function." JSON_CONTAINS kolonun yeni json tipinde olmasını istiyor; kolonu json'a çevirince aynı sorgu beş klasiği döndürdü. Yani compatibility level'ı yükseltmek tek başına yetmiyor, şemanın da (migration ile) güncellenmesi gerekiyor.
EF.Functions Kataloğu
EF.Functions, C#'ta karşılığı olmayan SQL özelliklerinin kapısı. Bu method'lar yalnız sorgunun içinde anlam taşır; bellekte çağırırsan exception alırsın. SQL Server provider'ında en çok kullanılanlar:
db.Books.Where(b => EF.Functions.Like(b.Title, "%[Mm]arch"));
db.Books.Where(b => EF.Functions.Like(b.Isbn, "978!_%", "!"));
db.Books.Select(b => new { b.Title, Bytes = EF.Functions.DataLength(b.Title) });
db.Books.Where(b => EF.Functions.FreeText(b.Title, "hobbit"));
WHERE [b].[Title] LIKE N'%[Mm]arch'
Middlemarch
WHERE [b].[Isbn] LIKE N'978!_%' ESCAPE N'!'
SELECT TOP(@p) [b].[Title], DATALENGTH([b].[Title]) AS [Bytes]
{ Title = Dune, Bytes = 8 }
WHERE FREETEXT([b].[Title], N'hobbit')
Like, Contains'in yapamadığını yapar: [Mm] gibi SQL Server'a özgü joker karakterleri olduğu gibi gönderir, istersen kendi escape karakterini verirsin. DataLength karakter değil byte sayar; "Dune" nvarchar olduğu için 8 byte. IsDate ve IsNumeric da var; eski şemalarda metin kolonuna yazılmış tarihleri ayıklarken işe yarıyor. DateDiffDay ailesi (DateDiffHour, DateDiffMonth…) ve DateFromParts da buraya ait.
FreeText ve Contains full-text search içindir ve tabloda full-text index ister. Docker'daki SQL Server imajında full-text bileşeni kurulu değil; SQL'i ToQueryString() ile aldım, çalıştırınca SQL Server şunu söyledi:
SqlException: Cannot use a CONTAINS or FREETEXT predicate on table or indexed view 'Books' because it is not full-text indexed.
Kendi SQL Server kurulumunda Full-Text Search bileşeni varsa önce catalog ve index'i oluşturman gerekir. EF Core 11 bunları migration'la da kurabiliyor (HasFullTextIndex), ama bu yazının kapsamı dışında.
HasDbFunction ile Kendi Function'ların
Bookstore'da indirim hesabı yıllardır SQL Server'daki bir function'da duruyor ve raporlar da onu kullanıyor. Aynı hesabı C#'ta tekrar yazmak yerine LINQ'ten o function'ı çağırmak istiyorsun. db/functions.sql'de iki function var; biri scalar, biri table-valued:
CREATE OR ALTER FUNCTION dbo.fn_DiscountedPrice (@price decimal(10,2), @percent int)
RETURNS decimal(10,2)
AS
BEGIN
RETURN ROUND(@price * (100 - @percent) / 100.0, 2);
END;
GO
CREATE OR ALTER FUNCTION dbo.fn_BestSellers (@since datetime2)
RETURNS TABLE
AS
RETURN
SELECT oi.BookId, SUM(oi.Quantity) AS Units
FROM dbo.OrderItems AS oi
JOIN dbo.Orders AS o ON o.Id = oi.OrderId
WHERE o.CreatedAt >= @since
GROUP BY oi.BookId;
C# tarafında scalar function için gövdesi olmayan bir static method, table-valued function için de context'te FromExpression kullanan bir method yazıp ikisini HasDbFunction ile eşliyorsun:
public static class BookstoreFunctions
{
// dbo.fn_DiscountedPrice'a eşlenir; yalnız LINQ sorgularında kullanılır.
public static decimal DiscountedPrice(decimal price, int percent)
=> throw new NotSupportedException("Only for use in LINQ queries.");
}
public class BookstoreContext(DbContextOptions<BookstoreContext> options) : DbContext(options)
{
public IQueryable<BestSeller> BestSellers(DateTime since)
=> FromExpression(() => BestSellers(since));
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDbFunction(typeof(BookstoreFunctions).GetMethod(nameof(BookstoreFunctions.DiscountedPrice))!)
.HasName("fn_DiscountedPrice")
.HasSchema("dbo");
modelBuilder.Entity<BestSeller>().HasNoKey().ToTable((string?)null);
modelBuilder.HasDbFunction(typeof(BookstoreContext).GetMethod(nameof(BestSellers), [typeof(DateTime)])!)
.HasName("fn_BestSellers");
}
}
ToTable((string?)null) satırını sonradan ekledim. İlk denemede EnsureCreated, function'ın satır tipi olan BestSeller için veritabanında boş bir tablo oluşturmuştu; bu satır o tipin bir tabloya eşlenmediğini söylüyor. Artık iki function da LINQ'in parçası:
db.Books
.Where(b => BookstoreFunctions.DiscountedPrice(b.Price, 20) < 10)
.Select(b => new { b.Title, b.Price, Sale = BookstoreFunctions.DiscountedPrice(b.Price, 20) });
db.BestSellers(DateTime.UtcNow.AddDays(-30))
.Join(db.Books, s => s.BookId, b => b.Id, (s, b) => new { b.Title, s.Units })
.OrderByDescending(x => x.Units);
SELECT [b].[Title], [b].[Price], [dbo].[fn_DiscountedPrice]([b].[Price], 20) AS [Sale]
FROM [Books] AS [b]
WHERE [dbo].[fn_DiscountedPrice]([b].[Price], 20) < 10.0
{ Title = Emma, Price = 9.90, Sale = 7.92 }
{ Title = Pride and Prejudice, Price = 8.75, Sale = 7.00 }
{ Title = İnce Memed, Price = 11.40, Sale = 9.12 }
SELECT [b].[Title], [f].[Units]
FROM [dbo].[fn_BestSellers](@since) AS [f]
INNER JOIN [Books] AS [b] ON [f].[BookId] = [b].[Id]
ORDER BY [f].[Units] DESC
Table-valued function bir IQueryable döndürdüğü için üstüne join, filtre, sıralama eklenebiliyor ve hepsi tek SQL'e gidiyor. 15. yazıdaki FromSqlRaw ile kıyasla: orada SQL'i string olarak yazıyorduk, burada function'ın imzası C#'ta tipli duruyor. API'deki /books/bestsellers endpoint'i de bu function'ı kullanıyor. Function'ların kendisini bu yazıda bir SQL dosyasıyla kurduk; 32. yazıda onları migration'la sürümleyeceğiz.
Çevrilemeyen Method ve Son Select
Türkçe büyük harf için ToUpper(new CultureInfo("tr-TR")) yazdığında EF Core'un SQL'de bir karşılığı yok:
var tr = new CultureInfo("tr-TR");
db.Books.Where(b => b.Title.ToUpper(tr) == "İNCE MEMED").ToList();
The LINQ expression 'DbSet<Book>()
.Where(b => b.Title.ToUpper(@tr) == "İNCE MEMED")' could not be translated. Additional information: Translation of method 'string.ToUpper' failed. If this method can be mapped to your custom function, see https://go.microsoft.com/fwlink/?linkid=2132413 for more information. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
Mesajı okurken iki yere bak. "Translation of method 'string.ToUpper' failed" hangi çağrının sorun olduğunu söylüyor; parametresiz ToUpper() çevriliyor, culture alan overload'ı çevrilmiyor. İkinci link de (2132413) tam olarak bu yazının HasDbFunction bölümüne gönderiyor: "bunu kendi function'ına eşleyebilirsin" diyor.
Aynı method son Select'te hata vermiyor, çünkü orada 09. yazıdaki client evaluation kuralı geçerli:
SELECT [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Price] > 12.0
DUNE
THE HOBBİT
MİDDLEMARCH
SQL'de yalnız Title var; büyük harfe çevirme bellekte, Türkçe kurallarla yapıldı ve "The Hobbit" THE HOBBİT oldu. Çeviri doğru; Türkçe kuralına göre "i"nin büyüğü "İ". Ama İngilizce bir kitap adı için istediğin bu muydu? Client evaluation sessizce çalıştığı için bu tür sonuçları ancak çıktıya bakınca görürsün.
Sargability: Index'i Kullanılamaz Hâle Getirmek
Şimdi açılıştaki sorunun ikinci yarısı. Ölçmek için Books tablosuna 50.000 satır ekledim ve aynı aramayı iki şekilde çalıştırdım; SET STATISTICS IO ON çıktısı:
-- Title.StartsWith(term): WHERE [b].[Title] LIKE @term_startswith ESCAPE N'\'
Table 'Books'. Scan count 1, logical reads 4, ...
-- Title.ToLower().StartsWith("dune"): WHERE LOWER([b].[Title]) LIKE N'dune%'
Table 'Books'. Scan count 1, logical reads 300, ...
Execution plan'da ilki Index Seek, ikincisi Index Scan. Kolon bir function'ın içine girince SQL Server index'teki sıralamayı kullanamıyor; her satır için LOWER'ı hesaplayıp karşılaştırmak zorunda kalıyor. Buna sargability (bir koşulun index'ten yararlanabilmesi) deniyor. SQL Temelleri serisinin "Scalar Function'lar ve CASE" yazısında SQL tarafından görmüştük; LINQ'te aynı tuzak daha kolay kuruluyor, çünkü ToLower() masum bir C# çağrısı gibi görünüyor.
Açılıştaki ikinci sorunun cevabı da burada: veritabanı collation'ı zaten case-insensitive (CI) olduğu için ToLower hiçbir işe yaramıyor, yalnız index'i devreden çıkarıyor. Contains ise baştaki % yüzünden zaten Index Scan; "içinde geçen" aramayı büyük tablolarda hızlandırmanın yolu index değil, full-text search.
Aynı kural tarihler için de geçerli. DATEDIFF(day, CreatedAt, GETUTCDATE()) <= 30 kolonu function'a sokuyor; CreatedAt >= @since ise kolonu çıplak bırakıyor ve CreatedAt üzerindeki bir index'ten yararlanabiliyor. Sınırı C#'ta hesapla, kolonu kendi hâlinde bırak.
Sık Düşülen Tuzaklar
Birincisi, case-insensitive arama için ToLower/ToUpper kullanmak. Önce kolonun collation'ına bak. CI ise gerek yok; CS (case-sensitive) ise ve bu arama sık yapılıyorsa çözüm, kolonu ya da sorguyu uygun collation ile tanımlamak, index'i de buna göre kurmak.
İkincisi, son Select'e pahalı bir method koymak. Client evaluation orada izinli olduğu için EF Core hata vermez; method'un ihtiyaç duyduğu kolonları çeker ve her satır için C#'ı çalıştırır. Method bir HTTP çağrısı, bir regex ya da bir veritabanı sorgusu yapıyorsa 10.000 satırlık bir listede 10.000 kez çalışır. Yukarıdaki ToUpper(tr) örneği ucuzdu, ama sonucu yine de beklenmedikti.
Üçüncüsü, STRING_AGG'in sınırı. Test için 50.000 kitabı aynı yazara bağladığımda yazar listesi sorgusu şu hatayla durdu:
Microsoft.Data.SqlClient.SqlException (0x80131904): STRING_AGG aggregation result exceeded the limit of 8000 bytes. Use LOB types to avoid result truncation.
Title nvarchar(200) olduğu için STRING_AGG sonucu nvarchar(4000), yani 8000 byte ile sınırlı. Küçük test verisinde hiç görünmeyen bir hata; grup büyüyebiliyorsa birleştirmeyi bellekte yap ya da listeyi Take ile sınırla.
Dördüncüsü, saat kaynağını karıştırmak. Lambda içinde DateTime.UtcNow veritabanının saati, dışarıda hesaplanan değişken uygulamanın saati. İkisini aynı sorguda karıştırma.
Küçük Bir Deneme
★☆☆ Title.EndsWith(term), Title.Replace(" ", "-") ve string.IsNullOrEmpty(b.Title) için ToQueryString() çıktısını al ve function mapping tablosuyla karşılaştır.
★★☆ Orders.CreatedAt'e bir index ekle. "Son 30 gün" filtresini hem CreatedAt >= since hem DateDiffDay ile yaz ve iki sorgunun SET STATISTICS IO çıktısını karşılaştır.
★★★ Bir yazarın kitap sayısını döndüren dbo.fn_AuthorBookCount(@authorId int) RETURNS int function'ı yaz, HasDbFunction ile eşle ve yazarlar listesinde Where içinde kullan. Sonra aynı function'ı son Select'te normal bir C# method'uyla değiştirip SQL'in nasıl değiştiğini incele.
Bu yazıda modelin kendisine pek dokunmadık; HasMaxLength ve HasPrecision dışında convention'larla ilerledik. Bir sonraki yazıda Fluent API'nin kataloğunu çıkarıyoruz: kolon tipleri, default ve computed değerler, sequence ve HiLo, index seçenekleri ve ConfigureConventions ile toplu ayarlar.