Selamlar! Nasılsınız? Umarım her şey yolundadır. Ben yine kodların başında sabahladım diyemem tabi :), hafta sonu aileyle Bursa’da kısa bir doğa kaçamağı yaptık. Ama neyse efendim, asıl konumuza dönelim. Bugün sizlere yazılım dünyasının o gizemli ama bir o kadar da hayat kurtaran köşelerinden birinden bahsedeceğim: Custom Type Mapping. Hani bazen bir şeyleri birbirine dönüştürmeye çalışırken kafayı yiyecek gibi olursunuz ya, işte tam da o anlarda devreye giren sihirli bir değnek gibi düşünün bunu.
Şimdi, hepimiz biliyoruz ki yazılım geliştirirken farklı veri tipleriyle uğraşıyoruz. Veritabanından gelen bir kayıt, kullanıcının girdiği bir form, bir API’den çekilen JSON verisi… Bunların hepsi farklı şekillerde karşımıza çıkabiliyor. Ve bazen de bu farklı yapıdaki verileri, kendi kodumuzda kullanabileceğimiz daha anlamlı, daha kullanışlı hale getirmemiz gerekiyor. İşte bu noktada Custom Type Mapping yani özel tür eşleştirmesi devreye giriyor.
Peki, neden böyle bir şeye ihtiyaç duyalım ki? Açıkçası, ilk başlarda ben de bunu hep hazır kütüphanelerle hallediyordum. İşte AutoMapper gibi harika araçlar var, sağ olsunlar işimizi çok kolaylaştırıyorlar. Fakat bazen öyle durumlar oluyor ki, o genel çözümler yetersiz kalabiliyor veya daha ince ayarlar yapmak isteyebiliyorsunuz. Ya da belki de kendi kütüphanenizi yazıyorsunuz ve bu temel yapı taşlarından birini kendiniz oluşturmak istiyorsunuz. İşte o zamanlar, kendi özel eşleştirme mantığınızı yazmanın zamanı gelmiş demektir.
Düşünsenize, bir tane Date sınıfınız var, içinde sadece yıl, ay, gün bilgisi var. Bir de veritabanında tuttuğunuz bir DateTime alanı var, onda hem tarih hem saat bilgisi mevcut. Siz bu ikisini eşleştirmek istiyorsunuz. Normalde direkt atama yapamazsınız, değil mi? İşte burada devreye Custom Type Mapper’ınız giriyor. Bu arkadaş, Date nesnesini alıp, içindeki bilgileri kullanarak veritabanındaki DateTime alanına uygun bir şekilde dönüştürüyor. Ya da tam tersi, veritabanından gelen DateTime’ı alıp, sizin Date nesnenize uygun hale getiriyor.
Bu arada, benim de başıma gelmişti böyle bir şey. Bir projede kullanıcıların doğum tarihlerini tutuyorduk. Veritabanında `datetime` olarak saklanıyordu ama biz uygulamada sadece yıl, ay, gün kısmıyla ilgileniyorduk. Kullanıcıya yaşını gösterirken veya bir etkinlik için yaş sınırı koyarken, saat bilgisi gereksiz bir karmaşa yaratıyordu. Hatta bir keresinde, gece yarısına yakın doğmuş birini bir gün büyük göstermiştim sanırım, eşim fark etmişti hemen. Tamam, hata benim 🙂 Neyse efendim, işte o zamanlar custom mapping mantığını daha derinlemesine araştırmaya başladım. Kendi Date sınıfımı oluşturdum ve veritabanındaki `datetime` ile benim Date sınıfım arasında çift yönlü bir dönüşüm yazdım. Sonuç? Kod daha temiz, anlaşılır ve hata payı azaldı. Ne güzel değil mi?
Peki, bunu nasıl yapıyoruz? Aslında mantık oldukça basit. Genellikle bir arayüz (interface) tanımlarsınız, adı da `ITypeMapper` gibi bir şey olabilir. Bu arayüzün iki temel metodu olur: biri `MapToTarget` (kaynaktan hedefe dönüştür) diğeri de `MapToSource` (hedefden kaynağa dönüştür). Sonra da bu arayüzü implement eden sınıflar yazarsınız. Her sınıf, belirli bir tür çifti arasındaki dönüşümden sorumlu olur.
Örnek Kodlarla Anlatalım
Şimdi lafı daha fazla uzatmadan, gelin bunu bir kod örneğiyle somutlaştıralım. Diyelim ki elimizde iki tane basit sınıf var: biri bizim kendi `MyDate` sınıfımız, diğeri de veritabanından gelen genel bir `DateTimeModel`. Bunları nasıl eşleştirebiliriz, bir bakalım.
Öncelikle, dönüşümü yapacak olan mapper sınıfımızı oluşturalım:
// MyDate sınıfımız (Sadece tarih bilgisi tutuyor) public class MyDate { public int Year { get; set; } public int Month { get; set; } public int Day { get; set; } }// Veritabanından gelen bir model (Tarih ve Saat bilgisi tutuyor) public class DateTimeModel { public DateTime Value { get; set; } }
// Custom Type Mapper arayüzü public interface ITypeMapper { TTarget MapToTarget(TSource source); TSource MapToSource(TTarget target); }
// MyDate ve DateTimeModel arasındaki dönüşümü yöneten sınıf public class MyDateMapper : ITypeMapper<DateTimeModel, MyDate> { public MyDate MapToTarget(DateTimeModel source) { if (source == null || source.Value == default(DateTime)) { return null; // Veya istediğiniz başka bir hata durumu } return new MyDate { Year = source.Value.Year, Month = source.Value.Month, Day = source.Value.Day }; }
public DateTimeModel MapToSource(MyDate target) { if (target == null) { return null; // Veya istediğiniz başka bir hata durumu } // Saat bilgisini varsayılan olarak gece yarısı alabiliriz, projenize göre değişir return new DateTimeModel { Value = new DateTime(target.Year, target.Month, target.Day, 0, 0, 0) }; } }
İşte bu kadar basit! Gördüğünüz gibi, `MyDateMapper` sınıfı `ITypeMapper` arayüzünü implement ediyor ve hem `DateTimeModel`’den `MyDate`’e hem de `MyDate`’den `DateTimeModel`’e dönüşümü sağlıyor. Bu arada, bu kod örneği gayet basit tutuldu. Gerçek projelerde null kontrollerini daha detaylı yapmanız, belki hata loglama eklemeniz gerekebilir.
Şimdi gelelim bunu nasıl kullanacağımıza. Genellikle bir mapper registry’si (kayıt defteri) tutarsınız. Bu registry, hangi tür çifti için hangi mapper’ın kullanılacağını bilir. Bir istek geldiğinde, registry doğru mapper’ı bulur ve dönüşümü gerçekleştirir. Bu registry’yi manuel olarak doldurabileceğiniz gibi, reflection veya dependency injection ile daha dinamik hale de getirebilirsiniz. Mesela şöyle bir temel kullanım:
// Mapper registry'miz (basit bir dictionary ile) public class MapperRegistry { private readonly Dictionary<Type, Dictionary<Type, object>> _mappers = new Dictionary<Type, Dictionary<Type, object>>(); public void Register<TSource, TTarget>(ITypeMapper<TSource, TTarget> mapper) { var sourceType = typeof(TSource); var targetType = typeof(TTarget);
if (!_mappers.ContainsKey(sourceType)) { _mappers[sourceType] = new Dictionary<Type, object>(); } _mappers[sourceType][targetType] = mapper; }
public TTarget Map<TSource, TTarget>(TSource source) { var sourceType = typeof(TSource); var targetType = typeof(TTarget);
if (_mappers.ContainsKey(sourceType) && _mappers[sourceType].ContainsKey(targetType)) { var mapper = (ITypeMapper<TSource, TTarget>)_mappers[sourceType][targetType]; return mapper.MapToTarget(source); } throw new InvalidOperationException($"No mapper found for {sourceType.Name} to {targetType.Name}"); } }
// Kullanım örneği var registry = new MapperRegistry(); registry.Register(new MyDateMapper()); // Mapper'ımızı kaydettik
var dbDateTime = new DateTimeModel { Value = new DateTime(1990, 5, 15, 10, 30, 0) }; var myDate = registry.Map<DateTimeModel, MyDate>(dbDateTime);
Console.WriteLine($"MyDate: {myDate.Year}-{myDate.Month}-{myDate.Day}"); // Çıktı: MyDate: 1990-5-15
var newMyDate = new MyDate { Year = 2000, Month = 12, Day = 25 }; var newDbDateTime = registry.Map<MyDate, DateTimeModel>(newMyDate); // Ters dönüşüm
Console.WriteLine($"DateTimeModel: {newDbDateTime.Value}"); // Çıktı: DateTimeModel: 25.12.2000 00:00:00
Bu registry mantığı tabii ki çok basit. Gerçek bir uygulamada daha karmaşık yapılar, farklı türler için otomatik mapper bulma gibi özellikler gerekebilir. Mesela, bir `DateTime`’ı başka bir `DateTime`’a maplerken saat dilimi dönüşümleri gibi detaylar da işin içine girebilir. Açıkçası, böyle bir durumda tüm bu detayları tek tek düşünmek yerine, hazır kütüphanelerle ilerlemek daha mantıklı olabilir. Ama konsepti anlamak için bu örnek yeterli sanırım.
Sonuç olarak, Custom Type Mapping, veri dönüşümlerini daha kontrollü ve şeffaf hale getirmenin harika bir yolu. Özellikle karmaşık veri yapılarıyla çalışırken veya belirli iş mantıklarını dönüşüm sürecine dahil etmek istediğinizde imdadınıza yetişiyor. Kendi özel çözümlerinizi yazmak, kodunuzun daha okunabilir olmasını sağlıyor ve uzun vadede bakımını kolaylaştırıyor. Belki ilk başta biraz uğraştırıcı gibi görünebilir ama inanın ki, bu emeğinizin karşılığını fazlasıyla alıyorsunuz. Siz de denemeye ne dersiniz?