İ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:
HasConversionile referans tipi kullanılıyorsa, mutlakaValueComparerda 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+:
PrimitiveCollectionkullanı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 ENUMdestekler — Value Converter gerekmez!- Native array:
List<string>→text[](converter gereksiz, PG native halleder)- Interval:
TimeSpan→INTERVAL(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ı |