EF Core'da value conversion (değer dönüştürme), bir property'nin değerini veritabanına yazarken ve veritabanından okurken başka bir değere çevirmenizi sağlar. En yaygın kullanım enum'ları sayı yerine metin olarak saklamaktır: bunun için HasConversion<string>() yazmanız yeterlidir. Entity Framework Core ayrıca bool, Guid, DateTime, TimeSpan, Uri, IPAddress gibi tipler için hazır dönüştürücüler içerir; kendi dönüşümünüzü iki ifade vererek HasConversion(v => ..., v => ...) biçiminde ya da bir ValueConverter sınıfı yazarak tanımlarsınız.

Bu yazıda dönüştürücülerin nasıl çalıştığını, hazır dönüşümleri, kolon ayarlarıyla birlikte kullanımı, tüm modele toplu uygulamayı, değer nesneleri ve koleksiyonlar için gereken value comparer'ları ve belgelerin saydığı sınırlamaları anlatıyoruz. Kolonun kendisinin yapılandırılması property ve kolon yazısında, birden çok kolona yayılan değer nesneleri ise complex types yazısında ele alınıyor. Serinin tamamı EF Core öğrenme rehberi sayfasında.

Value converter nasıl çalışır?

Value converter nasıl çalışır?

Bir dönüştürücü iki tip üzerinden tanımlanır. ModelClrType, entity'deki property'nin .NET tipidir; ProviderClrType ise veritabanı sağlayıcısının anladığı .NET tipidir. Enum'u metin olarak saklarken model tipi enum, sağlayıcı tipi string olur. İki tip aynı da olabilir; örneğin bir string'i şifreleyip yine string olarak saklamak.

Dönüşüm iki Func ifade ağacıyla tanımlanır: biri model tipinden sağlayıcı tipine, diğeri tersi yönde. Belgeler ifade ağacı kullanılmasının nedenini açıklar: bu ifadeler veritabanı erişim delegesinin içine derlenir, böylece dönüşüm verimli çalışır. Karmaşık bir dönüşüm için ifade, bir dönüşüm metoduna basit bir çağrı da içerebilir.

Akılda tutulması gereken temel kural: null değer bir dönüştürücüye hiçbir zaman gönderilmez. Veritabanındaki bir null her zaman entity'de de null'dur ve tersi. Bu, dönüştürücü yazmayı kolaylaştırır ve aynı dönüştürücünün nullable ve nullable olmayan property'ler arasında paylaşılmasına izin verir; ama null'u başka bir değere çevirmek isterseniz bunu dönüştürücüyle yapamazsınız.

Enum'u string olarak saklamak

Belgelerdeki örnek bir binicinin bindiği hayvanı enum ile tutar ve bu değerleri veritabanında "Donkey", "Mule" gibi metinler olarak saklamayı gösterir. Metin olarak saklanan değerler veritabanına doğrudan bakan biri için de okunur olur. Hazır dönüşüm tablosunda enum için iki seçenek bulunur: enum'un sayısal değeri ya da metin karşılığı. İlk yol, metin dönüşümünü iki ifadeyle açıkça yazmaktır:

csharp
public class Rider
{
    public int Id { get; set; }
    public EquineBeast Mount { get; set; }
}

public enum EquineBeast
{
    Donkey,
    Mule,
    Horse,
    Unicorn
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}

Aslında bunu elle yazmanıza gerek yoktur. EF Core'daki hazır dönüşümler, modeldeki property tipine ve istenen sağlayıcı tipine bakarak doğru dönüştürücüyü kendisi seçer. Generic HasConversion<string>() çağrısı enum'u string'e çeviren yerleşik dönüştürücüyü kullanır. Aynı sonuca kolon tipini açıkça string tipi olarak vererek de ulaşırsınız; [Column(TypeName = "nvarchar(24)")] ya da HasColumnType("nvarchar(24)") yazdığınızda enum değerleri ek bir yapılandırma olmadan metin olarak saklanır.

csharp
modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion<string>();

// veya kolon tipiyle
public class Rider2
{
    public int Id { get; set; }

    [Column(TypeName = "nvarchar(24)")]
    public EquineBeast Mount { get; set; }
}

Kolon ayarları ve mapping hints

Uzunluk, Unicode, precision ve scale gibi kolon ayarları dönüştürücülü property'lerde de normal şekilde verilir ve dönüştürülmüş veritabanı tipine uygulanır. Enum'u string'e çevirirken kolonu Unicode olmayan ve en fazla 20 karakterlik yapmak için:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>()
        .HasMaxLength(20)
        .IsUnicode(false);
}

Belgelere göre bu yapılandırma, SQL Server'a karşı EF Core migration'ları kullanıldığında varchar(20) kolonu üretir. Belgelerin gösterdiği örnek tablo şöyledir:

