In this article, we’re going to learn how to use Mapster in ASP.NET Core applications.

First, we’ll learn what Mapster is and how we can install it inside the .NET Core application. Then, we are going to try different options for mapping data when using Mapster. After that, we’ll learn about Mapster functionalities for generating code and flattening objects inside our Web API project.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

What is Mapster?

As the name suggests, Mapster is a library that maps one object type to a different object type. It is a convention-based mapper that is easy to configure and use. Writing code to map one object to another can be very repetitive and boring. Because of this, Mapster frees us from writing error-prone boilerplate code.

In most cases, we shouldn’t try to reinvent the wheel but use the available options.

Let’s find out if Mapster is a good option.

How to Get Mapster?

The initial step is to install the Mapster NuGet package:

Install-Package Mapster

That’s how easy it is. Now we have Mapster ready to use in our project.

Prepare the Environment

Initially, let’s create two classes to represent the entities and one class for the DTO object to which we will map the values from the entities:

public class Person
{
    public string? Title { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public DateTime? DateOfBirth { get; set; }
    public Address? Address { get; set; }
}

Here, we create the Person class we will use throughout the examples. Also, we have the Address class inside it, so let’s add that one as well:

public class Address
{
    public string? Street { get; set; }
    public string? City { get; set; }
    public string? PostCode { get; set; }
    public string? Country { get; set; }
}

Now, we have our entities in place. It is time to create our first DTO class:

public class PersonDto
{
    public string? Title { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public DateTime? DateOfBirth { get; set; }
}

The PersonDto class has all of the properties from the original Person entity, except for Address which we will use later in the article. It’s important to note that all of our properties inside the DTO class are named the same as our entity properties, so we can map them without additional configuration.

Seeding Entity Data

To prepare our examples, we will create a simple class DemoData to create a simple Person entity and a Person collection. In a real-life project, we would get this data from a database.

Let’s add our first method for creating a person:

public static Person CreatePerson()
{
    return new Person()
    {
        Title = "Mr.",
        FirstName = "Peter",
        LastName = "Pan",
        DateOfBirth = new DateTime(2000, 1, 1),
        Address = new Address()
        {
            Country = "Neverland",
            PostCode = "123N",
            Street = "Funny Street 2",
            City = "Neverwood"
        },
    };
}

Here we create a new method called CreatePerson() that returns a Person object with its Address added.

Similarly, we add the CreatePeople() method that works the same as our first method but returns a collection of Person objects.

Basic Mapping With Mapster

It’s time to look at the basic usage of Mapster for mapping objects and collections on the fly without configuration.

First, let’s create a new static class named MappingFunctions, which we will use to implement all of the different mapping functionalities:

public static class MappingFunctions
{
    private static readonly Person _person = DemoData.CreatePerson();
    private static readonly ICollection<Person> _people = DemoData.CreatePeople();
}

Here we add two static read-only fields and initialize a single entity and a collection of Person entities. 

We have two different ways of mapping a single object to another using the generic Adapt() method:

  • Mapping to a new object
  • Mapping to an existing object

Mapping to a New Object

First, let’s create a new static method inside our MappingFunctions class:

public static PersonDto MapPersonToNewDto()
{
    var personDto = _person.Adapt<PersonDto>();

    return personDto;
}

In the MapPersonToNewDto() method, we initialize a new variable personDto as a destination to which we map all values from the _person object.

Secondly, let’s create a new controller in our project with a method to retrieve the data from the function:

[Route("api/people")]
[ApiController]
public class PeopleController: ControllerBase
{
    [HttpGet("new-person")]
    public IActionResult GetNewPerson()
    {
        var person = MappingFunctions.MapPersonToNewDto();
        return Ok(person);
    }
}

Here we implement the PeopleController class so we can consume our API actions. We add a new action called GetNewPerson() that uses the MapPersonToNewDto() method from our MappingFunctions class. Once we store the result in the person variable, we return the data with the OK status code.

Let’s test this with Postman:

mapster basic mapping

Perfect, our first mapping function works.

Mapping to an Existing Object

We can use another way of mapping when we want to map data to an existing object:

public static PersonDto MapPersonToExistingDto()
{
    var personDto = new PersonDto();
    _person.Adapt(personDto);

    return personDto;
}

Unlike the previous example, in this case, we create a new PersonDto object first. After that, we map the data from the _person object to the existing personDto variable.

We use the same Adapt() method as before, which is currently mapping data based on the property names without custom logic.

Let’s create a new action in our controller:

[HttpGet("existing-person")]
public IActionResult GetExistingPerson()
{
    var person = MappingFunctions.MapPersonToExistingDto();

    return Ok(person);
}

Here we add the GetExistingPerson() action together with a new URI specified on the action.

Similarly, let’s test this with Postman:

mapster mapping existing

Works like a charm.

Mapping Collections

Mapster library also provides extensions to map from Queryable in .NET. With that, we can speed up the process of mapping data we get from the database inside the Queryable data type.

Let’s create another method in our MappingFunctions class:

public static IQueryable<PersonDto> MapPersonQueryableToDtoQueryable()
{
    var peopleQueryable = _people.AsQueryable();
    var personDtos = peopleQueryable.ProjectToType<PersonDto>();

    return personDtos;
}

In the MapPersonQueryableToDtoQueryable() method, we store the data from the _people object into the peopleQueryable variable with the AsQueryable() method. In the next step, we use the ProjectToType() generic method from Mapster to map the data to the personDtos variable.

Again, let’s create a new endpoint in our controller to test the functionality:

[HttpGet]
public IActionResult GetPeopleQueryable()
{
    var people = MappingFunctions.MapPersonQueryableToDtoQueryable();

    return Ok(people);
}

Finally, let’s call the API from our Postman client to get the results:

mapster iqueryable

We can see all of our DTO objects with correct data.

Custom Mapping With Mapster

Now, we are going to implement functionalities with custom mapping to see the potential of the Mapster library.

Let’s tackle some of the challenges we might encounter when mapping data, shall we?

We need to add a custom Mapster configuration to implement custom mapping logic. For that, we will use the TypeAdapterConfig class.

First, let’s add a MapsterConfig static class with an extension method for Mapster configuration:

public static class MapsterConfig
{
    public static void RegisterMapsterConfiguration(this IServiceCollection services)
    {
    }
}

Then, we can use the RegisterMapsterConfiguration() extension method from our services in the Program class:

builder.Services.RegisterMapsterConfiguration();

Great, now we are ready to implement custom mapping with Mapster.

Mapping to Different Members

What if we have properties with different names inside our DTO classes? Well, Mapster has a solution for that.

Let’s add another DTO class which we will use to present information for our Person entity:

public class PersonShortInfoDto
{
    public string? FullName { get; set; }
}

In our PersonShortInfoDto class, we have only one property which we will use to store the full name of our Person entity, called FullName

After that, we add a new configuration to our RegisterMapsterConfiguration() extension method:

public static void RegisterMapsterConfiguration(this IServiceCollection services)
{
    TypeAdapterConfig<Person, PersonShortInfoDto>
        .NewConfig()
        .Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}");
}

We use the TypeAdapterConfig generic class with the Person class for the source and the PersonShortInfoDto class as our destination.

Then, we use the Map() method that accepts two Func delegates. Here we use string interpolation to map TitleFirstName, and LastName from our src object to the FullName field of the dest object.

Lastly, we need to call the Scan() method inside the RegisterMapsterConfiguration() method:

TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());

The Scan() method scans the assembly and adds the registration to the TypeAdapterConfig.

Now, we can implement a new method in our MappingFunctions class which we will later use from our controller:

public static PersonShortInfoDto CustomMapPersonToShortInfoDto()
{
    var personShortInfoDto = _person.Adapt<PersonShortInfoDto>();

    return personShortInfoDto;
}

Here we use the same functionality with the Adapt() method, but our custom mapping from the MapsterConfig class will define how we map the data.

Let’s add a controller action:

[HttpGet("short-person")]
public IActionResult GetShortPerson()
{
    var person = MappingFunctions.CustomMapPersonToShortInfoDto();

    return Ok(person);
}

And check the results:

mapping to different members

There is our new DTO with the full name.

Awesome.

Mapping Based on Condition

The Map() method from Mapster can accept a third parameter which we can use to set a condition based on the source object. In a case where the condition is not met, null or a default value will be assigned to the destination object.

Let’s add another property to our PersonShortInfoDto class:

public int? Age { get; set; }

With that, let’s modify our Mapster configuration to add a conditional mapping:

