C# 9 ile gelen record tipleri, veri odaklı modelleri tasarlamak icin dil seviyesinde bir ifade kazandirdi. Mutable class'larin paylasilan referans uzerinden beklenmedik şekilde degismesi, çoklu thread ortamlarinda ve dagitik sistemlerde en sik karsilasilan hata kaynaklarindan biridir. Record'lar bu sorunu kokten ele alir: varsayilan olarak degismezdir, value-based equality uygular ve with ifadesi ile kontrollu kopya uretmeyi kolaylastirir.
Record ve class arasindaki temel farklar
Bir record tanimladiginizda derleyici arka planda bir class üretir; ancak davranis farklidir. Record'lar otomatik olarak Equals, GetHashCode ve ToString metodlarini üretir. Iki record ornegi, tüm özellik degerleri esitse esittir; referans kimligi önemli degildir. Bu, domain modellerinde anlam esitligini doğru modellemek icin kritik bir adimdir.
Mutable bir class ile karsilastiralim:
public class MutableOrder
{
public int Id { get; set; }
public string Status { get; set; } = "Pending";
public List<OrderLine> Lines { get; set; } = new();
}
public record ImmutableOrder(int Id, string Status, IReadOnlyList<OrderLine> Lines);
ImmutableOrder positional record olarak tanimlandi. Durum guncellemesi gerektiginde yeni bir örnek uretilir:
var order = new ImmutableOrder(1, "Pending", lines);
var shipped = order with { Status = "Shipped" };
order ve shipped farklı orneklerdir; orijinal veri bozulmaz. Bu kalip, event sourcing ve audit senaryolarinda da dogal bir uyum sağlar.
Positional record ve init-only property
Positional syntax kisa ve okunabilir olsa da her senaryoya uymaz. Karmaşık dogrulama veya hesaplanmis property gerektiginde init-only property tercih edilir:
public record CustomerEmail
{
public string Value { get; init; }
public CustomerEmail(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("Gecersiz e-posta", nameof(value));
Value = value.Trim().ToLowerInvariant();
}
}
Bu yaklaşım, value object kalibini record uzerine oturtur. Domain-driven design'da email, para birimi veya adres gibi kavramlar icin idealdir.
Collection immutability tuzagi
Record'un kendisi degismez olsa da icindeki mutable collection degisebilir. Bu en yaygın tuzaktir:
public record Basket(IReadOnlyList<BasketItem> Items);
var items = new List<BasketItem> { new(1, 2) };
var basket = new Basket(items);
items.Add(new BasketItem(3, 1)); // basket de etkilenir!
Çözüm: constructor'da defensive copy yapin veya ImmutableArray, ImmutableList kullanın:
public record SafeBasket
{
public IReadOnlyList<BasketItem> Items { get; init; }
public SafeBasket(IEnumerable<BasketItem> items)
{
Items = items.ToList().AsReadOnly();
}
}
Record struct: stack uzerinde value semantics
C# 10 ile gelen record struct, küçük ve sik kullanilan value type'lar icin uygundur. Örneğin koordinat, aralik veya para miktarı:
public readonly record struct Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Para birimi uyusmuyor");
return this with { Amount = Amount + other.Amount };
}
}
readonly record struct gereksiz kopyalamayi azaltir ve performans kritik hot path'lerde class record'a gore avantaj saglayabilir.
EF Core ve immutable modeller
Entity Framework Core, geleneksel olarak mutable entity bekler; cunku change tracker property setter'lari izler. Immutable domain modeli ile EF Core'u birlestirmek icin iki strateji vardir:
- Persistence model ayirimi: Domain record'lari ile EF entity'lerini ayri tutun; mapping katmaninda dönüşüm yapin.
- Owned type ve private constructor: Aggregate kokunu controlled mutation ile tasarlayin; disariya record DTO dondurun.
Örnek mapping yaklasimi:
public record ProductDto(int Id, string Name, decimal Price);
public class ProductEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
public static ProductDto ToDto(this ProductEntity e) =>
new(e.Id, e.Name, e.Price);
Bu ayrim, domain kurallarinin persistence detaylarindan izole kalmasini sağlar. Mediator veya repository katmaninda dönüşüm tek noktada toplanir.
JSON serilestirme ve API modelleri
ASP.NET Core minimal API ve controller'larda request/response modelleri icin record kullanımı giderek standart hale geldi. System.Text.Json, record'lari sorunsuz serilestirir. [JsonRequired] ve init-only property birlikte calisarak contract'i siki tutar:
public record CreateUserRequest
{
[JsonRequired]
public required string Email { get; init; }
[JsonRequired]
public required string DisplayName { get; init; }
}
C# 11 required keyword'u, deserialization sonrasi eksik alan birakilmasini derleme ve çalışma zamaninda yakalamaya yardımcı eder.
Test edilebilirlik ve determinizm
Immutable modeller test yazimini kolaylastirir. Arrange asamasinda olusturulan veri, Act asamasinda yanlislikla mutate edilmez; assertion'lar kararlı kalir. Özellikle paralel test calistirmada paylasilan mutable state kaynakli flaky testler azalir.
- Her test kendi record ornegini üretir; yan etki yoktur.
withifadesi ile varyasyon üretmek okunabilir fixture kodu sağlar.- Record'un
ToStringciktisi hata mesajlarini anlasilir kilar.
Ne zaman record, ne zaman class?
Record her yerde doğru seçim degildir. Identity-önemli entity'ler (DbContext, HttpClient wrapper, servis siniflari) icin class kullanmaya devam edin. Asagidaki tablo karar vermeyi kolaylastirir:
- Record tercih edin: DTO, command, query, value object, domain event, configuration snapshot.
- Class tercih edin: DI servisleri, ORM entity'leri (mutable tracker gerekiyorsa), IDisposable sahipligi.
- Record struct: Küçük, sik kopyalanan, stack'te yasayan degerler.
Performans dusunceleri
Record class'lar heap'te allocate edilir; büyük koleksiyonlar icin with her seferinde shallow copy yapar. Yüksek throughput senaryolarinda allocation profilini izleyin. record struct ve ReadOnlySpan tabanli API'ler gerektiginde devreye alin. Ancak çoğu web API ve is uygulamasinda okunabilirlik ve dogruluk kazanimi, mikro optimizasyondan onde gelir.
Özet kalıplar
Immutable model tasarımında asagidaki ilkeler tutarlı bir codebase oluşturur:
- Domain kavramlarini record value object olarak modelleyin.
- Collection alanlarinda defensive copy veya immutable koleksiyon kullanın.
- Persistence ve API katmanlarinda dönüşüm sinirlarini açıkça cizin.
- Durum gecislerini
withveya fabrika metodu ile yeni örnek uretimi olarak dusunun. - Mutable servis siniflari ile immutable veri modellerini karistirmayin.
Record'lar C#'ta veri odaklı dusunmeyi destekleyen güçlü bir aractir. Doğru sinirlarla kullanıldığında hem domain netligi hem de bakım kolayligi sağlar; yanlış collection paylasimi ve EF entegrasyonu gibi tuzaklarin farkinda olarak tasarım yapmak ise uzun vadede sistemin guvenilirligini belirler.
Derleyici uretimi kod ve equality
Record tipleri icin derleyici, <Clone>$ metodu ve deconstruct destegi üretir. Tuple deconstruction ile pattern matching birlestirilebilir:
public record Point(int X, int Y);
if (entity is Point { X: 0, Y: var y })
Console.WriteLine($"Y ekseni: {y}");
Custom equality gerektiginde IEqualityComparer<T> ile record dışında karşılaştırma da mumkundur; ancak çoğu senaryoda varsayilan value equality yeterlidir.
Thread safety ve concurrent pipeline
Immutable mesajlar Channel veya Parallel.ForEach pipeline'larinda guvenle paylasilir. Her asama yeni record uretirse race condition riski azalir. Mutable DTO kullanan legacy kodu record'a tasırken once read-only API sunup sonra ic implementasyonu değiştirmek incremental migration stratejisidir.
Serialization versioning
Record alanlarina yeni property eklendiginde geriye uyumluluk icin varsayilan degerler ve opsiyonel alanlar dusunun. Event store'da saklanan event tipleri versioning gerektirir; OrderCreatedV2 gibi ayri record tipleri veya schema registry yaklasimi tercih edilir.