Updated on

Mapster wins this comparison for most new projects: it is faster in almost every scenario we benchmark below, needs less configuration for common cases, and remains MIT-licensed with no conditions attached.

AutoMapper changed its licence at version 15.0.0 in July 2025 — not to “commercial”, but to a dual licence. We can take the Reciprocal Public License 1.5 and publish our own source under it, or take the commercial licence, which is free under a Community tier only if all four of its conditions hold at once. Version 14.0.0 and everything before it stays MIT permanently.

AutoMapper still earns its place in codebases that already use it heavily: it is mature, documented everywhere, and migration has a real cost.

Here is how they differ, feature by feature, with measurements.

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

Why Compare AutoMapper vs Mapster?

AutoMapper is one of the most popular object-object mapping libraries, with over 1.1 billion NuGet package downloads. It was first published in 2011 and its usage has grown ever since.

Mapster is an alternative to AutoMapper, first published in 2015, with over 76 million NuGet package downloads. Although its download count is nowhere near AutoMapper’s, it promises better performance and a lower memory footprint than other mapping libraries.

Based on the commit history of both AutoMapper and Mapster, we can see that both projects are actively maintained.

Apart from that, they both provide configuration options for simple to more advanced mapping scenarios.

The single biggest difference between the two today is licensing. From version 15.0.0 (July 2025), AutoMapper — together with MediatR, both maintained by Lucky Penny Software — is dual-licensed. We can accept the Reciprocal Public License 1.5, which is source-available and reciprocal, meaning we publish our own source under it and pay nothing, or we can take a commercial licence. Version 14.0.0 and everything before it stays MIT permanently, so pinning to [14.0.0, 15.0.0) keeps a fully permissive AutoMapper.

The commercial side does include a free Community tier, but only for individuals and organizations that meet all four of the vendor’s conditions at once: annual gross revenue (or non-profit budget) under $5 million; no more than $10 million in outside capital; not a government or quasi-government entity; and not a university using the library for institutional or operational software. If even one condition fails, the paid tiers apply — $499, $1,499, or $3,999 per year, per product, for the Standard, Professional, and Enterprise editions.

Mapster carries none of this. It stays MIT-licensed with no conditions, no revenue test, and no tiers, so it is free at any company size — which is the main reason it now heads most lists of AutoMapper alternatives.

If you’re interested to learn more about these libraries, check out our in-depth articles on AutoMapper and Mapster.

Let’s get started and find out how Mapster compares to AutoMapper for some of the most common object-to-object mapping scenarios.

Simple Type Mapping

In this case, both the source and the destination types have similar properties. The property names are always the same. However, we may use different data types for the properties. Our destination here is a DTO, and if you are unsure how that relates to a plain object, see the difference between a DTO and a POCO.

In order to demonstrate it, let’s create a simple source type User:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public bool IsActive { get; set; }
    public string Email { get; set; } = null!;
    public DateTime CreatedAt { get; set; }
}

And then, let’s create our destination type UserDto:

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public bool IsActive { get; set; }
    public string Email { get; set; } = null!;
    public string CreatedAt { get; set; } = null!;
}

Here, all the properties are similar to the source type except CreatedAt property. The CreatedAt property has string as the destination type whereas DateTime is the source type. In situations like this, the mapping libraries do the implicit type casting.

Both libraries support the type casting between the primitive types.

Simple Type Mapping With AutoMapper

To use AutoMapper, we first need to create the IMapper object. There are multiple ways to create it. One way is to use the MapperConfiguration class:

var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>(), NullLoggerFactory.Instance).CreateMapper();

Since version 15.0.0, the MapperConfiguration constructor requires an ILoggerFactory. Here we pass NullLoggerFactory.Instance from Microsoft.Extensions.Logging.Abstractions, while an ASP.NET Core app would supply the factory from dependency injection.

This way, we need to specify the source and the destination type as type params to the CreateMap<TSource, TDestination>() method. In our case, User is the source type and UserDto is the destination type.

The other way to do the mapper configuration is to use the profiles. We can learn more about this way in this article.

Before going ahead, let’s create our source object using the User type:

var source = new User
{
    Id = 1,
    Name = "User 1",
    Email = "[email protected]",
    IsActive = true,
    CreatedAt = DateTime.Now
};

Once we have the source object and the IMapper instance in place, we can now easily map to the destination object:

UserDto destination = mapper.Map<UserDto>(source);

