Chapter 13: Getting Started with Object Mappers

In this chapter, we'll explore object mapping. As we saw in the previous chapter, working with layers often leads to copying models from one layer to another. Object mappers solve that problem. We will first take a look at manually implementing an object mapper. Then, we'll improve our design. Finally, I will introduce you to an open source tool that helps us generate business value instead of writing mapping code.

The following topics will be covered in this chapter:

  • Overview of object mapping
  • Implementing an object mapper, and exploring a few alternatives
  • Using the Service Locator pattern to create a service in front of our mappers
  • Using AutoMapper to map an object to another, replacing our homebrewed code

Overview of object mapping

What is object mapping? In a nutshell, it is the action of copying the value of an object's properties into the properties of another object. But sometimes, properties' names do not match; an object hierarchy may need to be flattened, and more. As we saw in the previous chapter, each layer can own its own model, which can be a good thing, but that comes at the price of copying objects from one layer to another. We can also share models between layers, but we will need some sort of mapping at some point. Even if it's just to map your models to DTOs or view models, it is almost inevitable, unless you are building a tiny application, but even then, you may want or need DTOs and view models.

Note

Remember that DTOs define your API's contract. Having independent contract classes should help you maintain a system, making you choose when to modify them. If you skip that part, each time you change your model, it automatically updates your endpoints' contracts, possibly breaking some clients. Moreover, if you input your model directly, a malicious user could try to bind the value of properties that he should not, leading to potential security issues.

In the previous projects, we instantiated the objects manually where the conversion happened, duplicating the mapping logic, and adding additional responsibilities to the class doing the mapping. To fix that issue, we are going to extract the mapping logic into other components.

Goal

The object mapper's goal is to copy the value of an object's properties into the properties of another object. It encapsulates the mapping logic away from where the mapping takes place. The mapper is also responsible for transforming the values from the original format to the destination format if both objects do not follow the same structure. We could want to flatten the object hierarchy, for example.

Design

We could design this in many ways, but we could represent the basic design with the following:

Figure 13.1 – Basic design of the object mapper

Figure 13.1 – Basic design of the object mapper

From 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 13.2 – Updating the design of the object pattern

Figure 13.2 – Updating the design of the object pattern

We can now implement the IMapper<TSource, TDestination> interface, and create one class per mapping rule or one class that implements multiple mapping rules. For example, we could implement the mapping of Type1 to Type2 and Type2 to Type1 in the same class.

We could also use the following design and create an IMapper interface with a single method that handles all of the application's mapping:

Figure 13.3 – Object mapping using a single IMapper as the entry point

The biggest advantage of that last design is the ease of use. We always inject a single IMapper instead of one IMapper<TSource, TDestination> per type of mapping, which should reduce the number of dependencies and the complexity of consuming such a mapper. The biggest downside could be the complexity of the implementation, but we can take that out of the equation, as we are about to discover.

You could implement object mapping using any way that your imagination allows, but the critical part to remember is that the mapper has the responsibility of mapping an object to another. A mapper should not do crazy stuff such as loading data from a database and whatnot. It should copy the values of one object into another; that's it.

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 last chapter, which contains both a data model and a domain model. I used that version to showcase the mapping of both data to domain and domain to DTO. If you don't need a data model, don't create one, especially using Clean Architecture. Nonetheless, the goal is to demonstrate the design's versatility and encapsulate the mapping of entities into mapper classes to extract that logic from the repositories and the controllers.

First, we need an interface that resides in the Core project so the other projects can implement the mapping that they need. Let's adopt the second design that we saw:

namespace Core.Interfaces

{

public interface IMapper<TSource, TDestination>

{

TDestination Map(TSource entity);

}

}

With that interface, we can start by creating the data mappers. Since we are mapping Data.Models.Product to a Core.Entities.Product and vice versa, I opted for a single class to implement both mappings:

namespace Infrastructure.Data.Mappers

