How to implement a custom object mapper in C#

AutoMapper makes life easy, but has its limits. Learn how to implement a custom mapper to handle complex data structures or incompatible types.

data structure / data explosion / volume / velocity
Gremlin / Getty Images

When working on applications, you will often need to convert one type to another. Object mapping is the process of mapping a source object to a destination object, where the types of the source and destination objects might differ.

For example, you might need to create an instance of one class from an instance of another class, then copy the data from the source object to the destination object. Although there are many object mappers available to use, you may need to implement your own custom mappers in certain situations. In this article, we’ll see how we can implement a simple but fast object mapper in C#.

Create a console application project in Visual Studio

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2022 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Click Next.
  7. In the “Additional information” window, choose “.NET 8.0 (Long Term Support)” as the framework version you would like to use.
  8. Click Create.

We’ll use this .NET 8 console application project to implement a custom object mapper in the subsequent sections of this article.

AutoMapper benefits and limitations

The popular object mapping library AutoMapper is an excellent choice when you want to map objects of incompatible types having similar property names and structure. AutoMapper works nicely with Entity Framework, and writing unit tests for types that use AutoMapper is easy. One you have set up the mapping between the types, all you have to do is write a few lines of code to test the properties of the destination instance.

AutoMapper has several benefits that make it one of the most widely used mapping libraries around. Nevertheless, there are scenarios in which we should design our own custom object mappers in lieu of using any third-party object mapper. For example, while you can use AutoMapper when the destination type comprises a flattened subset of properties of the source type, AutoMapper is not well suited for complex layered architectures, or architectures that require complex mapping logic between objects of incompatible types.

Moreover, even if you use the latest versions of AutoMapper in your application, there are performance drawbacks you should be aware of. If you use a custom mapper, you will have more fine-grained control over the design, which means you can write code to optimize your custom object mapper.

What is a custom object mapper? Why do we need it?

A custom object mapper is a component used to map or transform a source object of one type to a destination object of another type. You can use a custom object mapper to handle complex mapping logic and tailor the mapping logic to cater to the needs of your application. Custom object mappers are useful in scenarios where you need to map objects of incompatible types, i.e., when the types have no inherent mapping relationship.

A custom object mapper makes code easier to read and maintain, and allows you to reuse mappings between projects. It reduces the processing time wasted on manual mappings, providing added flexibility to handle complicated scenarios with nested object collections. If you use a custom object mapper, you will be able to map data between complex and diverse data structures.

You can avoid writing unnecessary mapping logic by encapsulating it within a custom mapper and then reusing it throughout the application. When developing the models and entities that make up your application, it is recommended to employ lightweight data structures rather than hefty ones in order to achieve optimal speed.

Typical use cases for a custom object mapper

Here are a few reasons you might need to use a custom object mapper:

  • Support for mapping incompatible data structures: An object mapper bridges the gap between two classes or data structures that represent similar information but have different property names, types, or structures. You can take advantage of an object mapper to copy data from the source object to the destination object even if the objects possess similar or dissimilar names or data structures.
  • Support for integration with external components: When working with external APIs or databases, you might need to handle data models that are not aligned perfectly with your internal data models. You can use a custom object mapper to handle the mapping logic between different systems and data structures, primarily for types that have differing property names.
  • Support for versioning: Over time, your software will evolve in accordance with changes in business requirements. These requirements may include eradicating legacy types and models and introducing new types and models. You can easily handle the transformation to the new version of your models if you’re using a custom mapper instead of a third-party mapper such as AutoMappеr.
  • Performance optimization: A custom mapper allows you to write performance optimization code in your types and to control which properties are mapped, thereby reducing unnecessary overhead and improving performance. Additionally, you can write custom code to compress or decompress data, thereby reducing the network bandwidth and improving performance.
  • Domain-specific processing: A custom mapper also lets you write code for domain-specific rules and transformations in your types. For example, you might need to perform specific business operations, execute certain business rules, or perform data conversions that are specific to your application domain. In such cases, you can take advantage of a custom object mapper to incorporate code for domain-specific rules, data conversions, and transformations.

