UZMAN
JSON Columns — EF Core 7+
İlişkili verileri ayrı tabloda tutmak yerine aynı satırda JSON sütunu olarak saklar. Ayrı tablo + JOIN gerektirmez. Adres, ayarlar, metadata gibi her zaman parent ile birlikte okunan veriler için ideal.
Yapılandırma
// Owned entity → JSON sütununa map
public class Order
{
public int Id { get; set; }
public Address ShippingAddress { get; set; } // JSON'da saklanır
public List<OrderTag> Tags { get; set; } // JSON dizisi
}
builder.OwnsOne(o => o.ShippingAddress, addr => addr.ToJson());
builder.OwnsMany(o => o.Tags, tag => tag.ToJson());
// JSON sütununu sorgulama — LINQ çevrilir
var orders = context.Orders
.Where(o => o.ShippingAddress.City == "İstanbul")
.ToList();
SQL'de nasıl saklanır:
CREATE TABLE [Orders] (
[Id] INT IDENTITY(1,1) NOT NULL,
[ShippingAddress] NVARCHAR(MAX) NULL, -- JSON objesi (text olarak saklanır)
[Tags] NVARCHAR(MAX) NULL, -- JSON dizisi
CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED ([Id])
);
CREATE TABLE orders (
id INTEGER GENERATED ALWAYS AS IDENTITY,
shipping_address JSONB NULL, -- jsonb (binary JSON, indexlenebilir!)
tags JSONB NULL,
CONSTRAINT pk_orders PRIMARY KEY (id)
);
-- GIN index ile JSON içinde hızlı arama (SQL Server'da yok!):
CREATE INDEX ix_orders_shipping ON orders USING gin (shipping_address);
Örnek veri:
| Id | ShippingAddress | Tags |
|---|---|---|
| 1 | {"Street":"Bağdat Cad. 42","City":"İstanbul","PostalCode":"34710","Country":"Türkiye"} |
[{"Name":"Acil"},{"Name":"Hediye"}] |
| 2 | {"Street":"Kordon 15","City":"İzmir","PostalCode":"35220","Country":"Türkiye"} |
[{"Name":"Standart"}] |
EF'in ürettiği JSON sorgusu:
-- .Where(o => o.ShippingAddress.City == "İstanbul")
SELECT [o].[Id], [o].[ShippingAddress], [o].[Tags]
FROM [Orders] AS [o]
WHERE JSON_VALUE([o].[ShippingAddress], '$.City') = N'İstanbul';
-- .Where(o => o.ShippingAddress.City == "İstanbul")
SELECT o.id, o.shipping_address, o.tags
FROM orders AS o
WHERE o.shipping_address->>'City' = 'İstanbul'; -- ->> operatörü (text olarak al)
-- Alternatif: @> operatörü (GIN index kullanır, çok daha hızlı):
SELECT * FROM orders
WHERE shipping_address @> '{"City": "İstanbul"}';
JSON Güncelleme
// Nested property güncelleme — EF tüm JSON'u yeniden yazar
var order = await context.Orders.FindAsync(1);
order.ShippingAddress = order.ShippingAddress with { City = "Ankara" }; // record ise
await context.SaveChangesAsync();
// Koleksiyon ekleme
order.Tags.Add(new OrderTag { Name = "Express" });
await context.SaveChangesAsync();
-- EF tüm JSON bloğunu UPDATE eder (partial update değil):
UPDATE [Orders]
SET [ShippingAddress] = N'{"Street":"Bağdat Cad. 42","City":"Ankara","PostalCode":"34710","Country":"Türkiye"}'
WHERE [Id] = 1;
-- EF tüm JSON bloğunu UPDATE eder (partial update değil):
UPDATE orders
SET shipping_address = '{"Street":"Bağdat Cad. 42","City":"Ankara","PostalCode":"34710","Country":"Türkiye"}'::jsonb
WHERE id = 1;
Nested (İç İçe) LINQ Sorguları
// JSON içindeki koleksiyonda filtreleme
var urgentOrders = await context.Orders
.Where(o => o.Tags.Any(t => t.Name == "Acil"))
.ToListAsync();
// JSON property'ye göre sıralama
var sorted = await context.Orders
.OrderBy(o => o.ShippingAddress.City)
.ToListAsync();
// JSON property'yi projection'da kullanma
var addresses = await context.Orders
.Select(o => new { o.Id, City = o.ShippingAddress.City })
.ToListAsync();
-- .Where(o => o.Tags.Any(t => t.Name == "Acil"))
-- SQL Server:
SELECT [o].[Id], [o].[ShippingAddress], [o].[Tags]
FROM [Orders] AS [o]
WHERE EXISTS (
SELECT 1 FROM OPENJSON([o].[Tags]) WITH ([Name] NVARCHAR(MAX) '$.Name') AS [t]
WHERE [t].[Name] = N'Acil'
);
-- PostgreSQL:
SELECT o.id, o.shipping_address, o.tags
FROM orders AS o
WHERE o.tags @> '[{"Name": "Acil"}]'; -- jsonb contains (GIN index destekli)
Kısıtlamalar:
- JSON sütununa index konulamaz (computed column + index ile çözülebilir)
- Partial update yok — EF tüm JSON'u yeniden yazar (büyük JSON'da dikkat)
- SQLite ve eski MySQL JSON columns desteklemez
PostgreSQL JSON Avantajları
PostgreSQL JSON Avantajları:
- SQL Server
NVARCHAR(MAX)+JSON_VALUE()kullanırken, PostgreSQL nativejsonbtipi kullanırjsonbbinary formatta saklanır → parse overhead yok, sorgulama çok daha hızlıjsonbüzerine GIN index konulabilir → JSON içinde arama index'li olur!- Ek operatörler:
@>(contains),?(key exists),->(get field),->>(get text)- Partial update:
jsonb_set()fonksiyonu ile tek alan güncellenebilir (EF bunu henüz kullanmaz ama Raw SQL ile mümkün)
// PostgreSQL JSON — EF aynı API ama üretilen SQL farklı:
builder.OwnsOne(o => o.ShippingAddress, addr => addr.ToJson());
// SQL Server: NVARCHAR(MAX) + JSON_VALUE/OPENJSON
// PostgreSQL: jsonb + native operatörler
-- PostgreSQL'de oluşan tablo:
CREATE TABLE orders (
id INT GENERATED ALWAYS AS IDENTITY,
shipping_address JSONB NULL, -- jsonb (binary JSON, indexlenebilir!)
tags JSONB NULL,
CONSTRAINT pk_orders PRIMARY KEY (id)
);
-- GIN index ile JSON içinde hızlı arama:
CREATE INDEX ix_orders_shipping ON orders USING gin (shipping_address);
-- EF sorgusu: .Where(o => o.ShippingAddress.City == "İstanbul")
-- PostgreSQL çıktısı:
SELECT o.id, o.shipping_address, o.tags
FROM orders AS o
WHERE o.shipping_address->>'City' = 'İstanbul'; -- ->> operatörü (text olarak al)
-- JSON contains operatörü (GIN index kullanır):
SELECT * FROM orders
WHERE shipping_address @> '{"City": "İstanbul"}'; -- Çok hızlı!
jsonb vs json: PostgreSQL'de
json(text) vejsonb(binary) var. EF Core Npgsql her zamanjsonbkullanır — doğru tercih.jsonbindex destekler, duplicate key'leri kaldırır, daha hızlıdır.
JSON Sütun Limitleri:
Limit PostgreSQL ( jsonb)SQL Server ( NVARCHAR(MAX))Max boyut 1 GB (TOAST ile) 2 GB Max iç içe derinlik (nesting) Sınır yok (pratik: <100) 128 level Max key sayısı/obje Sınır yok Sınır yok Index desteği GIN index (computed column ile) Partial update (tek alan güncelleme) jsonb_set()ile (Raw SQL)Pratik sınırlar:
- Tek satırdaki JSONB boyutu < 100 KB tutulmalı (TOAST overhead, sorgu yavaşlar)
- Çok büyük JSON (>1 MB) → ayrı tabloya normalize et veya dosya storage kullan
- EF Core her update'te tüm JSON'u yeniden yazar — 500 KB'lık JSON'da bile her update 500 KB yazar!
- Deeply nested (>5 level) JSON → LINQ-to-SQL çevirisi karmaşıklaşır, performans düşer
🆕 EF Core 10: Native JSON Veri Tipi (SQL Server 2025)
SQL Server 2025 + EF Core 10 ile artık NVARCHAR(MAX) yerine native json veri tipi kullanılır:
// EF Core 10 + SQL Server 2025 (UseAzureSql veya compat level 170+)
// Otomatik olarak json tipi kullanılır — ekstra config gerekmez!
// Mevcut NVARCHAR(MAX) JSON sütunları migration ile json'a dönüşür
// Opsiyonel: eski davranışı korumak istersen:
builder.Property(o => o.ShippingAddress).HasColumnType("nvarchar(max)");
-- EF Core 10 ile oluşan tablo (SQL Server 2025):
CREATE TABLE [Orders] (
[Id] INT NOT NULL IDENTITY,
[ShippingAddress] json NOT NULL, -- Native JSON tipi (binary storage, daha hızlı)
[Tags] json NOT NULL,
CONSTRAINT [PK_Orders] PRIMARY KEY ([Id])
);
-- JSON_VALUE artık RETURNING clause kullanır (EF10):
SELECT [o].[Id], [o].[ShippingAddress]
FROM [Orders] AS [o]
WHERE JSON_VALUE([o].[ShippingAddress], '$.City' RETURNING nvarchar(max)) = N'İstanbul';
-- PostgreSQL zaten native JSONB destekler (SQL Server 2025'ten önce):
CREATE TABLE orders (
id INT GENERATED BY DEFAULT AS IDENTITY,
shipping_address JSONB NOT NULL, -- Binary JSON (index destekli)
tags JSONB NOT NULL,
CONSTRAINT pk_orders PRIMARY KEY (id)
);
-- JSONB operatörü ile sorgulama:
SELECT o.id, o.shipping_address
FROM orders AS o
WHERE o.shipping_address->>'City' = 'İstanbul';
🆕 EF Core 10 (GA): ExecuteUpdate ile JSON Property Güncelleme
// JSON içindeki tek bir alanı toplu güncelleme (Complex Type olarak map edilmişse):
// Önkoşul: ComplexProperty + ToJson() kullanılmalı (Owned Entity ile çalışmaz!)
modelBuilder.Entity<Blog>().ComplexProperty(b => b.Details, bd => bd.ToJson());
// Toplu güncelleme — JSON içindeki Views alanını +1 artır
await context.Blogs.ExecuteUpdateAsync(s =>
s.SetProperty(b => b.Details.Views, b => b.Details.Views + 1));
-- SQL Server 2025 (native json tipi ile):
UPDATE [b]
SET [Details].modify('$.Views', JSON_VALUE([b].[Details], '$.Views' RETURNING int) + 1)
FROM [Blogs] AS [b];
-- SQL Server eski sürüm (nvarchar JSON):
UPDATE [b]
SET [Details] = JSON_MODIFY([b].[Details], '$.Views', JSON_VALUE([b].[Details], '$.Views') + 1)
FROM [Blogs] AS [b];
-- PostgreSQL: jsonb_set ile partial update
UPDATE blogs
SET details = jsonb_set(details, '{Views}', ((details->>'Views')::int + 1)::text::jsonb)
;
Önemli: JSON ExecuteUpdate sadece Complex Type mapping ile çalışır.
OwnsOne/OwnsManyile map edilmiş JSON'larda desteklenmez — EF Core 10'da Complex Type'a geçiş önerilir.