{

public class ProductMapper : IMapper<Data.Models.Product, Core.Entities.Product>, IMapper<Core.Entities.Product, Data.Models.Product>

{

public Core.Entities.Product Map(Data.Models.Product entity)

{

return new Core.Entities.Product

{

Id = entity.Id,

Name = entity.Name,

QuantityInStock = entity.QuantityInStock

};

}

public Data.Models.Product Map(Core.Entities.Product entity)

{

return new Data.Models.Product

{

Id = entity.Id,

Name = entity.Name,

QuantityInStock = entity.QuantityInStock

};

}

}

}

These are simple methods that copy the value of an object's properties into the other. Nonetheless, now that we have done that, we are ready to update the ProductRepository class to use the new mapper:

namespace Infrastructure.Data.Repositories

{

public class ProductRepository : IProductRepository

{

private readonly ProductContext _db;

private readonly IMapper<Data.Models.Product, Core.Entities.Product> _dataToEntityMapper;

private readonly IMapper<Core.Entities.Product, Data.Models.Product> _entityToDataMapper;

public ProductRepository(ProductContext db, IMapper<Data.Models.Product, Core.Entities.Product> productMapper, IMapper<Core.Entities.Product, Data.Models.Product> entityToDataMapper)

{

_db = db ?? throw new ArgumentNullException(nameof(db));

_dataToEntityMapper = productMapper ?? throw new ArgumentNullException (nameof(productMapper));

_entityToDataMapper = entityToDataMapper ?? throw new ArgumentNullException (nameof(entityToDataMapper));

}

In the preceding code, we've injected the two mappers that we need. Even if we created a single class that implements both interfaces, the consumer (ProductRepository) does not know that. Next are the methods that use the mappers:

public IEnumerable<Core.Entities.Product> All()

{

return _db.Products.Select(p => _dataToEntityMapper.Map(p));

}

public void DeleteById(int productId)

{

var product = _db.Products.Find(productId);

_db.Products.Remove(product);

_db.SaveChanges();

}

public Core.Entities.Product FindById(int productId)

{

var product = _db.Products.Find(productId);

return _dataToEntityMapper.Map(product);

}

public void Insert(Core.Entities.Product product)

{

var data = _entityToDataMapper.Map(product);

_db.Products.Add(data);

_db.SaveChanges();

}

public void Update(Core.Entities.Product product)

{

var data = _db.Products.Find(product.Id);

data.Name = product.Name;

data.QuantityInStock = product.QuantityInStock;

_db.SaveChanges();

}

}

}

Then, the ProductRepository class uses the mappers to replace its copy logic (the preceding highlighted lines). That simplifies the repository, moving the mapping responsibility into mapper objects instead—one more step towards the Single Responsibility Principle (SRP – the "S" in SOLID).

