As we know, there are 2 Common Approaches to Model/DTO Mapping
- Manual mapping — the developer fully manages/controls the process of data transfer between models
- Auto-mapping — the developer uses a ready-made library that does the mapping automatically according to the developer’s instructions
Drawbacks of Manual Mapping
- Time spent on mapping essentially identical fields in models
- A lot of control is needed everywhere to check what you forgot to map — the compiler doesn’t prompt, you need to cover such cases with tests. You can probably use generation feature of IDE to create mapping between two models but the adding to/editing of existing mappings requires a lot of attentiveness
Issues with AutoMapper
For the second approach in .NET, the most popular library, AutoMapper, is usually used. It seems fine, you don’t have to think about field control as with manual mapping, you add to both models and it’s automatically mapped. But not everything is good here either:
- I’m not shown where my fields in the model are used, because the mapping happens in the depths of AutoMapper, so we learn about problems at runtime or by calling AssertConfigurationIsValid at application startup, or in unit tests
- I need to inject AutoMapper into services (classes) using the IMapper interface. And still, to find all the places in the code where my models are mapped, I need to search in the IDE for something like Map
<SomeDto>
or Map<List<SomeDto>
>, etc. - With AutoMapper, bugs with mapping were observed on projects directly in environments, especially bugs related to nullable fields. If nullable fields weren’t handled properly somewhere, that’s it. Example: requested the first 100 elements by pagination — it works, but on the next page — an error (c# object reference not set to an instance).
As communication with colleagues on projects shows, distrust in the mapping library is the reason for continuing to use manual mapping in projects.
Introducing Mapperly
There is a better alternative — a mapping library based on source generators And the name of this library is Mapperly (https://github.com/riok/mapperly)
This library solves the problems of AutoMapper and doesn’t exclude the use of manual mapping if you really want to :)
Advantages of Mapperly:
- Ease of use
- Ability to find where a field is used in mapping, thanks to source generators
- Faster performance, again, thanks to source generators
- You can combine both auto-mapping and manual mapping within one model
- More intelligently able to map types automatically
- No need to inject anything into classes
Setting Up Mapperly
- Install the Riok.Mapperly package
dotnet add package Riok.Mapperly
2. Create a class (preferably static) and set the [Mapper] attribute on it
3. We write a method as an extension to our model:
4. We specify with attributes additionally what needs to be mapped if the fields don’t match by name
In AutoMapper, the same would look like this:
Notice, I had to help AutoMapper to map datetime->dateonly, but Mapperly can do this. Let’s look at the generated code:
Now you can go to the method implementation in the code through the Go to Declaration or Usages call, or go through the solution explorer as in the screenshot on the left.
And most importantly, what we got, we can now see where our fields are used in both models
And we also have the ability to find where this method is used in the code:
Let’s now try to consider more custom scenarios:
- Ignore a specific field:
2. Apply custom mapping to a separate field:
IMPORTANT: MapToVisitsSummary uses => lambda. This is important for the mapping translation to work in a DB query. Always use lambda in Mapperly, not a return statement.
In AutoMapper, we would implement the example like this:
3. And how does Projection work there, you ask? It works fine, but you need to write one more method, which, by the way, is easier to track later:
This code is generated:
on line 49, due to that => lambda, the linq is built correctly
4. Let’s try to combine auto-mapping with manual mapping
Generated code:
5. Mapping using base models
Suppose we introduce a base model for entity: two matching fields Id and UserId + isDeleted field
And we introduce a base model for Dto with a custom Status field
We inherited our test model from these base ones:
And now we want to make the mapping continue to work. In AutoMapper, we need to give instructions on the base model:
In Mapperly, this case looks more complicated, in this aspect it is inferior to AutoMapper:
You have to specify for each inherited type how to map the custom Status field in BaseDto. The Id and UserId fields mapped without problems. For now, it works like this, there’s an open ticket in the library’s GitHub repository to improve work with derived types. Despite this drawback, it’s easier to keep track of unmapped fields in Mapperly than in AutoMapper, and even easier than with manual mapping
This is achieved by adding lines to the .editorconfig file of the project:
[*.cs]
dotnet_diagnostic.RMG012.severity = error # Unmapped target member
Now, if I add a new field in Dto with a name or type that Mapperly can’t map on the fly, I’ll get an error at the project compilation stage:
Actually, this feature will allow you not to have problems with the fact that you didn’t keep track of mapping somewhere when adding new fields to the model.
That’s all:) I hope the information was useful, and you will try to use it in your projects!
P.S. Code from this article here:
ifmelate/MappingDemo: .NET: Testing Mapperly vs Automapper (github.com)