In this case, we need to pass the source object as the parameter and specify the destination type as a generic type param to the Map<TDestination>(object source) method.

If we inspect the values of the destination variable, all the values of the properties in the source type are correctly mapped to the destination type properties:

{"Id":1,"Name":"User 1","IsActive":true,"Email":"[email protected]","CreatedAt":"01-09-2022 21:53:57"}

The CreatedAt property is also mapped by implicit type casting from DateTime to string.

Simple Type Mapping With Mapster

It’s even simpler in Mapster:

UserDto destination = source.Adapt<UserDto>();

We can directly invoke the Adapt<TDestination>(this object source) extension method on the source object where we must pass the destination type UserDto as the generic type parameter.

But, if we want to map to the existing destination object, we can do so:

var destination = new UserDto();
source.Adapt(destination);

Mapster provides other ways as well like query.ProjectToType<Dest>(), IMapper instance for dependency injection and a few others.

Now, if we inspect the values of the destination object, the mapping is as expected:

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

{"Id":1,"Name":"User 1","IsActive":true,"Email":"[email protected]","CreatedAt":"01-09-2022 21:53:57"}

Nested Type Mapping

In this case, we need to map nested objects. To illustrate this, let’s create one more type called Address:

public class Address
{
    public string AddressLine1 { get; set; } = null!;
    public string AddressLine2 { get; set; } = null!;
    public string City { get; set; } = null!;
    public string State { get; set; } = null!;
    public string Country { get; set; } = null!;
    public string ZipCode { get; set; } = null!;
}

Let’s use the Address type as one of the properties in User class:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string Email { get; set; } = null!;
    public Address Address { get; set; } = null!;
}

In nested or complex type mapping, the mapping libraries need to map all the types at any level in its way. In this instance, it needs to map both User and Address to their respective target types.

Let’s assume we have UserDto and AddressDto types as their destination.

Nested Type Mapping With AutoMapper

In AutoMapper, we need to create the mapping configuration for all the types to map to their destination types:

var mapper = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<User, UserDto>();
        cfg.CreateMap<Address, AddressDto>();
    }, NullLoggerFactory.Instance)
    .CreateMapper();

Here, we created the mapping for both User and Address to their destination types UserDto and AddressDto.

However, there’s no change in the actual mapping:

UserDto destination = mapper.Map<UserDto>(source);

This command will map both the first level (UserDto) and the second level inside the UserDto (AddressDto).

Nested Type Mapping With Mapster

In Mapster, since the source and the destination types have exactly the same properties then there’s no change and a simple command is sufficient:

UserDto destination = source.Adapt<UserDto>();

List or Array Mapping

In this scenario, we might want to map one list or array into another. To do this, we just need to specify the destination type param as List<TDestination>, TDestination[] or something similar. Other than that, we don’t need any special configuration for this to work.

List or Array Mapping With AutoMapper

In AutoMapper, we still need to specify the mapping configuration for the individual types:

var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>(), NullLoggerFactory.Instance).CreateMapper();
var sourceList = new List<User>() { ... };
List<UserDto> destinationList = mapper.Map<List<UserDto>>(sourceList);

If we look closely, we provided the destination type param as List<UserDto> to the Map<TDestination>() method.

List or Array Mapping With Mapster

In Mapster, we can directly invoke the Adapt() method while providing the destination type param as List<UserDto>:

List<UserDto> destinationList = sourceList.Adapt<List<UserDto>>();

Custom Property or Member Mapping

In this case, we want to map our custom properties in the source and the destination types.

Let’s say our destination type has FullName property:

public class UserDto
{
    public string FullName { get; set; } = null!;
}

But our source type only has FirstName and LastName property:

public class User
{
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
}

Now the mapping library will not know which property to map due to the name mismatch. Hence we need to explicitly tell the library of our custom mapping. Custom mapping also covers the opposite case — telling AutoMapper to ignore properties with AutoMapper or to ignore null values with AutoMapper so a destination member keeps its existing value.

Custom Property or Member Mapping With AutoMapper

In AutoMapper, we call the ForMember fluent method on the CreateMap() method to specify our indented mapping:

var mapper = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, UserDto>()
        .ForMember(
            dest => dest.FullName,
            config => config.MapFrom(src => $"{src.FirstName} {src.LastName}"
            ));
}, NullLoggerFactory.Instance);

Custom Property or Member Mapping With Mapster

In Mapster, we need to use the TypeAdapterConfig static class to do our custom property mapping:

TypeAdapterConfig<User, UserDto>
    .NewConfig()
    .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");

Here, we need to specify the source type as the first type param and the destination type as the second type param. Hence we say TypeAdapterConfig<User, UserDto>. The NewConfig() fluent method creates a new mapping configuration for the source and destination types while dropping any other configurations for those types.

Object Flattening

In object flattening, we can map the nested properties to the top-level properties using a simple naming convention.

For instance, the Address.ZipCode nested property from the User source type can be mapped to AddressZipCode property in the UserDto destination type:

public class User
{
    public Address Address { get; set; } = null!;
}

public class Address
{
    public string ZipCode { get; set; } = null!;
}

public class UserDto
{
    public string AddressZipCode { get; set; } = null!;
}

Another way is to have a method prefixed with Get followed by the destination property name. For instance, GetFullName() method will be mapped to FullName property:

public class User
{
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string GetFullName() => $"{FirstName} {LastName}";
}

public class UserDto
{
    public string FullName { get; set; } = null!;
}

We can use this feature in both AutoMapper and Mapster by default.

Reverse Mapping and Unflattening

The object unflattening is just the opposite. We can also call it reverse mapping because we do the mapping from the destination to the source object.

Reverse Mapping and Unflattening With AutoMapper

In AutoMapper, to achieve both reverse mapping and unflattening, we need to call the ReverseMap() method in the mapper configuration:

var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>().ReverseMap(), NullLoggerFactory.Instance).CreateMapper();

Reverse Mapping and Unflattening With Mapster

In Mapster, reverse mapping is termed as “two ways”. As the term suggests, we need to call the TwoWays() method in the type adapter config:

TypeAdapterConfig<User, UserDto>
    .NewConfig()
    .TwoWays()
    .Map(dest => dest.EmailAddress, src => src.Email);

This will do both reverse mapping and unflattening. Any mapping followed by the TwoWays() method will be used in both directions.

Attribute Mapping

So far, we saw the fluent configuration for different scenarios. We can use attributes as well to achieve the same functionality.

Both AutoMapper and Mapster provide attributes to do the custom mapping.

Attribute Mapping With AutoMapper

In AutoMapper, we can use attributes like AutoMap, Ignore, ReverseMap, SourceMember and so on:

public class User
{
    public DateTime CreatedAt { get; set; }
}

[AutoMap(typeof(User))]
public class UserDto
{
    [SourceMember("CreatedAt")]
    public string CreatedDate { get; set; } = null!;
}

Here we used the AutoMap attribute on the destination type to specify the source mapping type. And then, we used the SourceMember type to map to the source type’s property.

In order for this to work, we need to use the AddMaps() method in the mapper configuration which takes the assembly of the source and destination types as parameter:

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

var mapper = new MapperConfiguration(cfg => cfg.AddMaps(typeof(User).Assembly), NullLoggerFactory.Instance).CreateMapper();

When we perform the mapping, the end result will be the same as the fluent configuration.

Attribute Mapping With Mapster

In Mapster, we can use attributes like AdaptTo, AdaptFrom, AdaptTwoWays, AdaptMember and more to do the mapping for us.

public class User
{
    public DateTime CreatedAt { get; set; }
}

public class UserDto
{
    [AdaptMember("CreatedAt")]
    public string CreatedDate { get; set; } = null!;
}

In this case, we used the AdaptMember attribute to specify the source type’s property.

Dependency Injection

Both libraries provide support for dependency injection.

Dependency Injection With AutoMapper

Since version 13, the DI extensions ship inside the main AutoMapper package, so there is no longer a separate AutoMapper.Extensions.Microsoft.DependencyInjection package to install. We register everything with the AddAutoMapper() method on the IServiceCollection:

builder.Services.AddAutoMapper(cfg =>
{
    cfg.LicenseKey = builder.Configuration["AutoMapper:LicenseKey"];
}, typeof(Program).Assembly);

We pass one or more marker types (here Program) so AutoMapper scans their assemblies for profiles. From version 15.0.0, the commercial arm also expects a licence key at runtime in production — including on the free Community tier, which is a licence rather than the absence of one; the RPL-1.5 arm is the alternative to holding a key. During development and testing, AutoMapper runs without a key and only logs a reminder.

Dependency Injection With Mapster

Similarly, in Mapster as well, we need to install another NuGet package Mapster.DependencyInjection.