sql
CREATE TABLE [Rider] (
    [Id] int NOT NULL IDENTITY,
    [Mount] varchar(20) NOT NULL,
    CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));

Aynı dönüştürücüyü kullanan her kolonun varsayılan olarak bu ayarlara sahip olmasını istiyorsanız bilgiyi dönüştürücünün kendisine ConverterMappingHints olarak verebilirsiniz: new ConverterMappingHints(size: 20, unicode: false). Bunlar yalnızca ipucudur; property üzerinde açıkça verilen ayarlar her zaman ipuçlarını ezer.

ValueConverter sınıfı ve toplu yapılandırma

HasConversion arka planda bir ValueConverter<TModel, TProvider> örneği oluşturup property'ye atar. Bu nesneyi kendiniz de oluşturabilirsiniz; aynı dönüşümü birden çok property kullanıyorsa bu yararlıdır. Daha ileri gidip tüm modelde aynı tipteki her property'ye bir dönüştürücü uygulamak istiyorsanız, dönüştürücüyü bir sınıf olarak tanımlar ve ConfigureConventions içinde pre-convention yapılandırmayla bağlarsınız. Belgelerdeki örnek kendi Currency tipini decimal'a çevirir:

csharp
var converter = new ValueConverter<EquineBeast, string>(
    v => v.ToString(),
    v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

modelBuilder
    .Entity<Rider>()
    .Property(e => e.Mount)
    .HasConversion(converter);

// Tum modelde Currency tipi icin:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

Pre-convention yapılandırmanın davranışı ve sınırları model yapılandırma yazısında anlatılıyor. Burada bir ayrıntı daha var: EF genellikle, o tipteki bir property için dönüştürücü tanımladıysanız desteklenmeyen tipteki sabitleri içeren sorguları da çevirebilir. Ama sorguda o tipte hiçbir property yoksa EF doğru dönüştürücüyü bulamaz. Bu durumda configurationBuilder.DefaultTypeMapping<Currency>().HasConversion<CurrencyConverter>() ile sağlayıcı tip eşlemesini ekleyebilirsiniz.

Hazır dönüşümler

Hazır dönüşümler: bool, bool, Enum, Enum

EF Core, Microsoft.EntityFrameworkCore.Storage.ValueConversion ad alanında bir dizi hazır dönüştürücü sınıfla gelir ve çoğu durumda doğru olanı kendisi seçer. Örneğin bir bool property'de HasConversion<int>() kullanmak, değerleri sıfır ve bire çevirir; bu, yerleşik BoolToZeroOneConverter<int> örneğini açıkça atamakla işlevsel olarak aynıdır. Belgelerdeki tablodan sık kullanılanlar:

Model tipiVeritabanı tipiDönüşümKullanım
boolsayısal tipfalse/true değerini 0/1 yaparHasConversion<int>() vb.
boolstringfalse/true değerini "N"/"Y" yaparHasConversion<string>()
Enumsayısal tipEnum'un sayısal değeriHasConversion<int>() vb.
EnumstringEnum değerinin metin karşılığıHasConversion<string>()
stringGuid, DateTime, sayısalMetni ilgili tipe ayrıştırırHasConversion<Guid>() vb.
DateTimelongDateTime.Kind bilgisini koruyarak kodlarHasConversion<long>()
TimeSpanlongTick değeriHasConversion<long>()
Uri, IPAddressstringDeğerin metin hâliHasConversion<string>()
Guidstringdddddddd-dddd-dddd-dddd-dddddddddddd biçimiHasConversion<string>()

Bu dönüşümler değerin biçiminin uygun olduğunu varsayar; örneğin sayıya ayrıştırılamayan metinleri sayıya çevirmek başarısız olur. Belgelere göre tüm yerleşik dönüştürücüler durumsuzdur (stateless), bu yüzden tek bir örnek birden çok property tarafından güvenle paylaşılabilir.

Değer nesneleri: tek kolona sarmak

Dönüştürücüler, ilkel bir tipi daha anlamlı ve tip güvenli bir sarmalayıcıyla değiştirmek için de kullanılır. Belgelerdeki Dollars örneği bir decimal'ı saran salt okunur bir struct'tır ve veritabanında doğrudan decimal olarak saklanır. Değer nesnesi readonly struct olarak yazıldığı için EF Core onun anlık görüntüsünü alıp karşılaştırırken bir sorun yaşamaz.

csharp
public readonly struct Dollars
{
    public Dollars(decimal amount)
        => Amount = amount;

    public decimal Amount { get; }
}

public class Order
{
    public int Id { get; set; }
    public Dollars Price { get; set; }
}

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => v.Amount,
        v => new Dollars(v));