The same principle was then applied to the web application, creating ProductMapper and StockMapper, and updating the controllers to use them. I omitted the code for brevity, but it is basically the same. Please look at the code on GitHub (https://net5.link/rZdN).

The only missing piece is in the composition root, where we bind the mapper implementations with the IMapper<TSource, TDestination> interface. The data bindings look like this:

services.AddSingleton<IMapper<Infrastructure.Data.Models.Product, Core.Entities.Product>, Infrastructure.Data.Mappers.ProductMapper>();

services.AddSingleton<IMapper<Core.Entities.Product, Infrastructure.Data.Models.Product>, Infrastructure.Data.Mappers.ProductMapper>();

Since ProductMapper implements both interfaces, we bind both to that class. That is one of the beauties of abstractions; ProductRepository asks for two mappers but receives the same instance of ProductMapper twice, without even knowing it.

Note

Yes, I did that on purpose. That proves that we can compose an application as we want it to be, without impacting the consumers. That is done by depending on abstractions instead of concretions, as per the dependency inversion principle. Moreover, the division into small interfaces, as per the Interface Segregation Principle (ISP – the "I" in SOLID), makes that kind of scenario possible. Finally, all of those pieces are put back together using the power of Dependency Injection (DI).

I hope you are starting to see what I have in mind, as we are putting more and more pieces together.

Code smell: Too many dependencies

Using that kind of mapping could become tedious in the long run, and we would rapidly see scenarios such as injecting three or more mappers into a single controller. That controller would most likely have other dependencies already, leading to four or more dependencies.

That should raise the following flag:

  • Does that class do too much and have too many responsibilities?

In that case, not really, but our fine-grained interface would be polluting our controllers with tons of dependencies on mappers, which is not ideal and makes our code harder to read.

If you are curious, here is how I came up with the number three:

  • Mapping from Entity to DTO (GetOne, GetAll, Insert, Update, and maybe Delete)
  • Mapping from DTO to Entity (Insert)
  • Mapping from DTO to Entity (Update)

As a rule of thumb, you want to limit the number of dependencies to three or less. Over that number, ask yourself if there is a problem with that class; does it have too many responsibilities? Having more than four dependencies is not inherently bad; it is just an indicator that you should reconsider some part of the design. If nothing is wrong, keep it at 4 or 5 or 10; it does not matter.

If you don't like to have that many dependencies, you could extract service aggregates that encapsulate two or more of those dependencies and inject that aggregate instead. Beware that moving your dependencies around does not fix anything; it just moves the problem elsewhere if there was a problem in the first place. Using aggregates could increase the readability of the code though.

Instead of blindly moving dependencies around, analyze the problem to see if you could create classes with actual logic that could do something useful to reduce the number of dependencies.

Pattern – Aggregate Services

Even if aggregate services is not a magic problem-solving pattern, it is a viable alternative to injecting tons of dependency into another class. Its goal is to aggregate many dependencies in another class to reduce the number of injected services in other classes, grouping dependencies together. The way to manage aggregates would be to group those by concerns or responsibility. Putting a bunch of exposed services in another service just for the sake of it is rarely the way to go; aim for cohesion.

Note

Creating one or more central aggregation services that expose other services can be a great way to implement service (interface) discovery in a project. I'm not telling you to put everything into an aggregate firsthand. However, if the discovery of services is a concern in your project or you and your team find it hard, that is a possibility worth investigating.

Here is an example of a hypothetical mapping aggregate to reduce the dependency of our imaginary CRUD controller:

public interface IProductMappers

{

IMapper<Product, ProductDetails> EntityToDto { get; }

IMapper<InsertProduct, Product> InsertDtoToEntity { get; }

IMapper<UpdateProduct, Product> UpdateDtoToEntity { get; }

}

public class ProductMappers : IProductMappers

{

public ProductMappers(IMapper<Product, ProductDetails> entityToDto, IMapper<InsertProduct, Product> insertDtoToEntity, IMapper<UpdateProduct, Product> updateDtoToEntity)

{

EntityToDto = entityToDto ?? throw new ArgumentNullException(nameof(entityToDto));

InsertDtoToEntity = insertDtoToEntity ?? throw new ArgumentNullException(nameof(insertDtoToEntity));

UpdateDtoToEntity = updateDtoToEntity ?? throw new ArgumentNullException(nameof(updateDtoToEntity));

}

public IMapper<Product, ProductDetails> EntityToDto { get; }

public IMapper<InsertProduct, Product> InsertDtoToEntity { get; }

public IMapper<UpdateProduct, Product> UpdateDtoToEntity { get; }

}

public class ProductsController : ControllerBase

{

private readonly IProductMappers _mapper;

// …

public ProductDetails Method()

{

var product = default(Product);

var dto = _mapper.EntityToDto.Map(product);

return dto;

}

}

From that example, the IProductMappers aggregate could make sense as it regroups all mappers used in the ProductsController class. It has the single responsibility of mapping ProductsController-related domain objects to DTOs and vice versa. You can create aggregates with anything, not just mappers. That's a fairly common pattern in DI-heavy applications.

Note

As long as an aggregate service is not likely to change and implements no logic, we could omit the interface and directly inject the concrete type. Since we are focusing heavily on the SOLID principles here, I decided to include the interface (which is not a bad thing in itself). One advantage of not having an interface is that using the concrete type could reduce the complexity of mocking the aggregate in unit tests. And as long as you don't try to put logic in there, I see no drawback.

Pattern – Mapping Façade

Instead of what we did in the previous case, we could create a mapping façade instead of an aggregate. The code consuming the façade is more elegant. The responsibility of the façade is the same as the aggregate, but it implements the interfaces instead of exposing properties.

Here is an example:

public interface IProductMapperService : IMapper<Product, ProductDetails>, IMapper<InsertProduct, Product>, IMapper<UpdateProduct, Product>

{

}

public class ProductMapperService : IProductMapperService

{

private readonly IMapper<Product, ProductDetails> _entityToDto;

private readonly IMapper<InsertProduct, Product> _insertDtoToEntity;

private readonly IMapper<UpdateProduct, Product> _updateDtoToEntity;

// ...

public ProductDetails Map(Product entity)

{

return _entityToDto.Map(entity);

}

public Product Map(InsertProduct dto)

{

return _insertDtoToEntity.Map(dto);

}

public Product Map(UpdateProduct dto)

{

return _updateDtoToEntity.Map(dto);

}

}

public class ProductsController : ControllerBase

{

private readonly IProductMapperService _mapper;

// ...

public ProductDetails Method()

{

var product = default(Product);

var dto = _mapper.Map(product);

return dto;

}

}

From the ProductsController standpoint, I always find it way cleaner to write _mapper.Map(…) instead of _mapper.SomeMapper.Map(…). The controller does not want to know what mapper is doing what mapping; the only thing it wants is to map what needs mapping.

Now that we've covered a few mapping options and explored the too many dependencies code smell, it is time to continue our journey into object mapping, with a "mapping façade on steroids."

Project – Mapping service

The goal is to simplify the implementation of the mapper façade with a universal interface.

We are going to use our third diagram to achieve that goal. Here's a reminder:

Figure 13.4 – Object mapping using a single IMapper interface

Figure 13.4 – Object mapping using a single IMapper interface

Instead of naming the interface IMapper, I found IMappingService to be more suitable because it is not mapping anything; it is a dispatcher, sending the mapping request to the right mapper. Let's take a look:

namespace Core.Interfaces

{

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 implementation ServiceLocatorMappingService:

namespace Web.Services

{

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 with 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.

Tip

I do not like the Service Locator pattern much in the application's code. The Service Locator is a code smell, and sometimes even worse, an anti-pattern. However, sometimes it can come in handy, as in this case. We are not trying to cheat dependency injection here; on the contrary, we leverage its power. Moreover, that service location needs to be done somewhere. Usually, I prefer to let the framework do it for me, but in this case, we explicitly did it, which was fine.

The use of 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. That would break the IoC principle.

In this case, we load mappers dynamically from the IoC container, which does, however, limit the container's ability to control what to inject a little, but it is acceptable enough for this type of implementation.

Now, we can inject that service everywhere that we need mapping, then use it directly. We already registered the mappers, so we just need to bind the IMappingService to its ServiceLocatorMappingService implementation and update the consumers.

If we look at this new version of the ProductRepository, we now have the following:

namespace Infrastructure.Data.Repositories

{

public class ProductRepository : IProductRepository

{

private readonly ProductContext _db;

private readonly IMappingService _mappingService;

public ProductRepository(ProductContext db, IMappingService mappingService)

{

_db = db ?? throw new ArgumentNullException(nameof(db));

_mappingService = mappingService ?? throw new ArgumentNullException(nameof(mappingService));

}

Here, unlike the last sample, we inject a single service instead of one per mapper:

public IEnumerable<Product> All()

{

return _db.Products.Select(p => _mappingService.Map<Models.Product, Product>(p));

}

// ...

public Product FindById(int productId)

{

var product = _db.Products.Find(productId);

return _mappingService.Map<Models.Product, Product>(product);

}

public void Insert(Product product)

{

var data = _mappingService.Map<Product, Models.Product>(product);

_db.Products.Add(data);

_db.SaveChanges();

}

//...

}

}

That's very similar to the previous sample, but we replaced the mappers with the new service (the highlighted lines). The last piece is the DI binding:

services.AddSingleton<IMappingService, ServiceLocatorMappingService>();

And that's it; we now have a universal mapping service that delegates the mapping to any mapper that we register with the IoC container.

Note

I used the singleton lifetime because ServiceLocatorMappingService has no state; it can be reused every time without any impact on the mapping logic.

The nicest part is that this is not the apogee of object mapping just yet. We have one tool to explore, named AutoMapper.

Project – AutoMapper

We just covered different ways to implement object mapping, but here we will leverage an open source tool named AutoMapper, which does it for us instead of us implementing our own.

Why bother learning all of that if there is a tool that already does it? There are a few reasons to do so:

  • It is important to understand the concepts; you don't always need a full-fledged tool like AutoMapper.
  • It gives us the chance to cover multiple patterns that we applied to the mappers that can also be applied elsewhere to any components with different responsibilities. So, all in all, you should have learned multiple new techniques during this object mapping progression.
  • Lastly, we dug deeper into applying the SOLID principles to write better programs.

This project is also a copy of the Clean Architecture sample. The biggest difference between this project and the others is that we don't need to define any interface because AutoMapper exposes an IMapper interface with all of the methods we need and more.

To install AutoMapper, you can load the AutoMapper NuGet package using the CLI (dotnet add package AutoMapper), Visual Studio's NuGet Package Manager, or by updating your .csproj manually.

The best way to define our mappers is by using AutoMapper's profile mechanism. A profile is a simple class that inherits from AutoMapper.Profile and that contains maps from one object to another. We can use a similar grouping to earlier, but without implementing interfaces.

Finally, instead of manually registering our profiles, we can scan one or more assemblies to load all of the profiles into AutoMapper by using the AutoMapper.Extensions.Microsoft.DependencyInjection package.

There is more to AutoMapper than this, but it has enough resources online, including the official documentation, to help you dig deeper into the tool.

In the Infrastructure project, we need to map Data.Models.Product to Entities.Product and vice versa. We can do that in a profile that we are naming ProductProfile:

namespace Infrastructure.Data.Mappers

{

public class ProductProfile : Profile

{

public ProductProfile()

{

CreateMap<Data.Models.Product, Core.Entities.Product>().ReverseMap();

}

}

}

A profile in AutoMapper is nothing but a class where you create maps in the constructor. The Profile class adds the required methods for you to do that, such as the CreateMap method. What does that do?

CreateMap<Data.Models.Product, Core.Entities.Product>() tells AutoMapper to register a mapper that maps Data.Models.Product to Core.Entities.Product. Then the ReverseMap() method tells AutoMapper to reverse that map, so from Core.Entities.Product to Data.Models.Product. That's all that we need for now because AutoMapper maps properties using conventions, and both of our classes have the same set of properties with the same names.

From the Web project perspective, we needed some mappers too, which I divided into the two following profiles:

namespace Web.Mappers

{

public class StocksProfile : Profile

{

public StocksProfile()

{

CreateMap<Product, StocksController.StockLevel>();

}

}

public class ProductProfile : Profile

{

public ProductProfile()

{

CreateMap<Product, ProductsController.ProductDetails>();

}

}

}

We could have merged them into just one, but I decided not to because I felt one profile per controller made sense, especially as the application grows.

Here is an example that illustrates multiple maps in a single profile:

namespace Web.Mappers

{

public class ProductProfile : Profile

{

public ProductProfile()

{

CreateMap<Product, ProductsController.ProductDetails>();

CreateMap<Product, StocksController.StockLevel>();

}

}

}

To scan for profiles from the composition root, we can use one of the AddAutoMapper extension methods to do that (from the AutoMapper.Extensions.Microsoft.DependencyInjection package):

services.AddAutoMapper(

GetType().Assembly,

typeof(Infrastructure.Data. Mappers.ProductProfile).Assembly

);

That method accepts a params Assembly[] assemblies argument, which means that we can pass an array or multiple Assembly instances to it.

The first Assembly is the Web assembly, acquired from the Startup class by calling GetType().Assembly (that code is in the Startup.ConfigureServices method). From there, AutoMapper should find the StocksProfile and the ProductProfile classes.

The second Assembly is the Infrastructure assembly, acquired using the Data.Mappers.ProductProfile class. It is important to note that any type from that assembly would have given us a reference on the Infrastructure assembly; there's no need to find a class inheriting from Profile. From there, AutoMapper should find the Data.Mappers.ProductProfile class.

The beauty of scanning for types like this is that once you register AutoMapper with the IoC container, you can add profiles in any of the registered assemblies, and they get loaded automatically; there's no need to do anything else afterward but to write useful code. Scanning assemblies also encourages composition by convention, making it easier to maintain in the long run. The downside of assembly scanning is that it can be hard to debug when something does not get registered. It can also be hard to find what registration module is doing something wrong.

Now that we've created the profiles and registered them with the IoC container, it is time to use AutoMapper. Let's take a look at the ProductRepository class:

namespace Infrastructure.Data.Repositories

{

public class ProductRepository : IProductRepository

{

private readonly ProductContext _db;

private readonly IMapper _mapper;

public ProductRepository(ProductContext db, IMapper mapper)

{

_db = db ?? throw new ArgumentNullException(nameof(db));

_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));

}

In the preceding code, we've injected AutoMapper's IMapper interface.

public IEnumerable<Product> All()

{

#if USE_PROJECT_TO

// Transposed to a Select() possibly optimal in some cases; previously known as "Queryable Extensions".

return _mapper.ProjectTo<Product>(_db.Products);

#else

// Manual Mapping (query the whole object, then map it; could lead to "over-querying" the database)

return _db.Products.Select(p => _mapper.Map<Product>(p));

#endif

}

The All method (preceding code block) exposes two ways of mapping collections that I describe later. Next, are the two other updated methods:

// ...

public Product FindById(int productId)

{

var product = _db.Products.Find(productId);

return _mapper.Map<Product>(product);

}

public void Insert(Product product)

{

var data = _mapper.Map<Models.Product>(product);

_db.Products.Add(data);

_db.SaveChanges();

}

// ...

}

}

As you can see, it is very similar to the other options; we inject an IMapper, then use it to map the entities. The only method that has a bit more code is the All() method. The reason is that I added two ways to map the collection.

The first way implies the ProjectTo<TDestination>() method that uses the IQueryable interface to limit the number of queried fields. In our case, that changes nothing because we need the whole entity. Using that method is recommended with EF.

The All() method should have been as simple as the following:

public IEnumerable<Product> All()

{

return _mapper.ProjectTo<Product>(_db.Products);

}

The second method uses the Map method directly, as we did with our implementations, and is used in the projection like this:

public IEnumerable<Product> All()

{

return _db.Products.Select(p => _mapper.Map<Product>(p));

}

All of the other cases are very straightforward and use the Map method of AutoMapper. The two controllers are doing the same, like this:

namespace Web.Controllers

{

[ApiController]

[Route("[controller]")]

public class ProductsController : ControllerBase

{

private readonly IProductRepository _productRepository;

private readonly IMapper _mapper;

public ProductsController(IProductRepository productRepository, IMapper mapper)

{

_productRepository = productRepository ?? throw new ArgumentNullException (nameof(productRepository));

_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));

}

[HttpGet]

public ActionResult<IEnumerable<ProductDetails>> Get()

{

var products = _productRepository

.All()

.Select(p => _mapper.Map<ProductDetails>(p));

return Ok(products);

}

// ...

}