And then we need to register TypeAdapterConfig object as a singleton and register ServiceMapper class for IMapper interface with any lifetime:

services.AddSingleton(TypeAdapterConfig.GlobalSettings);
services.AddScoped<IMapper, ServiceMapper>();

In both the libraries, we can use the IMapper interface in the constructor to use the mapper object:

public class SampleService {
    private readonly IMapper _mapper;

    public SampleService(IMapper mapper) {
        _mapper = mapper;
    }
}

Is Mapster Faster Than AutoMapper?

Yes. In our BenchmarkDotNet measurements on .NET 10, Mapster is faster than AutoMapper in every scenario we test, by roughly 1× to 4× depending on the mapping. The gap is smallest for simple property copies and list mapping, where the two stay within about 30%, and largest for nested, flattened, and reverse mappings. Memory allocation, once a clear Mapster advantage, is now essentially identical on current versions.

The reason is architectural: Mapster compiles mapping functions up front, while AutoMapper resolves more of its configuration at map time.

Two caveats belong next to the numbers. First, object mapping is rarely an application’s bottleneck — a few hundred microseconds per thousand objects disappears next to one database round trip — so licensing and ergonomics usually weigh more than speed. Second, if performance is the deciding criterion, source-generated mappers like Mapperly beat both runtime libraries. Mapperly’s documentation states the mechanism: “Because Mapperly creates the mapping code at build time, there is minimal overhead at runtime.”

In order to do the performance benchmark, let’s use the BenchmarkDotNet library available in .NET.

Before we go ahead with the tests, let’s look at our benchmark setup:

[Benchmark(Description = "AutoMapper_SimpleMapping")]
public void AutoMapperSimpleObjectMapping()
{
    for (var i = 0; i < _size; i++)
    {
        AutoMapperSimpleTypeMapping.Map(_simpleObjectSource[i]);
    }
}

The AutoMapperSimpleObjectMapping() method will perform the object mapping for N number of items based on the _size field. In this case, the method will do the mapping using AutoMapper for a simple mapping scenario.

Let’s find out what the AutoMapperSimpleTypeMapping.Map() method does:

public class AutoMapperSimpleTypeMapping
{
    public static IMapper Mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>(), NullLoggerFactory.Instance).CreateMapper();

    public static UserDto Map(User source)
    {
        var destination = Mapper.Map<UserDto>(source);

        return destination;
    }
}

The Map() method in AutoMapperSimpleTypeMapping class will do the actual object-object mapping. It takes the source object as a parameter and returns the destination object after performing the object mapping. We also created the IMapper instance required for this particular mapping scenario.

Similarly, we’ve created the classes and methods for each scenario for both AutoMapper and Mapster.

Let’s look at how we generate the object source:

public class SimpleTypeMappingDataGenerator
{
    public static List<User> GetSources(int count = 1000)
    {
        var faker = new Faker<User>()
            .Rules((f, o) =>
            {
                o.Id = f.Random.Number();
                o.Name = f.Name.FullName();
                o.Email = f.Person.Email;
                o.IsActive = f.Random.Bool();
                o.CreatedAt = DateTime.Now;
            });
        return faker.Generate(count);
    }
}

The GetSources() method will produce the required number of source objects for the mapping. As the source object will differ for each scenario, we’ve created similar data generator classes for each scenario.

We generate the source objects before performing the actual benchmark:

[GlobalSetup(Targets = new[] { nameof(AutoMapperSimpleObjectMapping), nameof(MapsterSimpleObjectMapping) })]
public void SetupDataSourceForSimpleTypeMapping()
{
    _simpleObjectSource = SimpleTypeMappingDataGenerator.GetSources(_size).ToArray();
}

Finally, let’s set the _size field’s value to 1,000 and then run the benchmark on .NET 10 with the current library versions:

