Entity Framework Core – Value Converters
Merhaba,
.NET ile geliştirilen uygulamalarda olmazsa olmaz diyebileceğimiz ORM(Object Relational Mapping) aracımız olan Entity Framework Core, uygulamalarımızı işlevsel açıdan her ne kadar veritabanlarından izole bir vaziyete getiriyor olsa da bir yandan da verisel açıdan belli başlı değerlere olan bağımlılıkları ortadan kaldırabilme niteliğiyle eşsiz bir hizmet sağlayabiliyor. İşte bu içeriğimizde Entity Framework Core’un bu kabiliyetini sağlayan Value Converters özelliğini inceliyor olacağız.
Her şeyden önce direkt olarak değerlere olan bağımlılıklardan kastedileni izah ederek başlayalım. Farz-ı misal, şirketinizdeki personel verilerinin depolandığı tabloda cinsiyet verileri aşağıdaki gibi tutuluyor olsun.
Görüldüğü üzere bu tablodaki ‘Gender’ kolonundaki verilerden ‘M’ olanların erkek(male), ‘F’ olanların ise kadın(female) olduğunu anlıyoruz. Yani bu tabloda, bir personelin cinsiyeti hakkında bilgi edinebilmek için ‘Gender’ kolonuna bakmalı ve ‘M’ ya da ‘F’ değerlerine göre yorumda bulunulmalıdır. Buradan anlaşılan bir personelin cinsiyet bilgisini öğrenebilmek için ‘M’ ya da ‘F’ karakterlerine bağımlı bir şekilde hareket edilmelidir diyebiliriz.
İşte değerlere olan bağımlılıktan kastımız budur! Yani burada ‘M’ ya da ‘F’ değerleri yerine başka karakterlere, değerlere, ifadelere ve hatta cümlelere yer verilebilir ve yüklenen anlamlara göre cinsiyet durumları eşleştirilebilirdi. Misal olarak; ‘X’ değerine erkek, ‘Y’ değerine kadın denebilir ve haliyle böyle bir durumda da ‘X’ ve ‘Y’ değerlerine bağımlı hareket edilebilirdi.
EF Core, değerlere olan bu tarz bağımlılıkları yönetebilmemizi ve hatta ortadan kaldırabilmemizi sağlayabilecek niteliğe sahiptir. Nasıl mı? EF Core ile yapılan sorgulamalarda salt bir şekilde sorgu çekilirse eğer veriler ne ise o şekilde elde edilecektir. Lakin, Value Converters özelliği sayesinde gelecek olan veriler, veritabanından sorgulandıktan sonra farklı değerlere dönüştürülerek orjinalleriyle ilişkili olabilecek şekilde elde edilebilir. Bu durumu yine örneklendirmemiz gerekirse eğer, yukarıdaki tabloyu sorgularken cinsiyetlerin ‘M’ ya da ‘F’ olarak gelmesinden ziyade ‘M’ yerine ‘Male’, ‘F’ yerine ise ‘Female’ değerlerinin gelmesini sağlayabiliriz. Böylece iş mantığı süreçleri daha anlamlı olabilir ve veritabanındaki salt değerlere bağımlı bir şekilde hareket etmek zorunda kalınmayabilir.
Bu senaryo, sadece veri sorgulamakta değil ihtiyaç doğrultusunda veri ekleme ve güncelleme operasyonlarında da geçerli olabilmektedir. Yani bizler EF Core ile cinsiyet bilgisi ‘Male’ ya da ‘Female’ olan personeli veritabanına eklerken bu veriler yerine Value Converters sayesinde farklı değerler gönderebilir ve yine okurken de farklı dönüşümler neticesinde elde edip, okuyabiliriz.
Peki bu işlemi nasıl yapıyoruz? gelin inceleyelim…
Value Converters Kullanımı
EF Core ile Value Converters niteliğinin kullanımı oldukça basittir. Bunun için yapılması gereken ‘DbContext’ sınıfı üzerinde override
edilmiş olan ‘OnModelCreating’ metodu içerisinde aşağıdaki gibi işlemlerin gerçekleştirilmesidir.
class ExampleDbContext : DbContext { public DbSet<Employee> Employees { get; set; } . . . protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Employee>() .Property(e => e.Gender) .HasConversion( e => e, //Kaydedilirken - Insert | Update e => e == "M" ? "Male" : "Female" //Okunurken - Select ); } }
Yukarıdaki kod bloğunu incelerseniz eğer ‘Employee’ entity’sinin ‘Gender’ property’si üzerinde çağrılan HasConversion
fonksiyonu sayesinde value converters devreye sokulmakta ve birinci parametresinde veritabanına salt değer kaydedilirken(ki dönüşüm yaparakta bu işlem gerçekleştirilebilir), ikinci parametresinde işlenerek sorgulama süreçleri için cinsiyet verisi istenilen formata dönüştürülmektedir.
Haliyle ilgili context üzerinden aşağıdaki gibi veri ekleme operasyonu sonrasında:
ExampleDbContext context = new(); Employee e1 = new() { Name = "Gençay Yıldız", Price = 1000, Gender = "M" }; Employee e2 = new() { Name = "Gülsüm Yiğit", Price = 7000, Gender = "F" }; Employee e3 = new() { Name = "Eren Koş", Price = 14000, Gender = "M" }; await context.Employees.AddRangeAsync(e1, e2, e3); await context.SaveChangesAsync();
yandaki ekran görüntüsündeki gibi veriler kaydedilirken, bir yandan da aşağıdaki gibi sorgulanırlarsa:
ExampleDbContext context = new(); var employees = await context.Employees.ToListAsync();
elde edilen veriler yandaki görseldeki gibi gelmektedirler.
Yapısal olarak bu tarz bir kabiliyetin ne gibi getirileri olabileceğini tartışmak göreceli olsa da, eldeki verilerden anlamlı ve okunabilir bir sonuç çıkarabilmemizi sağlayacağı aşikardır. Bunu en iyi şekilde enum türünden verilerde gözlemleyebilmekteyiz.
Aşağıdaki ‘Soldier’ sınıfını incelerseniz eğer rütbeyi ifade eden ‘Rank’ property’si bir enum olarak tasarlanmıştır.
class Soldier { public int Id { get; set; } public string Name { get; set; } public Rank Rank { get; set; } } enum Rank { Lieutenant, Captain, Colonel }
İlgili tür üzerinde aşağıdaki gibi insert işlemi gerçekleştirdiğimizde:
ExampleDbContext context = new(); Soldier s1 = new() { Name = "Ahmet Şafak", Rank = Rank.Captain }; Soldier s2 = new() { Name = "Mehmet Şafak", Rank = Rank.Lieutenant }; Soldier s3 = new() { Name = "Rıfkı Şafak", Rank = Rank.Colonel }; await context.Soldiers.AddRangeAsync(s1, s2, s3); await context.SaveChangesAsync();
yandaki görseldeki gibi veriler kaydedilecektir. Dikkat ederseniz, veritabanında enum değerlere karşılık olarak integer türde değerler tutulmaktadır. Eee bu zaten enum değerlerin doğası itibariyle gayet normaldir. Ki enum değerler, belirli ifadeleri tam sayı karşılıklarında tutmamızı sağlayan değerlerdir. Bundan dolayı EF Core, enum bir property’i direkt olarak integer kabul etmekte ve o şekilde migrate etmektedir.
İşte bizler Value Converters sayesinde bu kabule müdahale edebilmekte ve bir property enum dahi olsa farklı türlerde kayıt işlemini gerçekleştirebilmekteyiz.
Misal olarak, aşağıdaki kod bloğunu incelerseniz eğer:
class ExampleDbContext : DbContext { public DbSet<Soldier> Soldiers { get; set; } . . . protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Soldier>() .Property(e => e.Rank) .HasConversion( v => v.ToString(), v => (Rank)Enum.Parse(typeof(Rank), v) ); } }
Görüldüğü üzere ‘Rank’ property’sinde kaydedilecek veri gönderilen enum’ın string değeriyken, sorgulama sürecinde elde edilecek hali ise enum türüne karşılık olan halidir. Hal böyleyken üretilen migration’a göz atarsak eğer artık ilgili kolon türünün integer değil string olarak ayarlandığını görebiliriz.
Ve veri eklendiğinde ilgili kolonun aşağıdaki gibi metinsel olarak kaydedildiğini gözlemlemekteyiz.
Nihayetinde kaydedilen verilerin bu hali bir önceki sayısal halinden daha okunaklı ve anlamlıdır diyebiliriz. İşte bu ve buna benzer amaçlarla value converter’lar kullanılabilmektedir.
ValueConverter Sınıfı
Value Converter işlemi için özünde kullanılan sınıf ValueConverter
sınıfıdır. Yani yukarıdaki işlemi aşağıdaki gibi ilgili sınıfla da gerçekleştirebilirdik:
. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { ValueConverter<Rank, string> rankConverter = new( v => v.ToString(), v => (Rank)Enum.Parse(typeof(Rank), v) ); modelBuilder.Entity<Soldier>() .Property(e => e.Rank) .HasConversion(rankConverter); } . . .
Bu şekilde bir kullanımdan ziyade, ValueConverter
sınıfından türeyen ve direkt verisel bazlı convert işlemine odaklanmış bir sınıf üretilmesi daha kullanışlı olacaktır kanaatindeyim.
class RankConverter : ValueConverter<Rank, string> { public RankConverter() : base( v => v.ToString(), v => (Rank)Enum.Parse(typeof(Rank), v) ) { } }
. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Soldier>() .Property(e => e.Rank) .HasConversion<RankConverter>(); } . . .
Built-in Converters Yapıları
EF Core basit dönüşümler için kendi bünyesinde yerleşik convert sınıfları barındırmaktadır. Misal olarak boolean türde ‘true’ ya da ‘false’ şeklinde tutulan bir değeri sayısal olarak ‘0’ ya da ‘1’e veya ‘Yes’ ya da ‘No’ gibi string değerlere dönüştürmemizi sağlayan hazır converter’lardan bahsediyoruz. Şimdi bu converter’ları örneklendirebilmek için aşağıdaki modelden yola çıkalım:
class Person { public int Id { get; set; } public string Name { get; set; } public bool Married { get; set; } }
‘Person’ modelini migrate ettiğimizde yandaki görselden de görüldüğü üzere ‘Married’ kolonu ‘bit’ türünde ayarlanmaktadır. Bizler istek ve ihtiyaç doğrultusunda bu değeri string, char ya da integer türünde tutabiliriz. İşte bunun gibi primitive türlere özel basit dönüşümler için BoolToStringConverter
, BoolToZeroOneConverter
ve BoolToStringConverter
gibi tonca converter türleri mevcuttur. Bizler burada kısmi birkaç örneklendirme yapıp konuyu sonlandıracağız.
- 1. – ilgili türün integer olarak tutulması için :
Bu işlem için aşağıdaki gibi. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .Property(p => p.Married) .HasConversion<int>(); } . . .
ya da
. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .Property(p => p.Married) .HasConversion<BoolToZeroOneConverter<int>>(); } . . .
BoolToZeroOneConverter
sınıfı eşliğinde çalışma yapılabilir. - 2. – ilgili türün string olarak tutulması için :
Bu işlem için iseBoolToStringConverter
sınıfı aşağıdaki gibi kullanılabilir:. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { BoolToStringConverter marriedConverter = new("Bekar", "Evli"); modelBuilder.Entity<Person>() .Property(p => p.Married) .HasConversion(marriedConverter); } . . .
- 3. – ilgili türün char olarak tutulması için :
Bu işlem için deBoolToTwoValuesConverter
türünden converter kullanılabilir.. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { BoolToTwoValuesConverter<char> marriedConverter = new('N', 'Y'); modelBuilder.Entity<Person>() .Property(p => p.Married) .HasConversion(marriedConverter); } . . .
Diğer tüm built-in converter sınıflarını incelemek istiyorsanız github üzerinden aşağıdaki görseldeki gibi ‘ValueConversion/Converter’ değerleri eşliğinde arama yapabilir ve mevcut olan sınıflara erişip, inceleyebilirsiniz.
İlkel Koleksiyonların(Collections of Primitives) Serilizasyonu
class Post { public int Id { get; set; } public string Title { get; set; } public string? Content { get; set; } public ICollection<string> Tags { get; set; } = new List<string>(); }
Yukarıdaki gibi içerisinde ilkel türlerden oluşturulmuş koleksiyonları barındıran modelleri migrate etmeye çalışıp aşağıdaki hatayı tanımayan yoktur sanırım 🙂
Hatanın metinsel hali;
The property ‘Post.Tags’ could not be mapped because it is of type ‘ICollection
Haliyle bu tarz veri yapılarında value converter yapılanmaları muazzam iş görmekte ve ilkel koleksiyonları normal metinsel değere dönüştürmemize fırsat verip, sorgulama süreçlerinde yine koleksiyon olarak elde edilmelerine imkan vermektedirler. Şöyle ki;
. . . protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .Property(p => p.Tags) .HasConversion( v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null) ); } . . .
Yukarıdaki convertion operasyonunu incelerseniz, ilkel koleksiyon türünden olan kolon json’a serilize edilerek sonrasında da okuma sürecinde deserilize’ye tabi tutulup elde edilmektedir. Tabi burada sizler json türünden ziyade daha farklı formatlarda serilizasyon gerçekleştirebilirsiniz.
Haliyle bu vaziyette aşağıdaki gibi veri eklendiğinde:
Post p1 = new() { Title = "Post 1", Tags = { "A", "B", "C", "D", "E", "F" } }; await context.Posts.AddAsync(p1); await context.SaveChangesAsync();
şeklinde kaydedilecek ve yine aşağıdaki gibi sorgulama gerçekleştirildiğinde:
ExampleDbContext context = new(); var posts = await context.Posts.ToListAsync();
şeklinde veriler koleksiyon olarak elde ediliyor olacaktır.
.NET 6 – Value Converter For Nullable Fields
.NET 6’dan önce value converter’lar da null/boş değerlerin dönüşümü desteklenmemekteydi. .NET 6 ile artık null/boş değerler de dönüşüm süreçlerinde kullanılabilmektedir.
Şöyle ki;
class Post { public int Id { get; set; } public string Title { get; set; } public string? Content { get; set; } public PostType? PostType { get; set; } public ICollection<string> Tags { get; set; } = new List<string>(); } enum PostType { Talk, Side, Citation, Unknown }
yukarıdaki modelde ‘PostType’ türünde tanımlanmış olan ‘PostType’ kolonunun null olma durumuna karşı aşağıdaki converter üretilmiştir.
class PostTypeConverter : ValueConverter<PostType, string> { public PostTypeConverter() : base( v => v == PostType.Unknown ? null : v.ToString(), v => v == null ? PostType.Unknown : Enum.Parse<PostType>(v), convertsNulls: true ) { } }
Burada görüldüğü üzere null durum hesaba katılarak dönüşüm sürecinde gerekli operasyon çekilmektedir. Tabi tüm bunları yapabilmek için ‘convertsNulls’ parametresinin ‘true’ değerine sahip olması gerekmektedir.
Haliyle yukarıdaki gibi ‘PostType’ı ‘Unknown’ olan bir veriyi eklediğimizde
görüldüğü üzere ilgili kolona null değerini atamaktadır. Benzer mantıkla sorgulama gerçekleştirdiğimizde ise
ilgili kolon ‘Unknown’ olarak gelmektedir.
İşte bu kadar 🙂
Nihai olarak,
EF Core bu davranışı sayesinde daha şeffaf halde çalışabilmemizi sağlamakla beraber, kendi gücüne de güç katmaktadır 🙂
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Elinize sağlık hocam. yine çok güzel ve anlaşılır bir yazı olmuş. 🙂