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:

Example.cs
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:

Example.cs
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:

Example.cs
// 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:

Example.cs
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:

Example.cs
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:

Example.cs
[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:

Example.cs
[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:

Choose Mapster if:

Choose neither if:

A Word on Manual Mapping

Both libraries exist to reduce boilerplate, but sometimes a simple extension method is clearer:

Example.cs
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.