We can design object mappers in many ways. Here is the most basic object mapper design:

Figure 15.1: Basic design of the object mapper
In the diagram, the Consumer uses the IMapper interface to map an object of Type1 to an object of Type2. That’s not very reusable, but it illustrates the concept. By using the power of generics, we can upgrade that simple design to this more reusable version:

Figure 15.2: Generic object mapper design
This design lets us map any TSource to any TDestination by implementing the IMapper<TSource, TDestination> interface once per mapping rule. One class could also implement multiple mapping rules. For example, we could implement the mapping of Type1 to Type2 and Type2 to Type1 in the same class (a bidirectional mapper).Another way would be to use the following design and create an IMapper interface with a single method that handles all of the application’s mapping:

Figure 15.3: Object mapping using a single IMapper as the entry point
The biggest advantage of this last design is the ease of use. We always inject a single IMapper instead of one IMapper<TSource, TDestination> per type of mapping, which reduces the number of dependencies and the complexity of consuming such a mapper.You can implement object mapping in any way your imagination allows, but the critical part is that the mapper is responsible for mapping an object to another. A mapper should avoid complex processes, such as loading data from a database and whatnot. It should copy the values of one object into another: that’s it. Think about the Single Responsibility Principle (SRP) here: the class must have a single reason to change, and since it’s an object mapper, that reason should be object mapping.Let’s jump into some code to explore the designs in more depth with each project.
Project – Mapper
This project is an updated version of the Clean Architecture code from the previous chapter. The project aims to demonstrate the design’s versatility of encapsulating entity mapping logic into mapper classes, moving that logic away from the consumers. Of course, the project is again focused on the use case at hand, making learning the topics easier.First, we need an interface that resides in the Core project so the other projects can implement the mapping they need. Let’s adopt the second design that we saw:
namespace Core.Mappers;
public interface IMapper<TSource, TDestination>
{
TDestination Map(TSource entity);
}
With this interface, we can start by creating the data mappers. But first, let’s start by creating record classes instead of anonymous types to name the DTOs returned by the endpoints. Here are all the DTOs (from the Program.cs file):
// Input stock DTOs
public record class AddStocksCommand(int Amount);
public record class RemoveStocksCommand(int Amount);
// Output stock DTO
public record class StockLevel(int QuantityInStock);
// Output “read all products” DTO
public record class ProductDetails(int Id, string Name, int QuantityInStock);
// Output Exceptions DTO
public record class ProductNotFound(int ProductId, string Message);
public record class NotEnoughStock(int AmountToRemove, int QuantityInStock, string Message);
Three of the four output DTOs need mapping:
- Product to ProductDetails
- ProductNotFoundException to ProductNotFound
- NotEnoughStockException to NotEnoughStock
Why not map the StockLevel DTO? In our case, the StockService returns an int when we add or remove stocks, so converting a primitive value like an int into a StockLevel object does not require an object mapper. Moreover, creating such an object mapper adds no value and makes the code more complex. If the service had returned an object, creating a mapper that maps an object to StockLevel would have made more sense.
Let’s start with the product mapper (from the Program.cs file):
public class ProductMapper : IMapper<Product, ProductDetails>
{
public ProductDetails Map(Product entity)
=> new(entity.Id ??
default, entity.Name, entity.QuantityInStock);
}
Leave a Reply