Mapwright · three-way comparison

Mapwright vs Mapperly vs AutoMapper

Three .NET mappers, two models, one honest table — including the places where Mapwright is the wrong answer. Every Mapwright claim here was checked against the source, the tests and the benchmark project in this repository; the other two columns are checked against the packages themselves wherever that was possible.

1. The short answer

If you are already on Mapperly and it is working, stay there — it is the mature source-generator mapper, and nothing on this page is worth a migration.

If you are on AutoMapper and want mappings the compiler checks, no runtime engine, and Native AOT that just works, then either generator is a real upgrade; pick Mapwright if you want generated code that reads like the code you would have written and a build that fails on stale configuration, and pick Mapperly if you want the larger community and the wider configuration surface. If your mappings are decided at runtime — assembled from configuration data, chosen by type discovered at execution time — stay on AutoMapper, because no compile-time generator can express that, Mapwright included.

The one-line version: Mapperly is the safe incumbent, AutoMapper is the dynamic one, and Mapwright is the strict one — readable output, drift as a build error, and the fastest collection path of the three in this repository's benchmark.

2. Two models, three libraries

The important split is not three-way. It is two-way: when the mapping is resolved.

Compile time — Mapwright, Mapperly

You declare partial methods; a Roslyn incremental generator writes the bodies as ordinary C# during the build. Nothing reflects, nothing compiles expressions at startup, and the mapping is visible in a .g.cs file. The cost is that everything must be knowable at compile time.

Run time — AutoMapper

You configure maps; an engine inspects them by reflection, builds an execution plan per pair and caches it. The cost is startup work, opacity when a mapping misbehaves, and a poor fit with trimming and Native AOT. The benefit is that the configuration is data, so it can be built at run time.

Everything below follows from that. Mapwright and Mapperly disagree about ergonomics and strictness; they agree completely against AutoMapper about when a mapping should be settled.

3. Feature matrix

The Mapwright column was verified against src/, tests/ and the README in this repository. The Mapperly column was verified against the abstractions assembly shipped in Riok.Mapperly 4.3.1 — the version benchmarks/Mapwright.Benchmarks references — and the AutoMapper column against AutoMapper 13.0.1 and its documented behavior. A dash means not verified here, not "missing": both libraries are large and both move faster than this page, so check their current docs before deciding on a dash.

present  ·  not available  ·  not verified

