Updated on
Mapster is a convention-based object mapping library for .NET: one Adapt<TDestination>() call copies a Person into a PersonDto with zero configuration when property names match.
It is MIT-licensed with no conditions attached, and faster than AutoMapper in most scenarios. Since AutoMapper’s licence changed at version 15.0.0 in July 2025 — to a dual licence whose free paths are reciprocal source release or a four-condition Community tier — Mapster is the most common unconditionally free replacement.
In this guide, we map objects and collections, add custom rules, and generate mapping code at build time.
VIDEO: Using Mapster in ASP.NET Core Applications.
What Is Mapster?
Mapster is an open-source object-to-object mapping library for .NET that eliminates the boilerplate of copying data between types, typically from entities to DTOs and back. It is convention-based: when source and destination properties share names, source.Adapt<TDestination>() maps them with no configuration, including nested objects, collections, and primitive conversions. When conventions are not enough, TypeAdapterConfig adds per-member rules, conditions, and ignores through a small fluent API.
Mapster’s two practical advantages over AutoMapper are speed and licensing: it compiles mapping functions instead of interpreting configuration at runtime, which makes it faster in most scenarios and lets it generate the mapping source code at build time so we can read exactly what executes.
And it remains MIT-licensed, unconditionally. AutoMapper became dual-licensed at version 15.0.0 in July 2025: free either under the Reciprocal Public License 1.5, which asks us to publish our own source, or under a Community tier requiring all four of its conditions at once. Mapster asks for neither. The current Mapster release is 10.0.11.
Mapster publishes BenchmarkDotNet figures for that claim. Over 1,000,000 flat Person → PersonDTO maps, Mapster 10.0.8 runs in 6.849 ms against AutoMapper 14.0.0’s 29.645 ms — a ratio of 1.01 to 4.37, with both allocating the same 76.29 MB (Mapster README benchmark). Those are the maintainers’ own numbers on their own scenario, not ours, and our AutoMapper vs Mapster comparison weighs the two directly.
How Do We Use Mapster in C#?
Using Mapster takes three steps, and the first two are one line each. We install the package with dotnet add package Mapster, then map with the Adapt() extension method: var dto = person.Adapt<PersonDto>();. No registration, no profiles, and no container setup are required for the default conventions.
The third step is optional configuration: when property names differ or we need computed values, we declare rules once with TypeAdapterConfig<TSource, TDestination>.NewConfig().Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}"), and every subsequent Adapt() call for that type pair applies them.
For ASP.NET Core, we typically put those rules in an extension method called from Program.cs and register TypeAdapterConfig.GlobalSettings plus ServiceMapper if we prefer injecting an IMapper. Collections and nested objects need no extra work: the same conventions apply at every level. That is the entire surface most applications touch; everything else in this article refines these three steps.
The initial step is to install the Mapster NuGet package:
dotnet add 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.
A DTO like this is a plain data carrier — the difference between a DTO and a POCO is worth knowing when deciding what belongs on it — and these carriers are also a natural fit for records in C# when we want them immutable.
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:
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:
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:
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 Title, FirstName, 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:
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. Subtracting the birth year from the current year — the obvious approach — reports the wrong number before the person’s birthday and depends on the local clock, so we compute the age with a small, leap-safe helper instead.
The companion video above uses a simpler DateTime.Now.Year - DateOfBirth.Year calculation, which is off by a year before the person’s birthday — the leap-safe CalculateAge() helper below fixes that.
public static int CalculateAge(DateOnly birthDate, DateOnly today)
{
var age = today.Year - birthDate.Year;
if (birthDate > today.AddYears(-age))
{
age--;
}
return age;
}
That helper plugs into the Mapster config as the Age rule:
TypeAdapterConfig<Person, PersonShortInfoDto>
.NewConfig()
.Map(dest => dest.FullName, src => $"{src.Title} {src.FirstName} {src.LastName}")
.Map(dest => dest.Age,
src => CalculateAge(DateOnly.FromDateTime(src.DateOfBirth!.Value),
DateOnly.FromDateTime(DateTime.UtcNow)),
srcCond => srcCond.DateOfBirth.HasValue);
Here we add the third parameter to the Map() function, a Func delegate returning a boolean, so Mapster assigns the Age only when DateOfBirth has a value. A static helper keeps the rule an expression Mapster can compile, and makes the age logic unit-testable on its own.
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 => CalculateAge(DateOnly.FromDateTime(src.DateOfBirth!.Value),
DateOnly.FromDateTime(DateTime.UtcNow)),
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 FullAddress instead of throwing a NullReferenceException.
Calling our controller action now returns the person’s full address alongside the full name and age.
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:
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:
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:
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:
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 "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a "$(TargetDir)$(ProjectName).dll"" />
</Target>
Finally, let’s run the dotnet build command in our console to see the results in our project structure:
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.
A source-generator-first alternative is Mapperly, which emits all of its mapping code at compile time rather than compiling configuration at runtime as Mapster does, trading Mapster’s runtime flexibility for zero startup cost.
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:
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.
As a quick reference, here are the Mapster APIs we used throughout the article:
| Task | Mapster API |
|---|---|
| Map to a new object | source.Adapt<TDestination>() |
| Map onto an existing object | source.Adapt(destination) |
| Project an EF Core query | query.ProjectToType<TDestination>() |
| Custom rules per type pair | TypeAdapterConfig<TSrc, TDest>.NewConfig().Map(...) |
| Ignore a member | .Ignore(dest => dest.Prop) or [AdaptIgnore] |
| Map both directions | .TwoWays() |
| Generate mapping code at build | Mapster.Tool + ICodeGenerationRegister |
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. If we are coming from the other major library, our getting started with AutoMapper guide covers that side.
Tested with .NET 10.0.10 and Mapster 10.0.11.