Here is the StocksController using AutoMapper to maps domain entities to DTOs (highlighted lines):

[ApiController]

[Route("products/{productId}/")]

public class StocksController : ControllerBase

{

private readonly IMapper _mapper;

public StocksController(IMapper mapper)

{

_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));

}

[HttpPost("add-stocks")]

public ActionResult<StockLevel> Add(

int productId,

[FromBody] AddStocksCommand command,

[FromServices] AddStocks useCase

)

{

var product = useCase.Handle(productId, command.Amount);

var stockLevel = _mapper.Map<StockLevel>(product);

return Ok(stockLevel);

}

[HttpPost("remove-stocks")]

public ActionResult<StockLevel> Remove(

int productId,

[FromBody] RemoveStocksCommand command,

[FromServices] RemoveStocks useCase

)

{

try

{

var product = useCase.Handle(productId, command.Amount);

var stockLevel = _mapper.Map<StockLevel>(product);

return Ok(stockLevel);

}

catch (NotEnoughStockException ex)

{

return Conflict(new

{

ex.Message,

ex.AmountToRemove,

ex.QuantityInStock

});

}

}

// ...

}

}

The last detail that I'd like to add is that we can assert whether our mapper configurations are valid when the application starts. That will not point to missing mappers, but it validates that the registered ones are configured correctly. From the Startup class, you can write the following code:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IMapper mapper)

