The goal is to simplify the implementation of the Mapper façade with a universal interface. To achieve this, we are implementing the diagram shown in Figure 13.3. Here’s a reminder:

 Figure 15.4: Object mapping using a single IMapper interfaceFigure 15.4: Object mapping using a single IMapper interface 

Instead of naming the interface IMapper, we will use the name IMappingService. This name is more suitable because it is not mapping anything; it is a dispatcher servicing the mapping request to the right mapper. Let’s take a look:

namespace Core.Mappers;
public interface IMappingService
{
    TDestination Map<TSource, TDestination>(TSource entity);
}

That interface is self-explanatory; it maps any TSource to any TDestination.On the implementation side, we are leveraging the Service Locator pattern, so I called the class ServiceLocatorMappingService:

namespace Core.Mappers;
public class ServiceLocatorMappingService : IMappingService
{
    private readonly IServiceProvider _serviceProvider;
    public ServiceLocatorMappingService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider ??
throw new ArgumentNullException(nameof(serviceProvider));
    }
    public TDestination Map<TSource, TDestination>(TSource entity)
    {
        var mapper = _serviceProvider.GetService<IMapper<TSource, TDestination>>();
        if (mapper == null)
        {
            throw new MapperNotFoundException(typeof(TSource), typeof(TDestination));
        }
        return mapper.Map(entity);
    }
}

The logic is simple:

  • Find the appropriate IMapper<TSource, TDestination> service, then map the entity with it
  • If you don’t find any, throw a MapperNotFoundException

The key to that design is to register the mappers with the IoC container instead of the service itself. Then we use the mappers without knowing every single one of them, like in the previous example. The ServiceLocatorMappingService class doesn’t know any mappers; it just dynamically asks for one whenever needed.

The Service Locator pattern should not be part of the application’s code. However, it can be helpful at times. For example, we are not trying to cheat DI in this case. On the contrary, we are leveraging its power.

Using a service locator is wrong when acquiring dependencies in a way that removes the possibility of controlling the program’s composition from the composition root, which breaks the IoC principle.

In this case, we load mappers dynamically from the IoC container, limiting the container’s ability to control what to inject which is acceptable since it has little to no negative impact on the program’s maintainability, flexibility, and reliability. For example, we can replace the ServiceLocatorMappingService implementation with another class without affecting the IMappingService interface consumers.

Now, we can inject that service everywhere we need mapping and use it directly. We already registered the mappers, so we only need to bind the IMappingService to its ServiceLocatorMappingService implementation and update the consumers. Here’s the DI binding:

.AddSingleton<IMappingService, ServiceLocatorMappingService>();

If we look at the new implementation of the remove stocks endpoint, we can see we reduced the number of mapper dependencies to one:

app.MapPost(“/products/{productId:int}/remove-stocks”, async (
    int productId,
    RemoveStocksCommand command,
    StockService stockService,
    IMappingService mapper,
    CancellationToken cancellationToken) => {
    try
    {
        var quantityInStock = await stockService.RemoveStockAsync(productId, command.Amount, cancellationToken);
        var stockLevel = new StockLevel(quantityInStock);
        return Results.Ok(stockLevel);
    }
    catch (NotEnoughStockException ex)
    {
        return Results.Conflict(mapper.Map<NotEnoughStockException, NotEnoughStock>(ex));
    }
    catch (ProductNotFoundException ex)
    {
        return Results.NotFound(mapper.Map<ProductNotFoundException, ProductNotFound>(ex));
    }
});

The preceding code is similar to the previous sample, but we replaced the mappers with the new service (the highlighted lines). And that’s it; we now have a universal mapping service that delegates the mapping to any mapper we register with the IoC container.

Even if you are not likely to implement object mappers manually often, exploring and revisiting those patterns and a code smell is very good and will help you craft better software.

This is not the end of our object mapping exploration. We have two tools to explore, starting with AutoMapper, which does all the object mapping work for us.