TypeAdapterConfig<Person, PersonShortInfoDto>
      .NewConfig()
      .Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}")
      .Map(dest => dest.Age,
            src => DateTime.Now.Year - src.DateOfBirth.Value.Year,
            srcCond => srcCond.DateOfBirth.HasValue);

Here we add the third parameter to the Map() function, which is a Func delegate that returns a boolean value. We assign a value to the Age property by subtracting the year of birth from today’s date year, but only if the DateOfBirth property has value.

We can now call our GetShortPerson() action from our controller again to see the result:

mapping on condition

There we go. We can see the age of our person.

Mapping Nested Members

Another example of custom mapping is the mapping of nested members. 

Let’s add a new property to our PersonShortInfoDto class:

public string? FullAddress { get; set; }

Now, we can modify the configuration to populate the FullAddress property of the destination class:

TypeAdapterConfig<Person, PersonShortInfoDto>
      .NewConfig()
      .Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}")
      .Map(dest => dest.Age,
            src => DateTime.Now.Year - src.DateOfBirth.Value.Year,
            srcCond => srcCond.DateOfBirth.HasValue)
      .Map(dest => dest.FullAddress, src => $"{src.Address.Street} {src.Address.PostCode} - {src.Address.City}");

Here we access the nested property through the Address property on our entity class. Based on the Address value, we construct the FullAddress with the string interpolation.

It is important to note that Mapster applies null propagation by default. From the above example, if the Address property is null on the src object, a null value will be assigned to the FullName instead of throwing a NullPointerException.

Let’s call our controller action and check the result from Postman:

mapping nested members

Success.

Ignoring Custom Members

By default, Mapster will map properties by their names without the configuration. We don’t need that in all use cases, so we can use the Ignore() method to ignore specific members:

TypeAdapterConfig<Person, PersonDto>
        .NewConfig()
        .Ignore(dest => dest.Title);

In this case, we ignore the Title property on our destination object, which is the PersonDto.

With that in place, let’s call our GetNewPerson() action and check the result again:

ignore custom members

We can see the difference from our previous call since the Title value is now null.

With Mapster, we can use the IgnoreNonMapped() method to ignore all members that are not explicitly set in the configuration. In addition to that, we can use the IgnoreIf() method with the condition based on the source or target object. When the condition is met, Mapster will skip the property.

Another way to ignore members with Mapster is to use the AdaptIgnore attribute on the class itself.

To implement that, we can add the annotation above our Title property:

[AdaptIgnore]
public string? Title { get; set; }

This will work in the same way as our rule-based Ignore() method from the previous example.

Two-Way Mapping

The two-way mapping in Mapster is a simple way to configure mapping in both ways, from source to destination and from the destination to the source object.

We can achieve this by calling a TwoWays() method in our configuration:

TypeAdapterConfig<Person, PersonDto>
        .NewConfig()
        .Ignore(dest => dest.Title)
        .TwoWays();

After that, let’s create a new method for returning our entity based on the DTO:

public static Person MapPersonDtoToPersonEntity()
{
    var personDto = MapPersonToNewDto();
    var person = personDto.Adapt<Person>();

    return person;
}

And add a new action to our controller:

[HttpGet("entity")]
public IActionResult GetPersonEntity()
{
    var person = MappingFunctions.MapPersonDtoToPersonEntity();

    return Ok(person);
}

Finally, we can call our action to confirm that two-way mapping is working:

two-way mapping

We see that all of the properties from the DTO are successfully mapped, with the title ignored.

Before and After Mapping

Mapster has more options for custom mapping, and it is essential to mention Before and After mapping functionalities. We can use the BeforeMapping() method to perform actions before the mapping operation.

To check this functionality, let’s add a new method to our PersonDto class:

public void SayHello()
{
    Console.WriteLine("Hello...");
}

Here we implement the SayHello() method that outputs some text. 

Now, let’s go to the MapsterConfig class and add a new configuration for PersonDto:

TypeAdapterConfig<Person, PersonDto>.ForType()
        .BeforeMapping((src, result) => result.SayHello());

We use the BeforeMapping() method from Mapster and then call the SayHello() method from our result variable.

Finally, let’s call our GetNewPerson() action again and check the console to see the output:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5002
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\GITHUB\CodeMaze\CodeMazeGuides\dotnet-client-libraries\HowToUseMapster\HowToUseMapster\
Hello...

We can see the output in our console.

Similarly, let’s now add another method to the PersonDto class:

