Mapt 1.0.0-preview.1
dotnet add package Mapt --version 1.0.0-preview.1
NuGet\Install-Package Mapt -Version 1.0.0-preview.1
<PackageReference Include="Mapt" Version="1.0.0-preview.1" />
<PackageVersion Include="Mapt" Version="1.0.0-preview.1" />
<PackageReference Include="Mapt" />
paket add Mapt --version 1.0.0-preview.1
#r "nuget: Mapt, 1.0.0-preview.1"
#:package Mapt@1.0.0-preview.1
#addin nuget:?package=Mapt&version=1.0.0-preview.1&prerelease
#tool nuget:?package=Mapt&version=1.0.0-preview.1&prerelease
Mapt
Explicit-config, source-generator-based object-to-object mapping for .NET.
No magic, no reflection. Every mapping is explicit and verified at compile time; generated code runs as fast as if you wrote it by hand.
Mapt turns a partial method into a real, readable mapping method at build time. There is no runtime engine, no reflection, no expression trees, and nothing to register. What you ship is ordinary C# — the kind you'd have written yourself — and the compiler proves it correct before it runs.
using Mapt;
[Mapper(Strictness = MappingStrictness.ImplicitSameName)]
public partial class PersonMapper
{
[MapProperty("Address.City", "AddressCity")]
public partial PersonDto Map(Person person);
}
Mapt generates:
public partial global::PersonDto Map(global::Person person)
{
if (person is null) throw new global::System.ArgumentNullException(nameof(person));
return new global::PersonDto
{
Name = person.Name,
Age = person.Age,
AddressCity = person.Address.City,
};
}
Install
dotnet add package Mapt
The single Mapt package contains both the attributes (referenced by your code) and the source generator (an analyzer). Nothing needs to be registered at startup.
Why Mapt
| Mapt | |
|---|---|
| Runtime reflection | None |
| Startup cost | None — no config to scan or compile |
| Native AOT / trimming | Safe — no dynamic codegen |
| Correctness | Compile-time — unmapped or incompatible members are build errors |
| Generated code | Readable — reads like hand-written mapping code, steppable in the debugger |
Core concepts
Mappers are partial classes
Mark a partial class with [Mapper] and declare partial mapping methods. Each method takes one argument (the source) and returns the target. Mapt writes the body.
[Mapper]
public partial class OrderMapper
{
public partial OrderDto Map(Order order);
public partial LineItemDto MapItem(LineItem item); // reused automatically for List<LineItem> -> List<LineItemDto>
}
When a member is itself a mappable type (including collection elements), Mapt reuses another mapping method declared on the same class.
Strictness modes
[Mapper(Strictness = MappingStrictness.Explicit)] // default
[Mapper(Strictness = MappingStrictness.ImplicitSameName)]
Explicit(default) — every target member must be declared with[MapProperty]or excluded with[MapperIgnoreTarget]. Nothing maps unless you say so. This is the "no magic" default.ImplicitSameName— members with the same name map automatically; you only declare renames and exceptions.
Attributes
| Attribute | Applies to | Purpose |
|---|---|---|
[Mapper(Strictness = …, Reverse = …)] |
class | Marks a partial class as a mapper. Reverse shares inverted config between paired methods. |
[MapProperty(source, target)] |
method | Explicit rename / nested path mapping. Paths may be dotted: "Address.City". |
[MapProperty(…, Using = nameof(m))] |
method | Convert the value with a method on the mapper class. |
[MapProperty(…, Condition = nameof(p))] |
method | Assign the target member only when a predicate returns true. |
[MapperIgnoreTarget(name)] |
method | Exclude a target member from mapping. |
[MapperIgnoreSource(name)] |
method | Exclude a source member (silences MAPT003). |
[MapDerivedType(typeof(S), typeof(T))] |
method | Dispatch a base-type mapping to a derived mapping by runtime type. |
[BeforeMap] / [AfterMap] |
method | Pre/post-processing hooks called around each mapping. |
Custom value converters
Give [MapProperty] a Using method, or just declare a converter method on the mapper — any
single-argument method is used automatically for its (source, target) type pair:
[Mapper]
public partial class InvoiceMapper
{
[MapProperty(nameof(Invoice.Total), nameof(InvoiceDto.Total), Using = nameof(FormatMoney))]
public partial InvoiceDto Map(Invoice invoice);
private static string FormatMoney(decimal amount) => amount.ToString("C");
}
Conditional mapping
[MapProperty(nameof(Source.Age), nameof(Dest.Age), Condition = nameof(IsAdult))]
public partial Dest Map(Source source);
private static bool IsAdult(Source source) => source.Age >= 18;
Conditional members are assigned after construction, so they are simply skipped when the predicate
is false.
Polymorphic mapping
[MapDerivedType(typeof(Car), typeof(CarDto))]
[MapDerivedType(typeof(Truck), typeof(TruckDto))]
public partial VehicleDto Map(Vehicle vehicle);
public partial CarDto MapCar(Car car);
public partial TruckDto MapTruck(Truck truck);
Mapt generates a type switch that dispatches to the matching derived mapping.
Before / after hooks
Mark methods with [BeforeMap] (called before the target is constructed) and [AfterMap] (called
after, before return). Each hook parameter is bound by type to the source and/or target:
[Mapper(Strictness = MappingStrictness.ImplicitSameName)]
public partial class PersonMapper
{
public partial PersonDto Map(Person person);
[AfterMap]
private static void Stamp(Person source, PersonDto target) => target.MappedAtUtc = DateTime.UtcNow;
}
Async mapping
Declare a mapping method that returns Task<T> or ValueTask<T> and Mapt generates an async
method. Async converters ([MapProperty(Using = …)] returning a task), async sibling mappers, and
async collection element mappers are all awaited:
[Mapper]
public partial class PersonMapper
{
[MapProperty(nameof(Person.Id), nameof(PersonDto.Name), Using = nameof(LookupNameAsync))]
public partial Task<PersonDto> Map(Person person);
private async Task<string> LookupNameAsync(int id) => await _db.GetNameAsync(id);
}
Reverse mapping
Set Reverse = true on [Mapper] and declare both directions. The [MapProperty] renames (and
[MapperIgnore*] rules) you write on one method are shared, inverted, with its inverse method — so
you configure once:
[Mapper(Strictness = MappingStrictness.ImplicitSameName, Reverse = true)]
public partial class PersonMapper
{
[MapProperty(nameof(Entity.FullName), nameof(Dto.Name))]
public partial Dto ToDto(Entity entity);
public partial Entity ToEntity(Dto dto); // gets Name -> FullName automatically
}
One-way rules (a converter or condition) are not inverted.
Map into an existing instance
Add a second parameter — the existing target — and Mapt maps onto it instead of constructing a new
one. Return void or the target (for chaining); init-only members are left untouched:
[Mapper(Strictness = MappingStrictness.ImplicitSameName)]
public partial class PersonMapper
{
public partial void Update(PersonPatch patch, Person target);
}
What Mapt maps
- Same-name property-to-property mapping
- Renames via
[MapProperty] - Flattening (
Address.City→AddressCity) and unflattening (City→Address.City) - Collections:
List<T>,T[],IEnumerable<T>,ICollection<T>,IReadOnlyList<T>,HashSet<T>,Dictionary<TKey,TValue>— always materialized into a fresh instance - Collections of complex elements (reusing a sibling mapping method)
- Primitive conversions: numeric ↔ numeric, enum ↔ int, enum ↔ string, number ↔ string
- Nullable value types (
T?→T,T→T?) - Records and constructor-based construction (positional records,
init-only,requiredmembers) - Custom value converters, conditional mapping, and polymorphic (derived-type) dispatch
- Before/after hooks and async mapping (
Task<T>/ValueTask<T>) - Reverse mapping (
Reverse = true) and mapping into an existing target instance
Benchmarks
Mapt's promise is "as fast as hand-written". The benchmark maps a representative graph — nested flattening, a collection of complex elements, and an enum-to-string conversion — and compares Mapt against a hand-written mapper and the other popular libraries (steady-state, after warmup).
| Method | Mean | Allocated |
|---|---|---|
| Handwritten | 235.4 ns | 400 B |
| Mapt | 120.5 ns | 328 B |
| Mapperly | 129.4 ns | 344 B |
| AutoMapper | 138.1 ns | 336 B |
| Mapster | 108.9 ns | 304 B |
<sub>BenchmarkDotNet, .NET 8, steady-state (post-warmup). Allocation is the stable, reproducible metric — Mapt allocates 328 B, less than Mapperly, AutoMapper, and the hand-written LINQ baseline, because Mapt emits a pre-sized foreach loop for collections rather than a Select().ToList(). Steady-state timings cluster tightly and carry real run-to-run variance on the test machine (see the baseline's wide error bar), so treat them as indicative and reproduce the suite for your own workload. The durable wins are elsewhere: zero startup cost (no config to scan or expression trees to compile) and Native-AOT safety.</sub>
Key takeaways:
- Mapt tracks the hand-written baseline — the generated code is essentially the code you'd write, so there is no measurable abstraction cost.
- Source-generator mappers (Mapt, Mapperly) and warmed-up reflection/expression mappers (AutoMapper, Mapster) are all within a small constant factor at steady state; the real differences show up at startup (Mapt and Mapperly have none — no config to scan or expression trees to compile) and under Native AOT / trimming, where Mapt has zero reflection.
Reproduce (results land in BenchmarkDotNet.Artifacts/):
dotnet run --project benchmarks/Mapt.Benchmarks -c Release -- --filter '*'
Native AOT
Mapt is verified under Native AOT: the samples/Mapt.AotSample app maps a
nested graph with a collection and an enum conversion, publishes with <PublishAot>true</PublishAot>,
and runs as a self-contained native binary with zero trim/AOT warnings. The CI aot job builds
and runs it on every push.
Diagnostics
Mapt reports mapping mistakes as compiler diagnostics, with the exact member involved and a suggested fix.
| Code | Severity | Meaning |
|---|---|---|
MAPT001 |
Error | A required (or, in Explicit mode, any) target member is not mapped. |
MAPT002 |
Error | Source and target member types are incompatible and no conversion is available. |
MAPT003 |
Warning | A source member is never used — often a typo or a stale rename. |
MAPT004 |
Error | A target member has more than one source mapping (ambiguous). |
Severity is configurable per project via .editorconfig:
# Treat "unused source member" as an error in this project
dotnet_diagnostic.MAPT003.severity = error
Code fixes
MAPT001 ships with IDE light-bulb fixes: Add [MapProperty] for '<member>' (with a best-guess
source member) and Ignore target member '<member>' (adds [MapperIgnoreTarget]).
Viewing generated code
Add this to a consuming project to emit the generated files to disk:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Generated mappers also support Go to Definition in the IDE.
Guides
- Cookbook — DTOs, records, collections, flattening, polymorphism, async, EF Core, DI
- Migrating from AutoMapper
- Migrating from Mapster
Samples
Runnable projects under samples/:
- Mapt.MinimalApiSample — ASP.NET Core minimal API, mapper injected via DI
- Mapt.EfCoreSample — EF Core: map loaded entities to DTOs, and update a tracked entity
- Mapt.BlazorSample — Blazor Server component using an injected mapper
- Mapt.AotSample — Native AOT console app (nested + collection + enum mapping)
Requirements
- .NET SDK with Roslyn 4.8+ (Visual Studio 2022 17.8+, or the .NET 8 SDK and later)
- Consuming projects target .NET Standard 2.0 or newer (the generator itself imposes no runtime dependency beyond
Mapt.Abstractions)
Building this repo
dotnet build
dotnet test
See CONTRIBUTING.md for the full workflow.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.1 | 69 | 7/25/2026 |