-
Notifications
You must be signed in to change notification settings - Fork 1
Why Mapsicle
AutoMapper 15.0 and later are no longer permissively licensed. They are governed by RPL-1.5 or a licence agreement from Lucky Penny Software, which includes a free Community License for those who qualify. Earlier versions keep their original licence. RPL-1.5 is strong reciprocal: its source obligations reach software that is only deployed internally, not just software you distribute. Check the terms against your own situation rather than taking this paragraph as advice. Mapsicle is MIT, the same licence as Mapperly and Mapster, so moving to it does not trade one licence question for another.
This section used to open by saying Mapsicle loses to a source generator on speed and always will. That stopped being true in 2.2.0: declare a pair and it is measured at 1.00x hand written code on a nine type aggregate, against 1.08 for Mapperly and 1.09 for Mapster. Undeclared pairs are a different story and are still 1.80x.
Be honest about the size of that lead. Ten percent against two mappers that are also free and also MIT is a tiebreaker, not a migration. What is worth switching for is that you get it without configuring anything, and that an undeclared pair still maps instead of failing to compile.
Reach for Mapsicle when:
| Situation | Why the alternatives do not fit |
|---|---|
Mapping a Dictionary<string, object> into a type |
Mapperly has nothing to generate against. AutoMapper needs the pair configured. |
| A collection whose items have different runtime types | Same: nothing to generate, and the shape is only known at runtime. |
| Types arriving from plugins, reflection or configuration | Compile-time generation is not available at all. |
Hundreds of DTOs and no appetite for a CreateMap per pair |
Mapsicle maps by convention with no setup. AutoMapper throws when it reaches an unconfigured pair. AssertConfigurationIsValid() validates the maps you configured, so it does not catch a pair you never registered. |
| Object graphs that contain cycles | Mapsicle returns the default at MaxDepth with no configuration. AutoMapper needs PreserveReferences() or MaxDepth(...), and an unhandled cycle can still overflow the stack. Mapperly needs UseReferenceHandling and Mapster needs PreserveReference; on default settings both abort the process. |
| The licence has to be permissive | Only AutoMapper is a problem here. Mapperly and Mapster are MIT too, so this rules out one of the four rather than picking Mapsicle. |
Reach for something else when:
| Situation | Choose |
|---|---|
| The compiler must prove every pair maps |
Mapperly. A pair it cannot generate does not compile. Mapsicle warns with MSG001 and falls back, which is safer at run time and weaker as a guarantee. |
| A rename must never silently unmap a member |
Mapperly. Its [MapProperty] uses nameof, so a rename is a compile error. Mapsicle's [MapFrom] takes a string and quietly stops matching. |
| You need AOT with no path that can fall back to runtime code generation |
Mapperly. Mapsicle's declared pairs are AOT clean, but an undeclared one throws NotSupportedException at first use under NativeAOT, and nothing at build time stops you leaving one undeclared by accident. |
| Collection throughput at around a hundred elements bounds your workload | Mapperly, by about 12 percent over an undeclared Mapsicle pair on x64. A declared pair closes that. |
| You want AutoMapper's configuration API without AutoMapper's licence |
Mapster. Its fluent config is deliberately shaped like CreateMap, so porting is mechanical. Mapsicle's fluent API is its own shape. |
Mapsicle is 1.30x to 1.44x faster than AutoMapper on single objects depending on the architecture, 1.09x to 1.20x on collections, and faster again on deeply nested graphs and large collections. Against Mapperly and Mapster the gap is ten percent either way. All of that is measured below, and all of it is a supporting argument rather than the reason to switch.
| Feature | Mapsicle | AutoMapper | Mapperly | Mapster |
|---|---|---|---|---|
| License | MIT | RPL-1.5, or a Lucky Penny Software agreement (a free Community License exists) | MIT | MIT |
| Architecture | Runtime + Caching, with an opt-in source generator | Runtime + Expressions | Source Generator | Runtime + Expressions, with an optional codegen tool |
| Setup Required | None, or one line to bind the assembly at compile time | Profiles, DI | Partial class | None |
| Dependencies | 0 (core) | 8 | 1, attributes only | 1 (Mapster.Core) |
| Deployed size | 59.5 KB | 1,117.4 KB | 29.5 KB, attributes only | 198.0 KB |
| Warm map, measured | 1.00x hand written when the pair is declared, 1.80x when it is not | 2.48x | 1.08x | 1.09x |
| Compile-time Safety | Partial. A pair it cannot emit warns and falls back | No | Full. It will not compile | Partial |
| AOT Compatible | Declared pairs yes, undeclared no | No | Yes, with no fallback to get wrong | With its codegen tool |
| Circular Refs | Terminates and returns a usable object, with no configuration. The cycle is expanded to MaxDepth (32) copies before a repeated instance stops it |
Preserves the reference by default (measured on 15.1.3 with a plain CreateMap) |
Default settings overflow the stack; correct with UseReferenceHandling
|
Default settings overflow the stack; correct with PreserveReference
|
| Memory Bounded | LRU Option | No | N/A | No |
| Cache Statistics | Yes | No | N/A | No |
| Integrated Validation | Yes | No | No | No |
| ASP.NET Core Helpers | Yes | No | No | No |
Two projects, each referencing one mapper and nothing else, dotnet publish -c Release on net8.0:
| Mapsicle | AutoMapper 15.1.3 | Mapperly 4.1.1 | Mapster 7.4.0 | |
|---|---|---|---|---|
| the mapper's own assembly | 59.5 KB | 286.0 KB | 29.5 KB, attributes only | 166.5 KB |
| assemblies it brings with it | 0 | 8 | 0 | 1 |
| total on disk | 59.5 KB | 1,117.4 KB | 29.5 KB | 198.0 KB |
Mapperly wins this one, and for a real reason: the only thing it ships at runtime is the attributes
assembly, because the mapping itself is your own source. Mapsicle is second and Mapster deploys
Mapster.Core alongside it.
More than half of what AutoMapper 15 deploys is not mapping code. Microsoft.IdentityModel.Tokens,
JsonWebTokens, Logging and Abstractions come to 599.1 KB, and they are there because
AutoMapper 15 validates a signed licence key. Referencing it puts a JWT validation stack into your
dependency closure, larger than the mapper itself, to check that you are allowed to use the mapper.
The remaining 232.4 KB is Microsoft.Extensions.* for dependency injection, options and logging.
Mapsicle's core has none of that, and not by luck: the core-has-no-dependencies job packs
src/Mapsicle and fails if the nuspec declares a single dependency. Everything else in the
ecosystem is a separate opt-in package.
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| Convention-based mapping | ✅ | ✅ | ✅ |
Flattening (Address.City to AddressCity) |
✅ | ✅ | ✅ |
| Custom member mapping | ✅ ForMember()
|
✅ ForMember()
|
✅ [MapProperty]
|
| Ignore members | ✅ [IgnoreMap]
|
✅ Ignore()
|
✅ [MapperIgnore]
|
| Reverse mapping | ✅ ReverseMap()
|
✅ ReverseMap()
|
✅ (define both) |
| Before/After map hooks | ✅ | ✅ | ✅ |
| Type converters | ✅ CreateConverter<>()
|
✅ ConvertUsing()
|
✅ User methods |
| Inheritance/Polymorphism | ✅ Include<>()
|
✅ Include<>()
|
✅ |
| Nested object mapping | ✅ | ✅ | ✅ |
| Collection mapping | ✅ | ✅ | ✅ |
| Constructor mapping | ✅ ConstructUsing()
|
✅ ConstructUsing()
|
✅ (automatic) |
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| Profile support | ✅ MapsicleProfile
|
✅ Profile
|
❌ (partial classes) |
| Fluent configuration | ✅ | ✅ | ❌ (attributes) |
| Attribute-based config | ✅ [MapFrom]
|
✅ | ✅ |
| Static zero-config API | ✅ obj.MapTo<T>()
|
❌ | ❌ |
| DI-friendly | ✅ IMapper
|
✅ IMapper
|
✅ |
| Assembly scanning | ✅ | ✅ | N/A |
| Package/Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| EF Core ProjectTo | ✅ Mapsicle.EntityFramework
|
✅ Built-in | ✅ (expressions) |
| FluentValidation | ✅ Mapsicle.Validation
|
❌ | ❌ |
| DataAnnotations | ✅ Mapsicle.DataAnnotations
|
❌ | ❌ |
| JSON serialization | ✅ Mapsicle.Json
|
❌ | ❌ |
| ASP.NET Core | ✅ Mapsicle.AspNetCore
|
❌ | ❌ |
| Caching | ✅ Mapsicle.Caching
|
❌ | N/A |
| Audit/Change tracking | ✅ Mapsicle.Audit
|
❌ | ❌ |
| Naming conventions | ✅ 5 conventions | ✅ Built-in | ✅ NamingStrategy
|
| Convention | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| PascalCase | ✅ | ✅ | ✅ |
| camelCase | ✅ | ✅ | ✅ |
| snake_case | ✅ | ✅ | ✅ |
| kebab-case | ✅ | ❌ | ❌ |
| SCREAMING_SNAKE_CASE | ✅ | ❌ | ❌ |
Measured rather than described. One order aggregate, nine types, three levels of nesting, two collections, mapped on an Apple M1 under .NET 8 Release, against the same projection written out by hand. Reproduce it from github.com/arnelirobles/mapsicle_samples.
All seven lanes in one process, each checked against the hand written baseline member by member before a single timing is taken, because a mapper that drops a member is faster than one that does not.
| Lane | Mean | vs hand written | Allocated |
|---|---|---|---|
| hand written | 288.5 ns | 1.00 | 1.41 KB |
| Mapsicle, pair declared | 287.7 ns | 1.00 | 1.41 KB |
| Mapperly 4.1.1 | 312.8 ns | 1.08 | 1.50 KB |
| Mapster 7.4.0 | 313.7 ns | 1.09 | 1.38 KB |
| Mapsicle, declared, untyped call | 316.1 ns | 1.10 | 1.41 KB |
| Mapsicle, nothing declared | 519.5 ns | 1.80 | 1.41 KB |
| AutoMapper 15.1.3 | 715.8 ns | 2.48 | 1.48 KB |
Read the middle of that table as a range, not a ranking. Mapperly and Mapster are 1.08 and 1.09 here and have swapped places between runs. The three modern mappers sit inside ten percent of each other and of hand written code, and nobody migrates a codebase for ten percent.
What holds across every run is the two ends. A declared pair is level with hand written code and allocates the same. An undeclared pair is 1.80, which is still 1.4x faster than AutoMapper and needs no setup at all.
Mapster allocates the least of anyone, including hand written code. Mapperly is the only lane
above the baseline, and that is one habit rather than anything structural: its collection helpers
take IReadOnlyCollection<T> where the member is a List<T>, so every foreach boxes the struct
enumerator. The section on
compile-time mapping shows both emitted
loops side by side.
| Mapsicle | AutoMapper | Mapperly | Mapster | |
|---|---|---|---|---|
| First map of a pair | 2,480 ns declared, 367,138 ns not | high, it compiles too | none, it is already code | it compiles at startup |
| Startup time impact | none for declared pairs | medium | none | compiles on first use or at startup |
| AOT compatible | declared pairs yes, undeclared no | no | yes | with its codegen tool |
| Scenario | Recommendation | Why |
|---|---|---|
| Fastest warm mapping | Mapsicle, pair declared | 1.00x hand written, against 1.08 and 1.09 for Mapperly and Mapster. Ten percent, which is a tiebreaker rather than a reason |
| The compiler must prove every pair maps | Mapperly | A pair it cannot generate does not compile. Mapsicle warns and falls back, which is safer at run time and weaker as a guarantee |
| AOT, and nothing may fall back to reflection | Mapperly | Mapsicle's declared pairs are AOT clean, but an undeclared one throws NotSupportedException at run time. Mapperly has no such path to leave open by accident |
| AOT, and you will declare every pair | Mapsicle or Mapperly | Both work. Check the build for MSG001 if you pick Mapsicle |
| An object graph with reference cycles | AutoMapper, or Mapsicle | AutoMapper preserves the reference so the cycle survives intact. Mapsicle terminates and returns something usable, but expands the cycle to MaxDepth (32) nested copies before it stops. Mapperly and Mapster both abort the process on default settings, and both are correct with one line of configuration |
| Quick prototyping, zero setup | Mapsicle or Mapster | Neither asks for configuration. Mapsicle additionally lets you add the generator later without touching a call site |
| A large graph you do not want to declare | Mapsicle or Mapster | Neither needs a line of setup. An undeclared Mapsicle pair is 1.80x hand written and still 1.4x faster than AutoMapper; Mapster is 1.09x with no declaration at all |
| Need integrated validation | Mapsicle |
Mapsicle.Validation, no equivalent in any of the other three |
| Existing AutoMapper codebase | AutoMapper (if licensed) or migrate | |
| Budget-conscious or OSS project | Mapsicle, Mapperly or Mapster | All three MIT. Permissive, and each asks only that the copyright and permission notice travels with the code |
| Complex mapping configurations | AutoMapper or Mapsicle (fluent) | |
| ASP.NET Core Minimal APIs | Mapsicle (AspNetCore package) | |
| Need audit trail of changes | Mapsicle (Audit package) |
Two of those rows go to Mapperly on purpose. Its guarantee is stronger than Mapsicle's precisely
because it has no fallback: if it cannot emit a mapper you find out at compile time, every time.
Mapsicle trades that for a mapper that always works, and the cost of the trade is that a MSG001 you
did not read is a pair running 1.80x instead of 1.00x.
Mapster shares several rows, and that is the honest picture rather than an oversight. It is MIT, it maps by convention with no setup, and its configuration API is deliberately shaped like AutoMapper's so a port is mechanical. Where Mapsicle differs is the pair of things no single competitor offers together: no configuration to start, and hand written speed when you want it, without the call site changing either way.
Mapsicle (Static - Zero Config)
var dto = user.MapTo<UserDto>();Mapsicle (Fluent)
var config = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>());
var mapper = config.CreateMapper();
var dto = mapper.Map<UserDto>(user);AutoMapper
var config = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>());
var mapper = config.CreateMapper();
var dto = mapper.Map<UserDto>(user);Mapperly
[Mapper]
public partial class UserMapper
{
public partial UserDto ToDto(User user);
}
// Usage
var dto = new UserMapper().ToDto(user);Features not found in AutoMapper or Mapperly:
-
Static zero-config API:
user.MapTo<UserDto>()- no setup required - Built-in validation integration: Map + validate in one call with FluentValidation or DataAnnotations
-
Audit/diff tracking: Track what changed during mapping with
MapWithAudit<T>() -
Caching integration: Cache mapped results with
IMemoryCache/IDistributedCache -
ASP.NET Core IResult helpers:
MapValidateAndReturn<T, TValidator>() -
JSON map-and-serialize:
MapToJson<TDest>(),MapFromJson<TIntermediate, TDest>() - LRU cache option: Memory-bounded cache for long-running applications
Mapsicle
Packages
- Core
- Fluent
- EntityFramework
- Validation
- NamingConventions
- Serilog
- Dapper
- AspNetCore
- Json
- Caching
- Audit
- DataAnnotations
Reference