EFEF Core Handbook

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.

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

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 native jsonb tipi kullanır
  • jsonb binary 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) ve jsonb (binary) var. EF Core Npgsql her zaman jsonb kullanır — doğru tercih. jsonb index 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/OwnsMany ile map edilmiş JSON'larda desteklenmez — EF Core 10'da Complex Type'a geçiş önerilir.