Hello! How are you? I hope everything is fine. I can’t say I stayed up all night coding again, of course 🙂 We took a short nature escape to Bursa with my family over the weekend. But anyway, let’s get back to our main topic. Today, I will talk about one of the mysterious yet life-saving corners of the software world: Custom Type Mapping. Think of it as a magic wand that comes into play when you’re about to go crazy trying to convert things from one form to another.
Now, we all know that when developing software, we deal with different data types. A record coming from a database, a form entered by a user, JSON data fetched from an API… All of these can appear in various forms. Sometimes, we need to turn these differently structured data into more meaningful, more useful forms within our code. This is where Custom Type Mapping, or special type matching, comes into play.
Why would we need such a thing? Honestly, at first, I always handled this with ready-made libraries. There are great tools like AutoMapper that make our job much easier. However, sometimes these general solutions fall short or you want to make finer adjustments. Or maybe you’re writing your own library and want to create this fundamental building block yourself. That’s when it’s time to write your own custom mapping logic.
Imagine you have a Date class that only stores year, month, and day information. And you have a DateTime field in your database that stores both date and time information. You want to match these two. Usually, you can’t assign them directly, right? Here, your Custom Type Mapper comes into play. It takes the Date object and, using its details, converts it appropriately to the DateTime field in the database. Or the other way around, taking the DateTime from the database and molding it into your Date class.
By the way, I experienced this myself once. In a project, we stored users’ birthdays. They were stored as
So, how do we do this? The logic is actually quite simple. Usually, you define an interface, maybe called
Let’s Explain with Example Code
Now, without further ado, let’s concrete this with a code example. Suppose we have two simple classes: one is our own
First, let’s create our mapper class that will perform the conversion:
// MyDate class (holds only date info) public class MyDate { public int Year { get; set; } public int Month { get; set; } public int Day { get; set; } }// Data model from the database (holds date and time info) public class DateTimeModel { public DateTime Value { get; set; } }
// Custom Type Mapper interface public interface ITypeMapper<TSource, TTarget> { TTarget MapToTarget(TSource source); TSource MapToSource(TTarget target); }
// Class managing conversion between MyDate and DateTimeModel public class MyDateMapper : ITypeMapper<DateTimeModel, MyDate> { public MyDate MapToTarget(DateTimeModel source) { if (source == null || source.Value == default(DateTime)) { return null; // Or any other error handling you prefer } 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; // Or any other error handling you prefer } // Can assign a default time, e.g., midnight, depending on your needs return new DateTimeModel { Value = new DateTime(target.Year, target.Month, target.Day, 0, 0, 0) }; } }
That’s it! As you can see, the
Now, how do we use this? Usually, you keep a mapper registry. This registry knows which mapper to use for which type pair. When a request comes in, it finds the correct mapper and executes the conversion. This registry can be filled manually or made more dynamic via reflection or dependency injection. For example, a basic usage might look like this:
// Our mapper registry (a simple dictionary) 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}"); } }
// Example usage: var registry = new MapperRegistry(); registry.Register(new MyDateMapper()); // Register our mapper
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}"); // Output: MyDate: 1990-5-15
var newMyDate = new MyDate { Year = 2000, Month = 12, Day = 25 }; var newDbDateTime = registry.Map<MyDate, DateTimeModel>(newMyDate); // Reverse transformation
Console.WriteLine($"DateTimeModel: {newDbDateTime.Value}"); // Output: DateTimeModel: 25.12.2000 00:00:00