Mapt 1.0.0-preview.1

This is a prerelease version of Mapt.
dotnet add package Mapt --version 1.0.0-preview.1
                    
NuGet\Install-Package Mapt -Version 1.0.0-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mapt" Version="1.0.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mapt" Version="1.0.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Mapt" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mapt --version 1.0.0-preview.1
                    
#r "nuget: Mapt, 1.0.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mapt@1.0.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mapt&version=1.0.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Mapt&version=1.0.0-preview.1&prerelease
                    
Install as a Cake Tool

Mapt

CI NuGet License: MIT

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.CityAddressCity) and unflattening (CityAddress.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, TT?)
  • Records and constructor-based construction (positional records, init-only, required members)
  • 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

Samples

Runnable projects under samples/:

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

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .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