Mapster in .NET 10
A practical, beginner-to-advanced guide to object mapping in modern .NET applications — from your first Adapt call to EF Core projection, dependency injection, testing, and code generation.
01. Introduction to Object Mapping
Most business applications represent the same information in different shapes. A database entity may contain persistence fields, a domain model may express business rules, and an API response should expose only the data a client needs. Object mapping is the process of copying and transforming values from one object type into another.
A mapper transforms object shape; it does not replace business validation or authorization.
For example, an Employee entity may contain an internal salary band, audit fields, and navigation properties. An EmployeeResponse DTO can expose only an identifier, display name, and department.
Example architecture: Angular frontend + ASP.NET Core Web API + Mapster + EF Core.
02. What Is Mapster?
Mapster is an open-source .NET object-mapping library. It maps objects using conventions and configurable rules. Its common API is the Adapt extension method, while TypeAdapterConfig controls mapping behavior.
Mapster also supports queryable projection and code-generation workflows. These capabilities are useful, but they solve different problems: runtime object mapping, query projection, and generated source code should be selected intentionally.
03. Why DTOs Matter
DTO means Data Transfer Object. It describes data crossing a boundary, such as an HTTP request or response. DTOs help prevent database implementation details from becoming public API contracts.
public sealed class Employee
{
public int Id { get; set; }
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public decimal Salary { get; set; }
public bool IsDeleted { get; set; }
}
public sealed record EmployeeResponse(
int Id,
string FullName
);
The response intentionally omits Salary and IsDeleted. Never rely on mapping configuration alone as a security boundary; expose only safe DTO members and enforce authorization separately.
04. How Mapster Works
At a high level, Mapster reads the source and destination types, applies conventions and configuration, builds a mapping expression, and uses the resulting mapping function. Mapping configuration should be created once and reused rather than rebuilt for every request.
The first mapping may involve expression construction or compilation depending on the path and configuration. Do not assume every mapping call has identical cost; benchmark the actual application scenario.
05. Mapster vs Manual Mapping vs AutoMapper
| Approach | Strength | Trade-off |
|---|---|---|
| Manual mapping | Explicit, easy to step through, no mapping dependency | Repetitive as models grow |
| Mapster | Concise convention mapping, configurable rules, projection and code generation options | Requires understanding configuration and generated expressions |
| AutoMapper | Established profile-based mapping ecosystem and broad adoption | Mapping behavior is abstracted behind configuration |
Choose based on team familiarity, mapping complexity, projection requirements, licensing and deployment constraints, and measured performance. A mapper is not automatically better than a few clear manual assignments.
06. Create a .NET 10 Project
Install the .NET 10 SDK and create a Web API project:
dotnet new webapi -n MapsterDemo
cd MapsterDemo
dotnet run
Use the SDK and target framework supported by your deployment environment. C# 14 features are available when compiling with the .NET 10 toolchain; the mapping examples below use straightforward syntax so the concepts remain clear.
07. Install Mapster
Install the stable Mapster package:
dotnet add package Mapster --version 10.0.14
Optional packages serve additional scenarios:
Mapster.DependencyInjection— inject services into mapping logic through Mapster's service mapper integration.Mapster.Tool— command-line code generation.Mapster.Core— lighter core package for scenarios that do not need the full configuration API.
Install only the packages your application needs. Confirm compatible package versions and target frameworks on NuGet.
08. Your First Mapping
When source and destination properties have matching names and compatible types, Mapster can map them by convention.
using Mapster;
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
public sealed class ProductResponse
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
Product product = new()
{
Id = 10,
Name = "Keyboard",
Price = 2499m
};
ProductResponse response = product.Adapt<ProductResponse>();
Adapt<T>() creates and returns a destination object. This is the most direct way to start using Mapster.
09. Mapping to an Existing Object
Use the overload that accepts a destination instance when you need to populate an existing object:
var response = new ProductResponse();
product.Adapt(response);
This is useful for controlled updates, but be deliberate about which properties may change. Mapping a request DTO directly into a tracked entity can overwrite fields the client should not control.
10. Convention-Based Mapping
Mapster conventionally matches destination members to source members by name and compatible type. For example, Id maps to Id and Name maps to Name. Different names, computed values, and security-sensitive members should be configured explicitly.
11. Explicit Mapping Configuration
Use TypeAdapterConfig when names differ or mapping behavior needs to be documented:
var config = new TypeAdapterConfig();
config.NewConfig<Product, ProductResponse>()
.Map(dest => dest.Name, src => src.Name);
ProductResponse dto = product.Adapt<ProductResponse>(config);
NewConfig creates/replaces the configuration for that type pair. ForType is useful when you want to extend an existing type-pair configuration rather than replace it.
12. Profiles and IRegister
Mapster does not use AutoMapper's Profile class as its central organization model. A common Mapster pattern is implementing IRegister and grouping rules by feature:
using Mapster;
public sealed class ProductMappingRegister : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Product, ProductResponse>()
.Map(d => d.DisplayName, s => s.Name);
}
}
// During application startup:
var config = new TypeAdapterConfig();
config.Scan(typeof(ProductMappingRegister).Assembly);
Scanning discovers registration classes in the selected assembly. Keep registrations deterministic and avoid scanning unrelated assemblies unnecessarily.
13. Custom Property Mapping
Use Map for renamed or calculated destination members:
config.NewConfig<Employee, EmployeeResponse>()
.Map(d => d.FullName,
s => s.FirstName + " " + s.LastName);
Keep mapping expressions focused on transformation. Business decisions such as whether an employee is allowed to view a field belong in application/domain logic, not hidden inside a mapping expression.
14. Ignoring Properties
Use Ignore to prevent a destination member from being populated:
config.NewConfig<EmployeeUpdateRequest, Employee>()
.Ignore(d => d.Id)
.Ignore(d => d.CreatedAt)
.Ignore(d => d.IsAdmin);
Ignoring fields is helpful, but allow-listing writable fields in a dedicated update DTO is safer than accepting a broad entity-shaped request.
15. Nested Objects and Flattening
Mapster can map nested objects when corresponding mappings and member shapes are compatible. It can also map nested source values into flattened destination members:
public sealed class Order
{
public int Id { get; set; }
public Customer Customer { get; set; } = new();
}
public sealed class OrderResponse
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
}
config.NewConfig<Order, OrderResponse>()
.Map(d => d.CustomerName, s => s.Customer.Name);
Nullability matters: if Customer can be null, define and test the expected behavior rather than assuming nested access is safe.
16. Collections and Child Objects
Mapster supports collection mapping. If element types have a mapping, a collection can be adapted to a collection of destination elements:
List<Product> products = GetProducts();
List<ProductResponse> responses =
products.Adapt<List<ProductResponse>>();
For large collections, consider allocation cost, pagination, and whether the data should be projected by the database instead of loading every entity first.
17. Reverse Mapping
You can configure both directions, but reverse mapping is not automatically safe for every application. A response DTO may contain read-only or calculated members that should never flow back into an entity.
config.NewConfig<Product, ProductResponse>()
.TwoWays();
For API writes, prefer a dedicated request type and an explicit mapping that ignores keys, audit fields, ownership, and other server-controlled values. Use bidirectional mapping only when both directions genuinely share the same rules.
18. Null Handling and Default Values
Define null behavior at the boundary. A nullable source member may need a nullable destination, a fallback value, or validation failure depending on the domain rule.
config.NewConfig<Customer, CustomerResponse>()
.Map(d => d.DisplayName,
s => s.Name ?? "Unknown");
Mapster also supports destination transforms for cross-cutting transformations such as trimming strings. Apply global transforms cautiously because they affect many mappings.
19. Conditional and Runtime Values
When a value is known only at runtime, Mapster's adapter builder can pass parameters through the mapping context:
config.NewConfig<Order, OrderResponse>()
.Map(d => d.RequestedBy,
s => (string)MapContext.Current.Parameters["user"]);
var dto = order.BuildAdapter()
.AddParameters("user", currentUserName)
.AdaptToType<OrderResponse>();
Use this pattern only when the value is genuinely mapping context. Avoid using it to conceal authorization or business workflow decisions.
20. Custom Conversion and Mapping Logic
For a conversion that cannot be expressed as a simple member rule, use a custom mapping expression or converter configuration. Keep converters deterministic and side-effect free wherever possible.
config.NewConfig<string, Money>()
.MapWith(value => Money.Parse(value));
Validate invalid input before mapping or ensure the conversion has a clear error contract. Mapping should not unexpectedly perform database calls or network requests.
21. Mapping to Existing EF Core Entities
When updating a tracked EF Core entity, map onto the existing instance rather than creating a second entity instance and attaching it blindly:
var entity = await db.Products
.SingleOrDefaultAsync(p => p.Id == id);
if (entity is null)
return Results.NotFound();
request.Adapt(entity);
await db.SaveChangesAsync();
Configure ignored members such as the primary key, tenant identifier, audit fields, and navigation properties. Load the entity through the correct authorization and tenant scope before applying changes.
22. Queryable Projection with ProjectToType
ProjectToType builds a query projection; successful SQL translation depends on the LINQ provider and expression support.
ProjectToType<T>() builds a projection expression for an IQueryable. With a supported LINQ provider such as EF Core, the provider may translate the projection into SQL so the application retrieves only the required columns.
var results = await db.Products
.Where(p => p.IsActive)
.ProjectToType<ProductResponse>()
.ToListAsync();
23. ASP.NET Core Dependency Injection
For applications that prefer an injectable mapper, install Mapster.DependencyInjection. Register a shared configuration and Mapster's service mapper:
var mapsterConfig = new TypeAdapterConfig();
mapsterConfig.Scan(typeof(Program).Assembly);
builder.Services.AddSingleton(mapsterConfig);
builder.Services.AddScoped<MapsterMapper.IMapper,
MapsterMapper.ServiceMapper>();
Then inject MapsterMapper.IMapper into the application service and call Map<TDestination>(source). Select a lifetime compatible with any services used by mapping logic. If mappings do not require injected services, static Adapt calls with a shared configuration may be sufficient.
24. Mapster in a Clean Architecture API
A practical feature-oriented structure could look like this:
src/
Api/
Endpoints/
Application/
Products/
CreateProduct/
GetProducts/
ProductMappingRegister.cs
DTOs/
Domain/
Products/
Infrastructure/
Persistence/
Keep DTOs and mapping rules near the application feature that owns the use case. Avoid letting infrastructure entities leak into public API contracts. Controllers or endpoints should orchestrate the request, not contain long mapping expressions.
25. Validation and Configuration Compilation
Mapster configuration supports validation-oriented settings such as requiring destination members to have source mappings and requiring explicit type-pair mappings. These settings help teams make mapping rules stricter.
var config = new TypeAdapterConfig
{
RequireDestinationMemberSource = true
};
// Register mappings before compiling.
config.Scan(typeof(Program).Assembly);
config.Compile();
Strict settings can expose legitimate computed or intentionally ignored members, so configure those rules explicitly. Run compilation/validation in automated tests or startup according to the application's needs.
26. Unit Testing Mapping Rules
Test mapping behavior as application behavior, especially when DTOs protect sensitive data or when custom rules are involved.
[Fact]
public void Product_maps_to_response()
{
var product = new Product
{
Id = 7,
Name = "Mouse",
Price = 899m
};
var dto = product.Adapt<ProductResponse>();
Assert.Equal(7, dto.Id);
Assert.Equal("Mouse", dto.Name);
Assert.Equal(899m, dto.Price);
}
Add tests for ignored members, null values, nested data, collection mapping, and EF Core projection. Projection tests should use the actual database provider when translation behavior matters.
27. Code Generation with Mapster.Tool
Mapster's code-generation tooling can generate DTOs, extension methods, or mapper implementations from configured models and interfaces. Generated code can make mapping logic visible to the compiler and debugger.
dotnet new tool-manifest
dotnet tool install Mapster.Tool
Generation commands and options depend on the selected workflow. Follow the current Mapster Tool documentation, commit generated source when appropriate for your build process, and inspect generated output rather than treating it as opaque magic.
Use runtime mapping for straightforward scenarios and evaluate code generation when compile-time visibility, trimming/AOT constraints, or measured performance requirements justify the extra build setup.
28. Performance and Best Practices
- Reuse a configured
TypeAdapterConfig; do not create a new global configuration per request. - Prefer clear conventions for simple mappings and explicit rules for important differences.
- Use
ProjectToTypewhen query projection is appropriate and supported by the provider. - Use pagination and database filtering before materializing large result sets.
- Keep mapping expressions deterministic and free from hidden I/O.
- Benchmark representative data sizes and warm/cold paths before making performance claims.
- Test security-sensitive DTO boundaries and update mappings.
Mapster is designed for fast mapping, but performance depends on configuration, object shape, query provider, allocations, and workload. Measure rather than relying on a generic benchmark headline.
29. Common Mistakes and Troubleshooting
| Problem | What to check |
|---|---|
| Destination member is empty | Check names, types, custom Map rules, and null source values. |
| Rule appears to be ignored | Check whether NewConfig replaced an earlier configuration; use ForType when extending. |
| EF Core projection fails | Check whether every expression is translatable by the provider. |
| Update overwrites protected fields | Use a narrow request DTO and explicitly ignore server-owned members. |
| Circular reference causes recursion | Review object graph design and Mapster reference-preservation / depth options. |
| Startup mapping error | Compile and validate registered mappings in a test or startup path to locate the failing pair. |
30. Practical Mini Project: Product API
Build a small API with a persistence entity, create/update request DTOs, and a response DTO. The objective is to practice mapping boundaries rather than to build a full production system.
- Create
Product,CreateProductRequest,UpdateProductRequest, andProductResponse. - Configure entity-to-response mapping, including any calculated display fields.
- Configure request-to-entity mapping while ignoring the key and audit fields.
- Use EF Core to load the existing product before applying an update.
- Use
ProjectToTypefor a paginated read endpoint if the provider can translate the projection. - Write unit tests for mapping rules and integration tests for database projection.
public sealed class ProductMappingRegister : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Product, ProductResponse>();
config.NewConfig<CreateProductRequest, Product>()
.Ignore(d => d.Id)
.Ignore(d => d.CreatedAt);
config.NewConfig<UpdateProductRequest, Product>()
.Ignore(d => d.Id)
.Ignore(d => d.CreatedAt);
}
}
Complete the project by reviewing which fields clients can read and write. Mapping rules should reinforce that contract, not define it on their own.
31. Interview Questions and Answers
1. What is Mapster?
Mapster is a .NET object-mapping library that maps source objects to destination objects using conventions and configurable rules.
2. What does Adapt<T> do?
It maps a source object to a new destination object of the requested type.
3. What is TypeAdapterConfig?
It stores mapping rules and options for source/destination type pairs and global mapping behavior.
4. What is the difference between NewConfig and ForType?
NewConfig creates/replaces a type-pair configuration; ForType retrieves or creates configuration while preserving and extending an existing one.
5. What is IRegister used for?
It provides a way to organize mapping registrations in classes and discover them through assembly scanning.
6. What is ProjectToType?
It builds a query projection from an IQueryable to a destination type, allowing a compatible provider to translate the projection.
7. When should mapping be manual?
When mapping is small, highly specific, security-sensitive, or clearer as explicit assignments.
8. Why should reverse mapping be used carefully?
Response models can contain calculated or server-controlled members that should not be accepted as input.
9. How do you map to an existing entity?
Load the tracked entity, validate the request and authorization, then map onto the existing instance while ignoring protected members.
10. How do you test Mapster configuration?
Write tests for type-pair rules, important fields, null handling, nested objects, and provider-backed projections.
32. Official Documentation and Further Learning
- Mapster official documentation
- Mapster GitHub repository
- Mapster GitHub Wiki
- Mapster on NuGet
- Mapster Tool overview
Use the official documentation and package release notes to verify API details when upgrading. Package APIs and recommended setup can evolve between major versions.
33. Final Thought
Mapster can reduce repetitive mapping code and provide a consistent way to translate between application models. The most important skill is not memorizing extension methods; it is designing clear DTO boundaries, configuring mappings deliberately, and testing the behavior that matters.
Start with convention-based mapping, introduce explicit configuration when the model shapes differ, and use query projection or code generation only when they solve a real requirement. Keep security, validation, and business rules in their proper application layers.
Skills Tutorials – Learn • Build • Grow Together