| Method                           |      Mean |    Error |   StdDev | Allocated |
|--------------------------------- |----------:|---------:|---------:|----------:|
| AutoMapper_SimpleMapping         | 219.02 us | 3.182 us | 2.977 us | 109.38 KB |
| Mapster_SimpleMapping            | 173.12 us | 3.410 us | 4.998 us | 109.38 KB |
| AutoMapper_ListOrArrayMapping    | 175.46 us | 2.646 us | 2.832 us | 133.45 KB |
| Mapster_ListOrArrayMapping       | 139.96 us | 2.786 us | 2.470 us | 125.11 KB |
| AutoMapper_NestedMapping         |  79.29 us | 0.876 us | 1.076 us | 117.19 KB |
| Mapster_NestedMapping            |  31.72 us | 0.632 us | 1.401 us | 117.19 KB |
| AutoMapper_FlattenedMapping      | 114.10 us | 2.153 us | 2.014 us | 136.84 KB |
| Mapster_FlattenedMapping         |  38.97 us | 0.768 us | 0.822 us | 136.63 KB |
| AutoMapper_CustomPropertyMapping | 151.42 us | 1.289 us | 1.006 us |  89.84 KB |
| Mapster_CustomPropertyMapping    |  88.13 us | 1.272 us | 1.128 us |  89.69 KB |
| AutoMapper_ReverseMapping        | 109.45 us | 2.103 us | 2.582 us | 117.19 KB |
| Mapster_ReverseMapping           |  28.73 us | 0.261 us | 0.204 us | 117.19 KB |
| AutoMapper_AttributeMapping      | 243.54 us | 4.645 us | 4.562 us | 117.19 KB |
| Mapster_AttributeMapping         | 172.46 us | 2.290 us | 2.030 us | 117.19 KB |

These numbers come from BenchmarkDotNet v0.15.8 on .NET 10.0.10, with AutoMapper 16.2.0, Mapster 10.0.11, and Bogus 35.6.5 generating the source objects.

From the results, we can see that Mapster is roughly 1.25 to 3.8 times faster than AutoMapper across these scenarios, with the biggest lead in nested, flattened, and reverse mapping. The memory footprint is now almost the same for both libraries, so Mapster’s advantage here is speed rather than allocations — a change from earlier versions, where AutoMapper allocated noticeably more.

In the end, we come to know that Mapster is a better option when performance is crucial. However, for non-performance critical applications, AutoMapper will do just fine, and the licensing terms should weigh at least as heavily as the microseconds.

What Are the Best AutoMapper Alternatives?

Three alternatives cover practically every migration away from AutoMapper. Mapster is the closest drop-in: the same runtime-mapping model with a near-identical feature set (custom member rules, EF Core projection, DI support) but faster, and MIT-licensed. Most teams migrate a type pair in minutes: CreateMap configuration becomes TypeAdapterConfig, and mapper.Map<TDest>(src) becomes src.Adapt<TDest>().

Mapperly is the compile-time option: a Roslyn source generator that emits plain mapping methods during build, giving zero runtime overhead, full debuggability, and compiler errors instead of runtime surprises. The cost is a partial-class declaration per mapper.

The third alternative is no library at all: hand-written mapping methods are explicit, trivially testable, and free of any dependency, which is why many teams treat a licensing change as the push to write their DTOs’ mappings by hand. The driver behind all three is AutoMapper’s dual licence from version 15.0.0: the free paths are the reciprocal RPL-1.5 arm, or a Community tier that needs all four of its conditions at once.

CriterionAutoMapperMapster
LicenseDual-licensed from 15.0.0 (July 2025): RPL-1.5 or commercial. The Community tier is free only if all four conditions hold — annual gross revenue (or non-profit budget) under $5M, no more than $10M outside capital, not a government or quasi-government entity, and not a university using it for institutional software. Commercial from $499/yr for 1–10 developers. 14.0.0 and earlier stay MIT permanently.MIT, unconditional — free at any company size, with no threshold to check.
Performance (our benchmark)Baseline~1.25–3.8× faster; similar memory allocation on current versions
Zero-config convention mappingYes (CreateMap still required per pair)Yes (Adapt() with no setup at all)
Custom rulesCreateMap().ForMember(...) profilesTypeAdapterConfig.NewConfig().Map(...)
EF Core projectionProjectTo<T>()ProjectToType<T>()
Compile-time code generationNoYes (Mapster.Tool)
DI registrationAddAutoMapper() (built into the main package since v13)Mapster.DependencyInjection package
Maturity / ecosystemSince 2011, the default for a decadeSince 2015, the leading free alternative

Conclusion

In this article, we have compared the usage of AutoMapper vs Mapster for some of the common object-object mapping scenarios. Finally, we did a performance benchmark to find the most optimal one. From the benchmark result, we came to know that Mapster performs better than AutoMapper in almost every scenario. In addition to the performance, Mapster scores in ease of use as well, and its unconditional MIT licence is one less thing to check.

If you want to learn about all the available options, please check out the official documentation of AutoMapper and Mapster.