A custom object mapper example in C#

Let us now dig into a bit of code. Consider the following two classes.

public class AuthorModel
{
    public int Id
    {
        get; set;
    }
    public string FirstName
    {
        get; set;
    }
    public string LastName
    {
        get; set;
    }
    public string Address
    {
        get; set;
    }
}
public class AuthorDTO
{
    public int Id
    {
        get; set;
    }
    public string FirstName
    {
        get; set;
    }
    public string LastName
    {
        get; set;
    }
    public string Address
    {
        get; set;
    }
}

The following code listing implements a custom object mapper that maps a source object to a destination object.

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)
                {
                    destinationProperty.SetValue
                    (destinationObject,
                   sourceProperty.GetValue(sourceObject));
                }
            }
        }
        return destinationObject;
    }
}

You can use the following code snippet to use the custom mapper. Note how the generic Map method has been used to map an instance of type AuthorModel to an instance of AuthorDTO.

var source = new AuthorModel();
source.Id = 1;
source.FirstName = "Joydip";
source.LastName = "Kanjilal";
source.Address = "Hyderabad, India";
CustomObjectMapper mapper = new CustomObjectMapper();
var destination = mapper.Map<AuthorModel, AuthorDTO>(source);
Console.WriteLine("Id = {0}, First Name = {1} Last Name = {2} Address = {3}",
    destination.Id, destination.FirstName, destination.LastName, destination.Address);
Console.ReadLine();

Use a custom object mapper in action methods in C#

In this section, we’ll examine how to use our custom mapper in action methods. First create an interface called IAuthorRepository and enter the following code.

public interface IAuthorRepository
{
    public AuthorDTO GetAuthor(int id);
}

The AuthorRepository class will implement this interface. So, create a new class named AuthorRepository and write the following code in there.

public class AuthorRepository : IAuthorRepository
{
    private readonly CustomObjectMapper _customObjectMapper;
    public AuthorRepository(CustomObjectMapper customObjectMapper)
    {
        _customObjectMapper = customObjectMapper;
    }
    public AuthorDTO GetAuthor(int id)
    {
        var sourceObject = GetAuthorInstance(id);
        var destinationObject = _customObjectMapper.Map<AuthorModel, AuthorDTO>(sourceObject);
        return destinationObject;
    }
}

Now, create another class named AuthorController and replace the auto-generated code with the following code listing.

public class AuthorController : ControllerBase
{
    private readonly IAuthorRepository _authorRepository;
    public AuthorController(IAuthorRepository authorRepository)
    {
        _authorRepository = authorRepository;
    }
    public IActionResult GetAuthor(int id)
    {
        var author = _authorRepository.GetAuthor(id);
        //Write your code here based on specific business rules
        return Ok(author);
    }
}

Note that in both of these classes dependency injection has been used to inject dependencies in the constructors. The AuthorRepository class encapsulates all calls to the custom mapper. This is why we have not written any code to access or use the custom mapper in the AuthorController class.

The AuthorController class retrieves data from the database using an instance of the AuthorRepository, i.e., it doesn’t need to interact with the database directly. In other words, your controller’s action methods should not contain code to perform CRUD operations against the database directly.

Object mapping is not always easy

Using a custom object mapper allows you to create cleaner, maintainable code bases and streamline data access and data transfer. By taking advantage of the flexibility and efficiency of custom object mappers, you can build solutions that contain mapping logic tailored to your specific requirements.

That said, you can avoid using a custom object mapper if the mapping is trivial, because the mapping will be easier to do without it. The bottom line is you have many design patterns and approaches to choose from. Use AutoMapper when you can, and take advantage of custom mappers when you need to.

Copyright © 2024 IDG Communications, Inc.