EFEF Core Handbook

İLERİ

Value Converters

C# tarafında zengin tipler (enum, DateOnly, custom class) kullanırken bunların veritabanında hangi formatta saklanacağını belirler. Örneğin enum'ı string olarak saklamak, ya da Money nesnesini decimal sütuna dönüştürmek.

Kullanım

// Enum → string
builder.Property(p => p.Status)
       .HasConversion<string>();

// Enum → int (varsayılan)
builder.Property(p => p.Status)
       .HasConversion<int>();

// Özel lambda dönüşümü
builder.Property(p => p.Tags)
       .HasConversion(
           v => string.Join(',', v),
           v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
       );

// ValueConverter sınıfı
var converter = new ValueConverter<Money, decimal>(
    money  => money.Amount,
    amount => new Money(amount, "TRY")
);

builder.Property(p => p.Price).HasConversion(converter);

// DateTimeOffset → long (Unix timestamp)
builder.Property(p => p.CreatedAt)
       .HasConversion(
           d => d.ToUnixTimeSeconds(),
           l => DateTimeOffset.FromUnixTimeSeconds(l)
       );

// Yerleşik built-in converters (EF Core 6+)
builder.Property(p => p.Flags)
       .HasConversion<EnumToStringConverter<MyEnum>>();

Enum → String dönüşümü DB'de nasıl görünür:

public enum OrderStatus { Pending, Confirmed, Shipped, Delivered, Cancelled }
Id Reference Status (C# int) Status (string conversion)
1 ORD-001 0 "Pending"
2 ORD-002 2 "Shipped"
3 ORD-003 4 "Cancelled"

Global Converter Convention — Tüm Enum'ları String Yap

// DbContext.ConfigureConventions() ile tek seferde:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    // Tüm enum property'ler string olarak saklanır
    configurationBuilder.Properties<OrderStatus>().HaveConversion<string>();
    configurationBuilder.Properties<PaymentType>().HaveConversion<string>();
    
    // Veya genel: tüm enum'lar string
    // (Custom convention ile — bkz. Bölüm 42)
}

Yaygın Converter Senaryoları

C# Tipi DB Tipi Converter Kullanım
Enum NVARCHAR HasConversion<string>() Okunabilirlik
Enum INT Varsayılan Performans
bool CHAR(1) 'Y'/'N' lambda Legacy DB uyumu
DateOnly DATE EF Core 8+ built-in .NET 6+ tipi
TimeOnly TIME EF Core 8+ built-in .NET 6+ tipi
List<T> NVARCHAR(MAX) JSON serialize EF Core 7 öncesi
Money (VO) DECIMAL Custom converter DDD value object
Uri NVARCHAR .ToString() / new Uri() URL saklama
CultureInfo NVARCHAR .Name / new CultureInfo() Dil bilgisi

Custom ValueConverter Sınıfı (Reusable)

// Tekrar kullanılabilir converter
public class StronglyTypedIdConverter<TId> : ValueConverter<TId, int>
    where TId : struct
{
    public StronglyTypedIdConverter()
        : base(
            id => (int)(object)id,
            value => (TId)(object)value)
    { }
}

// Kullanım
builder.Property(p => p.Id).HasConversion<StronglyTypedIdConverter<ProductId>>();

ValueComparer: HasConversion ile referans tipi kullanılıyorsa, mutlaka ValueComparer da tanımlanmalıdır. Aksi halde change tracking düzgün çalışmaz.

Value Comparer — Change Tracking ile İlişkisi

EF Core, bir property'nin değişip değişmediğini Value Comparer ile anlar.
Primitive tipler (int, string) için sorun yok — ama koleksiyon, JSON, custom class içeren property'lerde comparer tanımlanmazsa EF değişikliği algılayamaz.

Problem:

var product = await context.Products.FindAsync(1);
product.Tags.Add("yeni-tag");       // ← Listeye eleman eklendi
await context.SaveChangesAsync();   // ❌ EF bunu FARKETMEZ! Referans aynı kaldı.

Çözüm: Converter + Comparer birlikte tanımla:

builder.Property(p => p.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
        v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null)!)
    .HasColumnType("nvarchar(max)")
    .Metadata.SetValueComparer(new ValueComparer<List<string>>(
        (c1, c2) => c1!.SequenceEqual(c2!),              // Eşitlik: içerik bazlı
        c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),  // Hash
        c => c.ToList()));                                // Snapshot (deep copy)

Custom class (Value Object) için:

public record Money(decimal Amount, string Currency);

builder.Property(p => p.Price)
    .HasConversion(
        m => JsonSerializer.Serialize(m, (JsonSerializerOptions?)null),
        s => JsonSerializer.Deserialize<Money>(s, (JsonSerializerOptions?)null)!)
    .Metadata.SetValueComparer(new ValueComparer<Money>(
        (m1, m2) => m1!.Amount == m2!.Amount && m1.Currency == m2.Currency,
        m => HashCode.Combine(m.Amount, m.Currency),
        m => m with { }));  // record → deep copy

Ne zaman ValueComparer gerekir?

Property Tipi Gerekli mi? Neden
int, string, decimal Primitive — değer karşılaştırma zaten çalışır
DateTime, Guid Struct — değer bazlı
List<string>, List<int> Referans tipi (EF Core 8+ PrimitiveCollection ise otomatik)
Dictionary<string, object> Referans tipi
Custom class (Address, Money) Referans tipi (Owned Entity ise EF halleder)

EF Core 8+: PrimitiveCollection kullanıldığında ValueComparer otomatik atanır — manual tanımlamaya gerek kalmaz.

PostgreSQL Value Converter Farkları

PostgreSQL Value Converter Farkları:

  • Native enum desteği: PostgreSQL CREATE TYPE ... AS ENUM destekler — Value Converter gerekmez!
  • Native array: List<string>text[] (converter gereksiz, PG native halleder)
  • Interval: TimeSpanINTERVAL (SQL Server'da sınırlı destek)
  • inet/cidr: IP adresleri için native tip (converter gerektirmez)
// PostgreSQL native enum (Value Converter'a GEREK YOK!):
public enum OrderStatus { Pending, Shipped, Delivered, Cancelled }

// Npgsql'de enum mapping:
// 1. DbContext'e enum'ı kaydet:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasPostgresEnum<OrderStatus>();  // CREATE TYPE order_status AS ENUM(...)
}

// 2. Property doğrudan enum tipinde kalır:
builder.Property(o => o.Status)
       .HasColumnType("order_status");  // Native PG enum, string'e çevirmeye gerek yok!

// Avantaj: DB seviyesinde type safety + daha az disk alanı + index destekli
-- PostgreSQL'de:
CREATE TYPE order_status AS ENUM ('pending', 'shipped', 'delivered', 'cancelled');

CREATE TABLE orders (
    id     INT GENERATED ALWAYS AS IDENTITY,
    status order_status NOT NULL DEFAULT 'pending',  -- Native enum!
    CONSTRAINT pk_orders PRIMARY KEY (id)
);

-- WHERE ile doğrudan kullanılabilir:
SELECT * FROM orders WHERE status = 'shipped';
-- Geçersiz değer → DB hata verir (type safety)
INSERT INTO orders (status) VALUES ('invalid'); -- ERROR!

Enum karşılaştırma:

Yaklaşım SQL Server PostgreSQL Avantaj
HasConversion<string>() NVARCHAR TEXT Provider bağımsız
HasConversion<int>() INT INT Performanslı
Native PG enum CREATE TYPE AS ENUM Type-safe, küçük, hızlı