{

// ...

mapper.ConfigurationProvider.AssertConfigurationIsValid();

// ...

}

And this closes the AutoMapper project. At this point, you should begin to be familiar with object mapping.

I'd recommend that you evaluate whether AutoMapper is the right tool for the job whenever a project needs object mapping. If it is not, you can always load another tool or implement your own mapping logic.

Final note

AutoMapper is convention-based and does a lot on its own without any configuration from us, the developers. It is also configuration based, caching the conversions to improve performance. We can also create type converters, value resolvers, value converters, and more. AutoMapper keeps us away from writing that boring mapping code, and I have yet to find a better tool.

Summary

Object mapping is an avoidable reality in most cases. However, as we saw in this chapter, there are several ways of implementing object mapping, taking that responsibility away from the other components of our applications. One of those ways is AutoMapper, an open source tool that does that for us, offering us many options to configure the mapping of our objects.

Now let's see how object mapping can help us follow the SOLID principles:

  • S: It helps extract the mapping responsibility away from the other classes, encapsulating mapping logic into mapper objects or AutoMapper profiles.
  • O: By injecting mappers, we can change the mapping logic without changing the code of their consumers.
  • L: N/A.
  • I: We saw different ways of dividing mappers into smaller interfaces. AutoMapper is no different; it exposes the IMapper interface and uses other interfaces and implementations under the hood.
  • D: All of our code depends only on interfaces, moving the implementation's binding to the composition root.

Now that we are done with object mapping, in the next chapter, we'll explore the Mediator and CQRS patterns. Then we will combine our knowledge to learn about a new style of application-level architecture named Vertical Slice Architecture.

Questions

Let's take a look at a few practice questions:

  1. Is it true that injecting an Aggregation Service instead of multiple services makes our system better?
  2. Is it true that using mappers helps us extract responsibilities from consumers to mapper classes?
  3. Is it true that you should always use AutoMapper?
  4. When using AutoMapper, should you encapsulate your mapping code into profiles?
  5. How many dependencies should start to raise a flag telling you that you are injecting too many dependencies into a single class?

Further reading

Here are some links to build upon what we learned in the chapter:

  • If you want more object mapping, I wrote an article about that in 2017, titled Design Patterns: ASP.NET Core Web API, Services, and Repositories | Part 9: the NinjaMappingService and the Façade Pattern: https://net5.link/hxYf
  • AutoMapper official website: https://net5.link/5AUZ
  • AutoMapper Usage Guidelines is an excellent do/don't list to help you do the right thing with AutoMapper, written by the library's author: https://net5.link/tTKg
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.126.74