How to use Mapster with .NET 7 - A Quick Introduction
Learn how to use Mapster with C# and .NET in this short but explanatory tutorial.
β Christian Schou
Are you also facing a repetitive task, where you have to map all properties on a model to another over and over again? In today's software development, we often have to map objects across layers in our applications and that can be quite a big task when the application is growing.
Here is an example, and I am sure you know the struggle. π
public IActionResult CreateBook(BookDto bookDto)
{
Book book = new()
{
Title = dto.Title,
Author = dto.Author,
...
...
...
Pages = dto.Pages,
ISBN = dto.ISBN,
...
...
Publisher = dto.Publisher
};
...
return Ok("Book was created");
}
I have helped out customers, etc... fixing this issue, but one thing that always comes to my mind β Why would someone implement this manual mapping in the first place? It's a boring task, and time-consuming and you have to do it all places where you are working with the data transfer objects (DTOs) and domain models.
Instead, we can use something like Mapster or AutoMapper. Both are mapping libraries we can use to make this mapping happen. They are fast and we only have to maintain the mapping configuration/profile in one place.
In this quick introduction to Mapster, I will show you the essentials and how you can use Mapster to map between objects in your application blazingly fast. π₯ By the end of this tutorial you will find a link to the full repository and you will be able to make your own custom mappings using Mapster.
Requirements
To follow along in this tutorial you should be familiar with C# and .NET. You do not have to be an advanced programmer, but I recommend that you have a basic understanding of objects, etc... Also you should have an IDE or code editor.
Step 1 - Create A New Web API With .NET
If you are implementing Mapster in a current project, you don't have to perform this step. I will start from scratch and create a new .NET Core Web API running on .NET 7.
Give the project a name you prefer, and create it. You should now have a blank Web API project like mine below.
Awesome! Let's get to the fun part and write some code. βοΈ Before you start implementing anything, you can delete the WeatherForecast controller and model.
Step 2 - Create The Book Models
Before we create the DTO for our Book
I would like to have the Book
model in place. At the root of your project, right-click and create a new folder named Models.
Inside the Models folder, create a new class and name it Book
. This class holds all the details for our book in the system. Below is the implementation including a override
method for ToString()
on the Book
class.
using System.ComponentModel.DataAnnotations;
namespace TwcMapster.Models
{
public class Book
{
[Key]
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public int ReleaseYear { get; set; }
public string Publisher { get; set; } = string.Empty;
public double Price { get; set; }
public string Currency { get; set; } = "USD";
public int CategoryId { get; set; }
public Category Category { get; set; }
// Method to print the book details
public override string ToString()
{
return "The details of the book are: " + Title + ", " + Author + ", " + ReleaseYear + ", " + Publisher + ", " + Price;
}
}
}
Now create a new class and name it Category
and add the following code inside that class.
using System.ComponentModel.DataAnnotations;
namespace TwcMapster.Models
{
public class Category
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Book> Books { get; set; }
}
}
Perfect! Now we got the models in place. You can also see them at the link below if you need further details.
In the next section, we will install Mapster in our Web API project.
Step 3 - Install Mapster In The Project
It's fairly straightforward to add/install a new package/NuGet in .NET/C# projects. Open your Package Manager Console and type the following. Hit ENTER when you are done.
PM> Install-Package Mapster
A Quick Introduction to Mapster
If you would like to make from one object to a new one, this is how you do it. π¨βπ»
var destinationObject = sourceObject.Adapt<Destination>();
If you would like to map from one object to an existing one, you can do the following.
sourceObject.Adapt(destinationObject);
If you would like to map entities from your database or simply a list of data, you can do so by calling the ProjectToType<TDestination>.ToList()
. See my example below.
List<Destination> projectedItems = data.Books.ProjectToType<Destination>().ToList();
With that in place, let's move on and create a base DTO that will provide the methods we need when making custom mapping, etc... π
Step 4 - Create A Base DTO
Create a new folder named DTOs at the root of your project, and add a new BaseDto
class inside that folder. The BaseDto class has the following logic. I will explain below.
using Mapster;
namespace TwcMapster.DTOs
{
/// <summary>
/// Base Data Transfer Object for model mapping.
/// </summary>
/// <typeparam name="TDto">The type of the DTO.</typeparam>
/// <typeparam name="TModel">The type of the Model.</typeparam>
public abstract class BaseDto<TDto, TModel> : IRegister
where TDto : class, new()
where TModel : class, new()
{
/// <summary>
/// Configuration of type adapter.
/// </summary>
private TypeAdapterConfig Config { get; set; }
/// <summary>
/// Adds custom mappings to the configuration.
/// </summary>
public virtual void AddCustomMappings() { }
/// <summary>
/// Sets custom mappings for dto to model.
/// </summary>
/// <returns>The type adapter setter.</returns>
protected TypeAdapterSetter<TDto, TModel> SetCustomMappings() => Config.ForType<TDto, TModel>();
/// <summary>
/// Sets custom mappings for model to dto.
/// </summary>
/// <returns>The type adapter setter.</returns>
protected TypeAdapterSetter<TModel, TDto> SetCustomMappingsReverse() => Config.ForType<TModel, TDto>();
/// <summary>
/// Registers the type adapter configuration and adds custom mappings.
/// </summary>
/// <param name="config">The configuration of the type adapter.</param>
public void Register(TypeAdapterConfig config)
{
Config = config;
AddCustomMappings();
}
/// <summary>
/// Maps DTO to an existing Model.
/// </summary>
/// <returns>The Model.</returns>
public TModel ToModel()
{
return this.Adapt<TModel>();
}
/// <summary>
/// Maps DTO to an existing Model.
/// </summary>
/// <param name="model">The existing Model instance to be updated.</param>
/// <returns>The updated Model instance.</returns>
public TModel ToModel(TModel model)
{
return (this as TDto).Adapt(model);
}
/// <summary>
/// Maps a Model to a DTO.
/// </summary>
/// <param name="model">The Model to be mapped.</param>
/// <returns>The DTO.</returns>
public static TDto FromModel(TModel model)
{
return model.Adapt<TDto>();
}
}
}
WhatΒ΄s happening in the code above? π€
Class Declaration
public abstract class BaseDto<TDto, TModel> : IRegister
This declares an abstract class named BaseDto
which takes two generic types: TDto
and TModel
. The class also implements the IRegister
interface, which is a part of Mapster for configuration registration.
Type Constraints
where TDto : class, new()
- This constraint ensures that the generic typeTDto
is a reference type (class
) and has a parameterless constructor (new()
).where TModel : class, new()
- Similarly, this constraint ensures thatTModel
is a reference type with a parameterless constructor.
Properties
private TypeAdapterConfig Config { get; set; }
- This is a private property that holds the configuration for type mapping. This configuration is used by Mapster for mapping operations.
Methods
AddCustomMappings()
- An overridable method for adding custom mappings. In derived classes, you can provide specific implementation to define how certain properties map betweenTDto
andTModel
.SetCustomMappings()
andSetCustomMappingsReverse()
- These protected methods are used to set custom mappings for DTO-to-Model and Model-to-DTO respectively. They use theConfig
property to specify the source and destination types.Register(TypeAdapterConfig config)
- A method to register a type adapter configuration and add custom mappings. This method assigns the passed configuration to theConfig
property and then callsAddCustomMappings()
.ToModel()
- Converts the current DTO instance to a model of typeTModel
.ToModel(TModel model)
- Maps the current DTO instance to an existing model, effectively updating the model with values from the DTO.FromModel(TModel model)
- A static method that converts a given model of typeTModel
to its equivalent DTO representation of typeTDto
.
Awesome! Let's use the BaseDto
on the BookDto
we will create in the next section.
Step 5 - Create The Book DTO
Now it's time to create our BookDto
. Do that in the same folder as the BaseDto
and add the following code inside. I will explain it below.
namespace TwcMapster.DTOs
{
/// <summary>
/// Book DTO
/// </summary>
public class BookDto : BaseDto<BookDto, Book>
{
/// <summary>
/// Book Title
/// </summary>
public string Title { get; set; }
/// <summary>
/// Book Author
/// </summary>
public string Author { get; set; }
/// <summary>
/// Book Release Year
/// </summary>
public int ReleaseYear { get; set; }
/// <summary>
/// Book Publisher
/// </summary>
public string Publisher { get; set; }
/// <summary>
/// Book Price. This is made up by the custom mapping.
/// </summary>
public string Price { get; set; }
/// <summary>
/// Category ID
/// </summary>
public int CategoryId { get; set; }
/// <summary>
/// Category Name
/// </summary>
public string CategoryName { get; set; }
/// <summary>
/// Add the custom mappings for the DTO.
/// The AddMapster in Program.cs will look for this in order to add the mappings.
/// </summary>
public override void AddCustomMappings()
{
// Mapster can map properties with different names
// Here we split the price into two properties for the model behind the DTO
SetCustomMappings()
.Map(dest => dest.Price,
src => Convert.ToDouble(src.Price.Split(' ', StringSplitOptions.None)[0]))
.Map(dest => dest.Currency,
src => src.Price.Split(' ', StringSplitOptions.None)[1]);
// Mapping from model to DTO
SetCustomMappingsReverse()
.Map(dest => dest.Title, src => src.Title)
.Map(dest => dest.Author, src => src.Author)
.Map(dest => dest.ReleaseYear, src => src.ReleaseYear)
.Map(dest => dest.Publisher, src => src.Publisher)
.Map(dest => dest.Price, src => $"{src.Price} {src.Currency}")
.Map(dest => dest.CategoryName, src => src.Category.Name);
}
}
}
What happens in the code above? π€
Class Declaration
public class BookDto : BaseDto<BookDto, Book>
- This declares a public class named BookDto
which inherits from the BaseDto
class we created before. The class specifies BookDto
as the DTO type and Book
as the model type.
Properties
- Each of the properties (
Title
,Author
,ReleaseYear
,Publisher
,Price
,CategoryId
, andCategoryName
) corresponds to a piece of data about a book. - The
Price
property is especially notable. This one is constructed through custom mapping and will hold values like "20 USD" (with both the amount and the currency).
Method Override - AddCustomMappings
This method is overridden from the base class and is designed to provide custom mappings between the BookDto
and Book
model.