Birden çok property'den oluşan değer nesnelerinde, örneğin tutar ve para birimini birlikte tutan bir Money tipinde, durum farklıdır. Belgelere göre dönüştürücüler şimdilik değerleri yalnızca tek bir kolona ve tek bir kolondan dönüştürebilir; bu yüzden nesnenin tüm değerleri tek bir kolon değerine kodlanmalıdır. Bu genellikle nesneyi yazarken JSON gibi bir biçime serileştirip okurken geri çözerek yapılır. Birden çok kolona eşlemek istiyorsanız EF Core 10'da yeniden ele alınan complex type desteğine bakmanız daha doğrudur (complex types ve JSON kolonları).

Value comparer: değişiklik takibi için karşılaştırma

Dönüştürücüler değişiklik takibiyle doğrudan ilişkilidir ve gözden kaçan hataların çoğu buradan çıkar. EF Core, bir entity yüklendiğinde onun anlık görüntüsünü (snapshot) alır ve SaveChanges sırasında bu görüntüyü uygulamaya verdiği nesneyle karşılaştırarak nelerin değiştiğini belirler. Standart tipler için bu mantık hazırdır. Ama dönüştürücüyle eşlenmiş bir property'de EF, rastgele kullanıcı tiplerini karşılaştırmak zorunda kalır. Varsayılan olarak tipin kendi eşitlik karşılaştırmasını (Equals) kullanır; anlık görüntü için değer tiplerini kopyalar, referans tiplerinde ise kopyalama yapmaz, aynı nesneyi görüntü olarak kullanır.

Bu varsayılan, değiştirilemez (immutable) tipler için sorunsuzdur: eşitliği doğru tanımlanmış değiştirilemez bir sınıf ya da salt okunur bir struct için özel bir şey yapmanız gerekmez. EF Core struct'lar için üye bazlı karşılaştırmaları kendisi üretir. Sorun değiştirilebilir referans tiplerinde başlar. List<int> referans eşitliği kullanır, yani aynı değerleri içeren iki liste farklı sayılır; üstelik değiştirilebilir, yani listeye eleman ekleyip çıkarabilirsiniz. Böyle bir listeyi JSON'a çeviren bir dönüştürücü yazdığınızda, EF'in listeye yapılan değişikliği fark edebilmesi için bir ValueComparer vermeniz gerekir:

csharp
modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

ValueComparer<T> constructor'ı üç ifade alır: eşitlik kontrolü, hash kodu üretimi ve anlık görüntü alma. Örnekte eşitlik, sayı dizilerinin aynı olup olmadığına bakılarak kontrol edilir; hash kodu aynı diziden üretilir; anlık görüntü ise listeyi ToList ile klonlayarak alınır. Belgeler iki uyarı ekler: değiştirilebilir değerler üzerinden hesaplanan hash kodu sorun çıkarabilir ve klonlama yalnızca listeler gerçekten değiştirilecekse gereklidir; mümkünse değiştirilemez tipler tercih edin. Karşılaştırma ve anlık görüntü mantığı birbiriyle uyumlu olmalıdır: derin karşılaştırma, derin anlık görüntü gerektirir. Change tracker'ın genel işleyişi için change tracker yazısına bakabilirsiniz.

Byte dizileri bu konunun iyi bir örneğidir. EF Core anahtar olmayan byte[] property'lerini varsayılan olarak referansla karşılaştırır: değişiklik yalnızca dizi yeni bir diziyle değiştirildiğinde algılanır, dizinin baytlarını yerinde değiştirmek algılanmaz. Bu, büyük dizileri kopyalayıp bayt bayt karşılaştırmaktan kaçınan bilinçli bir tercihtir; bir resmi başka bir resimle değiştirmek gibi yaygın senaryoyu verimli çalıştırır. Yerinde değişikliğin algılanmasını istiyorsanız property'ye içerik karşılaştıran bir comparer atarsınız. Anahtar olan byte dizilerinde ise EF zaten derin karşılaştırma yapar. Farklı anahtar semantiği gereken nadir durumlar için SetKeyValueComparer vardır; eski SetStructuralValueComparer artık kullanımdan kaldırılmıştır.

Belgelerdeki diğer kullanım örnekleri

Value conversion belgeleri birkaç pratik senaryo daha gösterir. SQL Server datetime ve datetime2 tiplerinde DateTime.Kind bilgisini saklamaz; bu yüzden veritabanından gelen değerlerin Kind'ı her zaman Unspecified'dır. HasConversion<long>() bu bilgiyi koruyan 8 baytlık opak bir değer üretir, ama o zaman veritabanında tanınabilir bir tarih kolonu kalmaz. Belgelerin daha yaygın bulduğu yol, her zaman UTC saklayıp okurken Kind'ı UTC olarak işaretlemektir:

csharp
modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v.ToUniversalTime(),
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Diğer örnekler: SQL Server rowversion kolonunu bayt dizisi yerine ulong property'ye eşlemek için IsRowVersion().HasConversion<byte[]>() (ayrıntılar concurrency yazısında); sabit uzunluklu char(20) anahtarlardaki boşluk dolgusunu okurken Trim ile temizlemek; anahtarları BlogKey gibi değer nesnelerine sarmak (primary key yazısı). Belgeler değerleri dönüştürücüyle şifreleme fikrini de, gerçek şifreleme yerine metni ters çeviren bir örnekle gösterir; ancak kendi şifrelemenizi yazmanın tüm sonuçlarını anlamanızı ve SQL Server'daki Always Encrypted gibi hazır mekanizmaları düşünmenizi açıkça uyarır.

Sınırlamalar

Sınırlamalar: null dönüştürülemez; null her iki yönde de null kalır., Dönüştürülmüş property'lerin içine sorgu yazılamaz; örneğin LINQ sorgusunda..., Bir property'nin dönüşümünü...

Belgelerin saydığı mevcut sınırlamalar, dönüştürücüyü ne zaman kullanmamanız gerektiğini de söyler:

  • null dönüştürülemez; null her iki yönde de null kalır.
  • Dönüştürülmüş property'lerin içine sorgu yazılamaz; örneğin LINQ sorgusunda dönüştürülmüş .NET tipinin üyelerine başvuramazsınız. Belgeler bunun yerine JSON kolonu kullanmayı düşünmenizi önerir.
  • Bir property'nin dönüşümünü birden çok kolona yaymak ya da tersini yapmak şu an mümkün değildir.
  • Dönüştürücüyle eşlenen anahtarların çoğunda değer üretimi desteklenmez.
  • Dönüştürücüler geçerli DbContext örneğine ya da başka oturum durumuna erişemez; bu da kullanılabilecek şifreleme türlerini sınırlar.
  • Dönüştürülmüş tiplerdeki parametreler şu an ham SQL API'lerinde kullanılamaz (FromSql yazısı).

Sık yapılan hatalar

  • Liste veya değiştirilebilir sınıf için comparer vermemek. Listeye eleman eklediğinizde EF değişikliği görmez ve kaydetmez.
  • Dönüştürücünün null'u işlemesini beklemek. Null hiçbir zaman dönüştürücüye ulaşmaz.
  • Dönüştürülmüş tipin üyeleri üzerinden LINQ ile filtrelemeye çalışmak. Bu sorgular çevrilemez; JSON kolonu ya da complex type düşünün.
  • Dönüştürülmüş tipte bir değeri ham SQL parametresi olarak vermek. Belgelere göre bu şu an desteklenmez ve sınırlamalar listesinde yer alır.
  • Mapping hints ile property ayarlarını karıştırmak. ConverterMappingHints yalnızca ipucudur; HasMaxLength gibi açık ayarlar onları ezer.

Sık Sorulan Sorular

Enum'u string olarak saklamanın en kısa yolu nedir?

Property(e => e.Mount).HasConversion<string>(). Aynı enum tipini kullanan çok sayıda property varsa, belgelerdeki Currency örneğinde olduğu gibi bir dönüştürücü sınıfı yazıp ConfigureConventions içinde Properties<T>().HaveConversion<TConverter>() ile tüm modele bir kerede bağlayabilirsiniz; pre-convention yapılandırma o tipteki tüm property'lere uygulanır.

Value converter ile complex type arasında nasıl seçim yaparım?

Dönüştürücü tek bir değeri tek bir kolona çevirir ve dönüştürülmüş değerin içine sorgu yazılamaz. Değer nesneniz birden çok alandan oluşuyor ve bu alanlar üzerinden sorgulama yapmak istiyorsanız complex type daha uygun bir araçtır.

Value comparer ne zaman gerekir?

Dönüştürücüyle eşlediğiniz tip değiştirilebilir bir referans tipiyse (liste, dizi, değiştirilebilir sınıf) ya da varsayılan eşitlik karşılaştırması amacınıza uymuyorsa. Değiştirilemez sınıflar ve salt okunur struct'lar için genellikle gerekmez.

Hazır dönüştürücüyü birden çok property'de aynı örnekle kullanabilir miyim?

Evet. Belgelere göre tüm yerleşik dönüştürücüler durumsuzdur ve tek bir örnek birden çok property tarafından güvenle paylaşılabilir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: EF Core öğrenme rehberi · Property ve kolon yapılandırma · Complex types ve JSON kolonları · Change tracker ve entity state · Concurrency ve rowversion · Model ve toplu yapılandırma