EFEF Core Handbook

UZMAN

Primitive Collections — EF Core 8+

List<string>, int[], List<DayOfWeek> gibi basit tip koleksiyonları artık ayrı tablo gerektirmeden JSON sütununa otomatik saklanır. Tags, roller, izin listeleri gibi basit diziler için ayrı ilişki tablosu açmak yerine bu özelliği kullan.

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

Kullanım

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string[] Tags { get; set; }         // JSON array olarak saklanır
    public List<int> LikedByUserIds { get; set; }
}

// Yapılandırma (genellikle convention yeterli)
builder.PrimitiveCollection(p => p.Tags)
       .HasMaxLength(1000);

// Sorgulama (LINQ → SQL çevrimi)
var tagged = context.Posts
    .Where(p => p.Tags.Contains("efcore"))
    .ToList();

Tabloda nasıl görünür:

Id Title Tags LikedByUserIds
1 EF Core 8 Yenilikleri ["efcore","dotnet","csharp"] [1,5,12,28]
2 React ile SPA ["react","frontend"] [3,7]

Contains sorgusu SQL'e nasıl çevrilir:

-- .Where(p => p.Tags.Contains("efcore"))
SELECT [p].[Id], [p].[Title], [p].[Tags], [p].[LikedByUserIds]
FROM [Posts] AS [p]
WHERE N'efcore' IN (
    SELECT [t].[value] FROM OPENJSON([p].[Tags]) WITH ([value] NVARCHAR(MAX) '$') AS [t]
);
-- .Where(p => p.Tags.Contains("efcore"))
SELECT p.id, p.title, p.tags, p.liked_by_user_ids
FROM posts AS p
WHERE p.tags @> '["efcore"]';  -- jsonb contains operatörü (GIN index destekli)

-- Veya array tipinde saklanıyorsa:
-- WHERE 'efcore' = ANY(p.tags);  -- native array operatörü

Desteklenen LINQ Operasyonları

// Contains — dizide eleman var mı
context.Posts.Where(p => p.Tags.Contains("efcore"));

// Count — dizi uzunluğu
context.Posts.Where(p => p.LikedByUserIds.Count > 5);

// Any — koşullu kontrol
context.Posts.Where(p => p.Tags.Any(t => t.StartsWith("dot")));

// Indexer — belirli pozisyon (EF Core 8+)
context.Posts.OrderBy(p => p.Tags[0]);  // İlk tag'e göre sırala

EF Core 8 Öncesi — Manual JSON Conversion

// EF Core 7 ve öncesinde bu tip koleksiyonlar için manual converter gerekiyordu:
builder.Property(p => p.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
        v => JsonSerializer.Deserialize<string[]>(v, (JsonSerializerOptions?)null)!)
    .HasColumnType("nvarchar(max)");
// EF Core 8+ → bunlara gerek yok, PrimitiveCollection otomatik halleder

Primitive Collection vs JSON Column vs Ayrı Tablo

Yöntem Ne Zaman LINQ Desteği Performans
PrimitiveCollection (EF8+) Basit liste (string, int) Contains, Count, Any OPENJSON overhead
JSON Column (ToJson()) Kompleks nested nesne Full LINQ Parse overhead
Ayrı tablo (1-N ilişki) Büyük veri, index gerekli Full En hızlı (index)
PostgreSQL Native Array Desteği

PostgreSQL Büyük Avantaj — Native Array Desteği:

  • SQL Server'da PrimitiveCollectionNVARCHAR(MAX) + OPENJSON ile sorgulanır (yavaş)
  • PostgreSQL'de native array tipi vardır: text[], int[], uuid[] — JSON'a gerek yok!
  • Native array üzerine GIN index konulabilir → @> (contains) sorguları index kullanır
  • EF Core Npgsql, List<string> / string[] → otomatik text[] olarak map eder
// PostgreSQL — Native Array (JSON değil!)
public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string[] Tags { get; set; }         // PG: text[] (native array)
    public List<int> LikedByUserIds { get; set; } // PG: integer[]
}

// Configuration (Npgsql otomatik yapar ama açık belirtilebilir):
builder.Property(p => p.Tags)
       .HasColumnType("text[]");

builder.Property(p => p.LikedByUserIds)
       .HasColumnType("integer[]");

// GIN index ile hızlı array arama:
builder.HasIndex(p => p.Tags).HasMethod("gin");
-- PostgreSQL'de oluşan tablo:
CREATE TABLE posts (
    id                INT GENERATED ALWAYS AS IDENTITY,
    title             TEXT NOT NULL,
    tags              TEXT[]    NULL,      -- Native array! JSON değil!
    liked_by_user_ids INTEGER[] NULL,
    CONSTRAINT pk_posts PRIMARY KEY (id)
);

-- GIN index:
CREATE INDEX ix_posts_tags ON posts USING gin (tags);

-- EF sorgusu: .Where(p => p.Tags.Contains("efcore"))
-- PostgreSQL çıktısı (OPENJSON yerine native operatör!):
SELECT p.id, p.title, p.tags, p.liked_by_user_ids
FROM posts AS p
WHERE p.tags @> ARRAY['efcore']::text[];  -- @> = contains, GIN index kullanır!

-- Diğer array operatörleri:
WHERE tags && ARRAY['efcore','dotnet']  -- overlap (herhangi biri var mı)
WHERE 'efcore' = ANY(tags)              -- ANY ile eleman kontrolü
WHERE array_length(liked_by_user_ids, 1) > 5  -- Count karşılığı

Performans farkı: SQL Server'da OPENJSON her sorgu için JSON parse eder. PostgreSQL'de native array operatörleri GIN index ile O(1) erişim sağlar — büyük veri setlerinde dramatik fark yaratır.