CapabilityMapwrightMapperly 4.3.1AutoMapper
Resolution model Compile-time generator Compile-time generator Runtime reflection + compiled plans
Runtime component to ship attributes only attributes only the engine ships with you
Object maps
In-place copy onto an existing (EF-tracked) instance void M(src, dest) [MappingTarget] Map(src, dest)
EF-translatable projections (Expression<Func<,>> / IQueryable) both shapes queryable projections ProjectTo
Collections, arrays, sets, dictionaries
Nested maps synthesized without a declaration private helpers, opt-out by convention
Flattening and unflattening via dotted paths [MapProperty], source and target SourceFullName/TargetFullName conventions and ForPath
Enum maps by value or by name two strategies plus naming strategies, per-value overrides, ignored values
Built-in scalar conversions culture-invariant, fixed plus per-member StringFormat and FormatProvider type converters
Polymorphic / derived-type dispatch [MapDerivedType] [MapDerivedType] IncludeAllDerived
Deep cloning DeepClone = true UseDeepCloning
Update an existing object graph in place (nested instances copied into, collections merged by key) ExistingTargets = Merge, [MergeCollection] nested targets are replaced (issue #1311), no collection merge (issue #665) nested yes; collections need the separate AutoMapper.Collection package
Reference preservation across a cyclic graph in-place merges track visited targets; object maps still recurse UseReferenceHandling PreserveReferences
Before / after hooks as attributes [BeforeMap], [AfterMap] no hook attribute in 4.3.1's abstractions BeforeMap, AfterMap
Hand-written converters with injected services instance methods on an instance mapper [UserMapping], [UseMapper], [ObjectFactory] value resolvers and type converters
Generic dispatch Map<TSource, TTarget> typeof chain over declared maps native to the model
Mappings decided at run time (config assembled from data) the reason to keep it
Unmapped destination member is a compiler diagnostic MW0001 runtime AssertConfigurationIsValid()
A rename or ignore naming a member that no longer exists fails the build MW0003
Generated C# you can open, diff and breakpoint plans live inside the engine
Native AOT and trimming nothing executes at run time at odds with the model
Configuration surface on the mapper attribute 4 properties 17 properties, ~30 public abstraction types the largest of the three
Package frameworks netstandard2.0, net8.0, net10.0 netstandard2.0 broad, varies by major version
Package license MIT Apache-2.0 changed in 2025 — see below
Community, age, production history the smallest and newest of the three large, years of production use the largest in .NET, a decade of answers

4. Where Mapperly wins

Mapperly is the mature choice, and if you are on it, the honest recommendation is to stay.

It is the incumbent source-generated mapper for .NET: a very large user base, years of production hardening across projects far stranger than yours, a real contributor community, and an edge-case catalogue that only time produces. Mapwright is new. When Mapwright meets a mapping shape nobody has tried yet, you will be the one who finds out; when Mapperly meets one, there is a good chance someone already filed the issue and it was fixed two releases ago. That difference does not show up in a feature table and it matters more than most rows in one.

The configuration surface is not close either, and it is measurable. Mapperly 4.3.1's [Mapper] attribute carries seventeen properties — reference handling, null-mismatch behavior, obsolete-member strategies, constructor selection, member visibility, enum naming strategies, enabled conversion sets — against four on Mapwright's. Its abstractions assembly publishes around thirty public types, including per-value enum mapping, object factories, mapper composition, named mappings and per-member format providers. Mapwright ships eight attributes. Where your mapping needs a knob, Mapperly is much more likely to have one.

Two concrete capabilities Mapwright does not have at all: reference handling (UseReferenceHandling, which preserves object identity across a cyclic graph — Mapwright turns cycles into recursive calls and does not track instances), and culture control (FormatProvider/StringFormat per member — Mapwright's scalar conversions are culture-invariant, full stop, which is the right default and not always the right answer).

And on the single-object benchmark in this repository, Mapperly is simply faster than Mapwright. The number is in section 7, unrounded.

5. Where AutoMapper wins

AutoMapper is a mature, very widely deployed library, and its ecosystem is in a different weight class: a decade of Stack Overflow answers, tutorials, conference talks, and colleagues who already know it. On an existing team, "everyone has used this before" is a genuine engineering property, not a soft one.

More importantly, its runtime model does something neither generator can. When the mapping itself is data — pairs discovered by scanning assemblies at start-up, member routing driven by a configuration file or a tenant record, a destination type chosen from a value that only exists at execution time — a compile-time generator has nothing to generate from. AutoMapper's configuration is built while the program runs, so it can. That is not a workaround for AutoMapper's model; it is the point of it, and if your application lives there, AutoMapper is the correct tool and Mapwright is not a candidate.

Its convention engine also does more guessing for you, which is a cost in a strict codebase and a saving in a fast-moving one. A prototype where a CreateMap<A, B>() covers forty properties without a word of configuration is a real convenience.

6. Where Mapwright wins

Five things, none of them "more features than the others":

7. Performance, read honestly

benchmarks/Mapwright.Benchmarks maps an order with a nested customer and ten lines, four ways (BenchmarkDotNet, .NET 10, Intel i5-1035G1). The table below is reproduced verbatim from the repository README — absolute nanoseconds are machine-specific, so read the ratios and the allocations:

MethodMeanRatioAllocated
Hand-written, single order117.6 ns1.00768 B
Mapwright, single order105.3 ns0.90696 B
Mapperly, single order96.6 ns0.82696 B
AutoMapper 13, single order242.8 ns2.07888 B
Hand-written, 100 orders13.2 µs1.0077,728 B
Mapwright, 100 orders10.8 µs0.9270,456 B
Mapperly, 100 orders12.0 µs1.0270,456 B
AutoMapper 13, 100 orders20.6 µs1.7590,992 B

The honest reading, in order:

Perspective: the gap between the two generators is nanoseconds on one object and roughly a microsecond on a hundred. For nearly every application this is noise next to a single database round-trip, and it is a bad reason to choose either one. Pick on the model, the diagnostics and the ecosystem; treat the benchmark as evidence that neither generator is slow.

dotnet run -c Release in benchmarks/ reproduces the table on your own hardware, which is the only version of it that should convince you.

8. Migration effort

From AutoMapper

This is the substantial one, because the model changes. Each CreateMap<A, B>() becomes one static partial method on a [Mapper] class; each ForMember(..., opt => opt.Ignore()) becomes a [MapIgnore(nameof(...))]; ProjectTo<T>() becomes an Expression<Func<,>> or IQueryable<T> partial; IncludeAllDerived() becomes explicit [MapDerivedType] pairs; value resolvers that need services become hand-written methods on an instance mapper. The injected IMapper disappears, and with it the DI registration.

The work is front-loaded and noisy in a good way: the generator reports every destination member your profiles were silently leaving unset, so the migration usually surfaces bugs that were already in production. Budget it per profile, not per property. The case study walks an eight-entity profile through end to end.

From Mapperly

Mechanically much smaller — both are declare-a-partial-method generators, and the shapes line up closely — but the honest advice is don't. You would be trading a mature library for a new one to gain readable output, build-failing configuration rot and a faster collection path, while giving up reference handling, format providers, the wider configuration surface and the community. That is a bad trade for a working system. Mapwright is worth evaluating on a new project, or when one of its specific properties is something you actually need.

9. When not to use Mapwright

A comparison that recommends its own library everywhere is not worth reading. These are the cases where the answer is another tool:

Mappings decided at run time

Type pairs discovered by scanning, member routing driven by configuration data, destination types chosen from a runtime value. A source generator has nothing to read at build time. Use AutoMapper.

You are already well served by Mapperly

A working mapping layer on a mature generator is not a problem that needs solving. Stay. Revisit only if you hit something specific — output readability, or the collection path under real load.

You need a long support history

If your organization requires a large contributor base, a long release history, or a support story you can point at in a review, Mapwright does not have those yet. That is a fact about its age, and no feature list changes it.

Old TFMs pinned to an old language version

Mapping declarations are static partial methods with return values, which C# 7.3 — the default on pre-.NET 6 targets — predates. If you cannot set <LangVersion>latest</LangVersion> on those projects, Mapwright will not compile there.

Cyclic graphs needing identity preservation

Mapwright resolves cycles as recursive calls; it does not track instances, so a shared reference visited twice becomes two objects. Mapperly's UseReferenceHandling and AutoMapper's PreserveReferences exist for exactly this.

Culture-sensitive formatting inside the map

Built-in scalar conversions are culture-invariant and not configurable. If you need per-member format providers or format strings, that is a hand-written converter here, or a Mapperly feature there.

10. Licensing, factually

Stated plainly and without spin, because licensing is a decision input and not an argument: Mapwright is MIT and free for commercial use, with no runtime component to license. Mapperly 4.3.1 ships under Apache-2.0 per its package metadata. AutoMapper's terms changed in 2025 — package 13.0.1 declares the MIT expression, while 16.0.0 ships a license file offering RPL-1.5 or a separate commercial agreement.

Terms change and this page is a snapshot. Verify the current license of any version you actually intend to ship, for your own use, from the vendor — not from a comparison page.
Keep going: the beginner's guide explains how source-generated mapping works from zero; the side-by-side reference puts AutoMapper and Mapwright statement against statement with real generated output; and the production case study walks an eight-entity AutoMapper profile through the migration end to end.
Mapwright · MIT licensed · github.com/lodestar-labs/Mapwright
Benchmark figures are reproduced verbatim from the repository README (benchmarks/Mapwright.Benchmarks, .NET 10, Intel i5-1035G1) and are machine-specific — re-run them yourself. Mapperly figures and capabilities refer to Riok.Mapperly 4.3.1, the version this repository benchmarks; AutoMapper figures refer to 13.0.1. Cells marked "—" are ones this page could not verify, not features that are missing. Licensing notes reflect package metadata read at the time of writing — verify current terms for your usage.