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.
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 important split is not three-way. It is two-way: when the mapping is resolved.
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.
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.
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
| Capability | Mapwright | Mapperly 4.3.1 | AutoMapper |
|---|---|---|---|
| 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 |
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.
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.
Five things, none of them "more features than the others":
GetValueOrDefault() where a nullable column collapses, a null-guard before a
nested map. Turn on <EmitCompilerGeneratedFiles> and it lands in
obj/generated as the samples in this repository do; git diff shows
behavior changes, and breakpoints work. There is no engine to reverse-engineer.[MapIgnore] or [MapProperty] naming a member that no longer exists is
MW0003, a build error. Ignore lists that quietly stop matching
anything are one of the standard failure modes of mapping configuration, and here they are not
possible.List backing span. Section 7 has the numbers.netstandard2.0, net8.0 and net10.0; the generator is a
build-time analyzer that ships no lib/ and adds no runtime dependency. Native AOT
and trimming are not a compatibility exercise.[Mapper(ExistingTargets = ExistingTargetStrategy.Merge)] an in-place copy onto an
EF Core tracked aggregate copies into the nested instances the target already holds and merges
child collections by key — matched elements updated, new ones added, missing ones removed —
instead of replacing them, which is what turns an update into a delete-and-insert of every
child row. Mapperly replaces nested targets (its issue #1311, open since 2024) and has no
collection merge (#665, open since 2023); AutoMapper needs the separate AutoMapper.Collection
package. A collection with no recognizable key is a build error, not a silent replace.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:
| Method | Mean | Ratio | Allocated |
|---|---|---|---|
| Hand-written, single order | 117.6 ns | 1.00 | 768 B |
| Mapwright, single order | 105.3 ns | 0.90 | 696 B |
| Mapperly, single order | 96.6 ns | 0.82 | 696 B |
| AutoMapper 13, single order | 242.8 ns | 2.07 | 888 B |
| Hand-written, 100 orders | 13.2 µs | 1.00 | 77,728 B |
| Mapwright, 100 orders | 10.8 µs | 0.92 | 70,456 B |
| Mapperly, 100 orders | 12.0 µs | 1.02 | 70,456 B |
| AutoMapper 13, 100 orders | 20.6 µs | 1.75 | 90,992 B |
The honest reading, in order:
List backing span
(CollectionsMarshal.SetCount + AsSpan): no enumerator, no per-element
delegate, no Add capacity checks. That path needs
CollectionsMarshal.SetCount, so it is what .NET 8 and later get; older targets get
the same loop pre-sized with Add — same result, same allocations, one extra bounds
check per element.dotnet run -c Release in benchmarks/ reproduces the table on your own
hardware, which is the only version of it that should convince you.
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.
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.
A comparison that recommends its own library everywhere is not worth reading. These are the cases where the answer is another tool:
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.
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.
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.
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.
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.
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.
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.
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.