How to implement a custom object mapper in C#

By FoxLearn 12/31/2024 9:45:51 AM   64
AutoMapper simplifies the process of object mapping, especially when dealing with objects that have similar structures.

However, it has limitations, particularly when handling complex data structures or incompatible types. In such cases, implementing a custom object mapper becomes necessary.

The Role of Object Mappers

Object mapping essentially means transferring data from one object (source) to another (destination).

What is a custom object mapper?

A custom object mapper allows developers to define specific mapping rules tailored to their application's needs.

Why do we use a custom object mapper?

Using custom mappers also provides greater control over performance, enabling optimizations that are difficult to achieve with third-party libraries.

Some common use cases for custom mappers include:

  • Handling incompatible data structures: When source and destination objects have mismatched properties or types, a custom mapper can bridge the gap.
  • Integration with external components: If external APIs or databases use different data models than the internal system, a custom mapper ensures smooth integration.
  • Versioning: As software evolves, changes in data models may necessitate remapping. A custom mapper can easily accommodate these shifts.
  • Performance optimization: Fine-tuning how data is mapped allows for reducing overhead and improving application speed.
  • Domain-specific rules: Custom mappers can implement specific transformations or business rules that go beyond simple property copying.

Custom Object Mapper in C#

Let’s consider a scenario where we need to map data between two objects, CustomerModel and CustomerDTO. The CustomerModel may be used internally within an application, while the CustomerDTO might be used to transfer data over a network or API.

CustomerModel (Internal Model)

public class CustomerModel
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
    public string Address { get; set; }
    public DateTime DateOfBirth { get; set; }
}

CustomerDTO (Data Transfer Object)

public class CustomerDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Address { get; set; }
    public string Age { get; set; }  // Derived from DateOfBirth in CustomerModel
}

Notice that CustomerModel includes a DateOfBirth field, while CustomerDTO has an Age field, which needs to be calculated during mapping. This is an ideal scenario where a custom object mapper would be useful.

Implementing a Custom Object Mapper

Here’s how we can implement a custom object mapper that handles the mapping, including custom logic for calculating the Age from the DateOfBirth field.

public class CustomObjectMapper
{
    public TDestination Map<TSource, TDestination>(TSource sourceObject)
    {
        var destinationObject = Activator.CreateInstance<TDestination>();
        if (sourceObject != null)
        {
            foreach (var sourceProperty in typeof(TSource).GetProperties())
            {
                var destinationProperty = typeof(TDestination).GetProperty(sourceProperty.Name);
                if (destinationProperty != null)
                {
                    // Handle special logic for Age field
                    if (destinationProperty.Name == "Age" && sourceProperty.Name == "DateOfBirth")
                    {
                        DateTime dateOfBirth = (DateTime)sourceProperty.GetValue(sourceObject);
                        int age = DateTime.Now.Year - dateOfBirth.Year;
                        if (DateTime.Now.DayOfYear < dateOfBirth.DayOfYear)
                            age--;
                        destinationProperty.SetValue(destinationObject, age.ToString());
                    }
                    else
                    {
                        destinationProperty.SetValue(destinationObject, sourceProperty.GetValue(sourceObject));
                    }
                }
            }
        }
        return destinationObject;
    }
}

In this CustomObjectMapper, we add special handling for the Age field in CustomerDTO, which is derived from DateOfBirth in CustomerModel.

Using the Custom Mapper in Action

Now, let’s use our custom object mapper to map a CustomerModel object to a CustomerDTO object.

public class CustomerController
{
    private readonly CustomObjectMapper _mapper;

    public CustomerController()
    {
        _mapper = new CustomObjectMapper();
    }

    public CustomerDTO GetCustomer(int id)
    {
        // Assuming GetCustomerInstance(id) fetches a CustomerModel object from the database
        var sourceCustomer = GetCustomerInstance(id);
        var customerDTO = _mapper.Map<CustomerModel, CustomerDTO>(sourceCustomer);
        return customerDTO;
    }
}

Example data

public CustomerModel GetCustomerInstance(int id)
{
    return new CustomerModel
    {
        Id = id,
        FullName = "John Doe",
        Email = "[email protected]",
        Address = "123 Elm Street",
        DateOfBirth = new DateTime(1990, 5, 15)  // Age will be calculated from this
    };
}

Custom Mapper in Action

Here’s what happens when we call GetCustomer in our controller:

var customerDTO = GetCustomer(1);
Console.WriteLine($"Id: {customerDTO.Id}, Name: {customerDTO.Name}, Age: {customerDTO.Age}, Email: {customerDTO.Email}, Address: {customerDTO.Address}");

Output:

Id: 1, Name: John Doe, Age: 34, Email: [email protected], Address: 123 Elm Street

Benefits of Using a Custom Mapper

  • Custom Logic: You can easily introduce special logic for fields like Age, which requires calculations based on other properties like DateOfBirth.
  • Separation of Concerns: The controller only interacts with the DTO, keeping business logic and data mapping in the repository or service layer.
  • Reusability: The custom mapper can be reused across the application wherever this mapping logic is required.

When to Use a Custom Mapper

While AutoMapper is great for straightforward property mappings, custom mappers are ideal when:

  • You need to apply additional logic (like calculating values or transforming data).
  • The source and destination structures differ significantly (e.g., different property names, types, or complex nested objects).
  • You need to optimize performance by controlling which properties are mapped.