Object mapping — copying data from one type to another — is tedious but unavoidable. You need to map entities to DTOs, API responses to view models, and commands to domain objects. AutoMapper has been the default choice for over a decade, but Mapster has emerged as a faster, lighter alternative. Here's how they compare.
AutoMapper: The Established Choice
AutoMapper uses reflection-based profile configuration. You define mapping profiles, register them, and inject IMapper:
public class OrderProfile : Profile
{
public OrderProfile()
{
CreateMap<Order, OrderDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer.FullName))
.ForMember(dest => dest.ItemCount,
opt => opt.MapFrom(src => src.Items.Count));
}
}
// Registration
builder.Services.AddAutoMapper(typeof(OrderProfile).Assembly);
// Usage
public class OrderService
{
private readonly IMapper _mapper;
public OrderService(IMapper mapper) => _mapper = mapper;
public OrderDto GetOrder(Order order)
=> _mapper.Map<OrderDto>(order);
}
AutoMapper also offers ProjectTo for EF Core, which translates mappings directly into SQL projections:
var dtos = await _context.Orders
.ProjectTo<OrderDto>(_mapper.ConfigurationProvider)
.ToListAsync();
Mapster: The Performance Contender
Mapster uses code generation (via compile-time or adaptive approaches) instead of pure reflection, making it significantly faster. The API is simpler too:
// No profile needed for convention-based mapping
var dto = order.Adapt<OrderDto>();
// Custom configuration
TypeAdapterConfig<Order, OrderDto>.NewConfig()
.Map(dest => dest.CustomerName, src => src.Customer.FullName)
.Map(dest => dest.ItemCount, src => src.Items.Count);
Mapster also supports DI-based usage through IMapper:
builder.Services.AddMapster();
// Configure mappings
var config = TypeAdapterConfig.GlobalSettings;
config.NewConfig<Order, OrderDto>()
.Map(dest => dest.CustomerName, src => src.Customer.FullName);
// Inject and use
public class OrderService
{
private readonly IMapper _mapper;
public OrderService(IMapper mapper) => _mapper = mapper;
public OrderDto GetOrder(Order order)
=> _mapper.Map<OrderDto>(order);
}
For EF Core projections, Mapster offers ProjectToType:
var dtos = await _context.Orders
.ProjectToType<OrderDto>()
.ToListAsync();
Performance
Mapster consistently outperforms AutoMapper in benchmarks. For simple mappings, Mapster is roughly 2-4x faster. For complex nested mappings, the gap narrows but Mapster still leads. Mapster achieves this by generating efficient mapping code at runtime rather than relying on reflection for every call.
That said, object mapping is rarely your bottleneck. If you're mapping thousands of objects per request, performance matters. If you're mapping a handful of DTOs, the difference is negligible.
Code Generation
Mapster offers a source generator (Mapster.Tool) that produces mapping code at compile time:
[AdaptTo("[name]Dto")]
public class Order
{
public int Id { get; set; }
public string CustomerEmail { get; set; } = string.Empty;
public decimal Total { get; set; }
}
Running the code generator produces a strongly-typed OrderDto class and mapping extensions. This eliminates all runtime reflection and makes mappings visible in your IDE.
Configuration Validation
AutoMapper has AssertConfigurationIsValid() which you can call in a test to verify all mappings are correctly configured:
[Fact]
public void All_mappings_should_be_valid()
{
var config = new MapperConfiguration(cfg =>
cfg.AddMaps(typeof(OrderProfile).Assembly));
config.AssertConfigurationIsValid();
}
Mapster has TypeAdapterConfig.GlobalSettings.Compile() which validates and pre-compiles all configurations, throwing if anything is misconfigured.
When to Choose Which
Choose AutoMapper if:
- Your team already uses it and has established profiles
- You rely heavily on
ProjectTowith EF Core - You want a large ecosystem of extensions and community knowledge
Choose Mapster if:
- Performance matters (high-throughput mapping scenarios)
- You prefer less configuration boilerplate
- You want compile-time code generation
- You're starting a new project with no existing mapper investment
Choose neither if:
- You have few mappings — manual mapping with a static method is often clearer and has zero dependencies
A Word on Manual Mapping
Both libraries exist to reduce boilerplate, but sometimes a simple extension method is clearer:
public static class OrderMappingExtensions
{
public static OrderDto ToDto(this Order order) => new()
{
Id = order.Id,
CustomerName = order.Customer.FullName,
ItemCount = order.Items.Count,
Total = order.Total
};
}
No configuration, no magic, no performance overhead, and fully refactor-safe. For small projects, this approach scales better than you might expect.