The preceding code is straightforward; the ProductMapper class implements the IMapper<Product, ProductDetails> interface. The Map method returns a ProductDetails instance. The highlighted code ensures the Id property is not null, which should not happen. We could also add a guard clause to ensure the Id property is not null.All in all, the Map method takes a Product as input and outputs a ProductDetails instance containing the same values.Then let’s continue with the exception mappers (from the Program.cs file):

public class ExceptionsMapper : IMapper<ProductNotFoundException, ProductNotFound>, IMapper<NotEnoughStockException, NotEnoughStock>
{
    public ProductNotFound Map(ProductNotFoundException exception)
        => new(exception.ProductId, exception.Message);
    public NotEnoughStock Map(NotEnoughStockException exception)
        => new(exception.AmountToRemove, exception.QuantityInStock, exception.Message);
}

Compared to the ProductMapper class, the ExceptionsMapper class implements the two remaining use cases by implementing the IMapper interface twice. The two Map methods handle mapping an exception to its DTO, leading to one class being responsible for mapping exceptions to DTOs.Let’s look at the products endpoint (original value from the clean-architecture project of Chapter 14, Layering and Clean Architecture):

app.MapGet(“/products”, async (
    IProductRepository productRepository,
    CancellationToken cancellationToken) =>
{
    var products = await productRepository.AllAsync(cancellationToken);
    return products.Select(p => new
    {
        p.Id,
        p.Name,
        p.QuantityInStock
    });
});

Before analyzing the code, let’s look at the updated version (from the Program.cs file):

app.MapGet(“/products”, async (
    IProductRepository productRepository,
    IMapper<Product, ProductDetails> mapper,
    CancellationToken cancellationToken) =>
{
    var products = await productRepository.AllAsync(cancellationToken);
    return products.Select(p => mapper.Map(p));
});

In the preceding code, the request delegate uses the mapper to replace the copy logic (the highlighted lines of the original code). That simplifies the handler, moving the mapping responsibility into mapper objects instead (highlighted in the preceding code)—one more step toward the SRP.Let’s skip the add stocks endpoint since it is very similar to the remove stocks endpoint but simpler, and let’s focus on the later (original value from the clean-architecture project of Chapter 14, Layering and Clean Architecture):

app.MapPost(“/products/{productId:int}/remove-stocks”, async (
    int productId,
    RemoveStocksCommand command,
    StockService stockService,
    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(new
        {
            ex.Message,
            ex.AmountToRemove,
            ex.QuantityInStock
        });
    }
    catch (ProductNotFoundException ex)
    {
        return Results.NotFound(new
        {
            ex.Message,
            productId,
        });
    }
});

Once again, before analyzing the code, let’s look at the updated version (from the Program.cs file):

app.MapPost(“/products/{productId:int}/remove-stocks”, async (
    int productId,
    RemoveStocksCommand command,
    StockService stockService,
    IMapper<ProductNotFoundException, ProductNotFound> notFoundMapper,
    IMapper<NotEnoughStockException, NotEnoughStock> notEnoughStockMapper,
    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(notEnoughStockMapper.Map(ex));
    }
    catch (ProductNotFoundException ex)
    {
        return Results.NotFound(notFoundMapper.Map(ex));
    }
});

The same thing happened for this request delegate, but we injected two mappers instead of just one. We moved the mapping logic from inline using an anonymous type to the mapper objects. Nevertheless, a code smell is emerging here; can you smell it? We will investigate this after we are done with this project; meanwhile, keep thinking about the number of injected dependencies.Now that the delegates depend on interfaces with object mappers encapsulating the mapping responsibility, we must configure the composition root and bind the mapper implementations to the IMapper<TSource, TDestination> interface. The service bindings look like this:

.AddSingleton<IMapper<Product, ProductDetails>, ProductMapper>()
.AddSingleton<IMapper<ProductNotFoundException, ProductNotFound>, ExceptionsMapper>()
.AddSingleton<IMapper<NotEnoughStockException, NotEnoughStock>, ExceptionsMapper>()

Since ExceptionsMapper implements two interfaces, we bind both to that class. That is one of the beauties of abstractions; the remove stocks delegate asks for two mappers but receives an instance of ExceptionsMapper twice without even knowing it.We could also register the classes so the same instance is injected twice, like this:

.AddSingleton<ExceptionsMapper>()
.AddSingleton<IMapper<ProductNotFoundException, ProductNotFound>, ExceptionsMapper>(sp => sp.GetRequiredService<ExceptionsMapper>())
.AddSingleton<IMapper<NotEnoughStockException, NotEnoughStock>, ExceptionsMapper>(sp => sp.GetRequiredService<ExceptionsMapper>())

Yes, I did that double registration of the same class on purpose. That proves we can compose an application as we want without impacting the consumers. That is done by depending on abstractions instead of implementations, as per the Dependency Inversion Principle (DIP—the “D” in SOLID). Moreover, the division into small interfaces, as per the Interface Segregation Principle (ISP—the “I” in SOLID), makes that kind of scenario possible. Finally, we can glue all those pieces together using the power of Dependency Injection (DI).