Skip to content

Custom Type Mapping: Opening a New Door in the Coding World

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 datetime in the database, but in our app, only year, month, and day mattered. Showing their age to the user or setting age limits for events was complicated by the time info. Once, I even mistakenly showed someone born near midnight as a full day older, my wife caught it immediately. My fault 🙂 Anyway, at that point, I started to research the concept of custom mapping more deeply. I created my own Date class and wrote bi-directional conversions between the database’s datetime and my Date class. Result? The code became cleaner, more understandable, and less prone to errors. Isn’t that great?

So, how do we do this? The logic is actually quite simple. Usually, you define an interface, maybe called ITypeMapper. This interface has two basic methods: one MapToTarget (convert from source to target) and the other MapToSource (convert from target to source). Then, you write classes implementing this interface. Each class handles the transformation between specific types.

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 MyDate class, and the other a generic DateTimeModel coming from the database. Let’s see how to match these.

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 MyDateMapper class implements the ITypeMapper interface and handles both conversions: from DateTimeModel to MyDate and vice versa. Keep in mind this example is quite simple. In real projects, you may want more detailed null checks, error logging, or other considerations.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.