public void SayGoodbye()
{
    Console.WriteLine("Goodbye...");
}

We will use the SayGoodbye() method to demonstrate how AfterMapping() method works in Mapster.

That said, let’s modify the MapsterConfig class:

TypeAdapterConfig<Person, PersonDto>.ForType()
        .BeforeMapping((src, result) => result.SayHello())
        .AfterMapping((src, result) => result.SayGoodbye());

And then check the console after calling the same action:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5002
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\GITHUB\CodeMaze\CodeMazeGuides\dotnet-client-libraries\HowToUseMapster\HowToUseMapster\
Hello...
Goodbye...

We can see the output from both methods.

Code Generation with Mapster

One of the problems we often mention when using mapping libraries is the potential regression that can occur when changing property names in our classes. To avoid that, it would be great to see the actual code that Mapster uses for mapping members.

Well, Mapster has a solution for that. We can use the code generation option to generate the source code of mapping functions.

First, let’s create a new configuration class and implement ICodeGenerationRegister interface from Mapster:

public class CodeGenerationConfig : ICodeGenerationRegister
{
    public void Register(Mapster.CodeGenerationConfig config)
    {
        config.AdaptTo("[name]Model")
            .ForType<Person>()
            .ForType<Address>();

        config.GenerateMapper("[name]Mapper")
            .ForType<Person>()
            .ForType<Address>();
    }
}

Here we automatically create two classes with the “Model” prefix based on the first AdaptTo() method. With this approach, we also can avoid writing DTO classes in our projects.

Then we use the GenerateMapper() method to dynamically create two classes with the mapping logic. By default, Mapster stores all new classes in the “Models” folder.

Now, let’s add the following code to our csproj file to generate classes on build:

<Target Name="Mapster" AfterTargets="AfterBuild">
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
</Target>

Finally, let’s run the dotnet build command in our console to see the results in our project structure:

code generation models

Fantastic, we have four new classes automatically generated from Mapster. Let’s check our PersonMapper class to see the actual mapping code:

public static PersonModel AdaptToModel(this Person p1)
{
    return p1 == null ? null : new PersonModel()
    {
        Title = p1.Title,
        FirstName = p1.FirstName,
        LastName = p1.LastName,
        DateOfBirth = p1.DateOfBirth,
        Address = p1.Address == null ? null : new AddressModel()
        {
            Street = p1.Address.Street,
            City = p1.Address.City,
            PostCode = p1.Address.PostCode,
            Country = p1.Address.Country
        }
    };
}
public static PersonModel AdaptTo(this Person p2, PersonModel p3)
{
    if (p2 == null)
    {
        return null;
    }
    PersonModel result = p3 ?? new PersonModel();
    
    result.Title = p2.Title;
    result.FirstName = p2.FirstName;
    result.LastName = p2.LastName;
    result.DateOfBirth = p2.DateOfBirth;
    result.Address = funcMain1(p2.Address, result.Address);
    return result;
    
}

We can see the logic for mapping functions AdaptToModel() and AdaptTo() that we can now use. With that in place, we can be sure that the compiler will report any potential errors we could have when modifying properties on our entities and models.

Another way to use Mapster’s code generation is through data annotations. We can use the AdaptTo attribute to achieve this:

[AdaptTo("[name]Model"), GenerateMapper]
public class Person

Flattening and Unflattening Complex Objects With Mapster

By default, Mapster will perform flattening for us. Let’s say that we have a Person entity that has the Address property with the Street property inside it. Now we can add a new property called AddressStreet to our PersonDto class. At that point, Mapster will perform the mapping without any additional configuration.

Let’s add the AddressStreet property to our PersonDto class:

public string? AddressStreet { get; set; }

Now, let’s call our GetNewPerson() action again without additional configuration and check the result:

Flattening and Unflattening with mapster

We can see the address street populated automatically based on the flattening.

However, we need to configure unflattening process explicitly. So, if we want to map to Address.Street from AddressStreet, we can call the Unflattening method from the TypeAdapterConfig class and set the first parameter to true.

Conclusion

In this article, we’ve learned how to use Mapster to perform basic mapping operations on our data in the ASP.NET Core project. We have seen how automatic code generation works with Mapster, and how we can flatten or unflatten our objects with a few steps.

The main conclusion is that Mapster is a very useful and simple library as far as mapping in the .NET ecosystem is concerned, so check it out.