Compile-time mapping. No runtime mystery.
Transformations.Mapping is a Roslyn source generator for .NET. Describe the relationship once — get predictable, type-safe mapping methods emitted as plain C# at compile time. No reflection, no startup cost, no runtime mapper tax.
- user.ToUserDto()new instance
- users.ToUserDtoList()optimized list path
- users.ToUserDtoArray()array output
- users.ToUserDtoEnumerable()lazy IEnumerable
- users.ToUserDtoAsyncEnumerable()lazy IAsyncEnumerable
- user.MapTo(existing)update existing target
- query.ProjectToUserDto()IQueryable expression
- dto.ToUser()reverse mapping
Fast because there is almost nothing left to do.
Measured with BenchmarkDotNet on an AMD Ryzen 7 5700G. The point is not benchmark theatre: generated mapping lands on the same allocation baseline as hand-written code, with no reflection, no profile discovery, and no runtime configuration step.
| Library | Time | Config cost | |
|---|---|---|---|
fastestTransformations |
compile-time only | ||
referenceHand-written |
manual upkeep | ||
competitorMapster |
runtime startup | ||
competitorAutoMapper |
runtime startup |
| Library | Time | Alloc vs manual | |
|---|---|---|---|
fastestTransformations |
1.00× baseline | ||
competitorMapster |
1.00× baseline | ||
referenceHand-written |
1.00× baseline | ||
competitorAutoMapper |
+10% allocations |
Bars are proportional within each test group — shorter is faster.
The collection path uses CollectionsMarshal.SetCount + dual-span indexed writes,
avoiding per-element List.Add overhead.
Generated methods carry [AggressiveInlining], so the JIT can see through the call boundary when the surrounding code allows it.
Four steps to make mapping disappear.
The Roslyn incremental generator runs inside your build. There is no container registration, profile scan, or runtime discovery step — the methods simply exist when your project compiles.
-
Annotate
Describe the relationship at the source
Add
[MapTo<Dto>]andpartialto the class you own. Multiple targets take multiple attributes. Rename a property with[MapProperty("Target")]. Flatten a nested object withSourcePath = "Customer.Name". Exclude sensitive or irrelevant members with[IgnoreMap]. -
Build
The compiler emits the boring code
The Roslyn incremental generator emits one extension class per mapping pair. Every method carries
[AggressiveInlining]. The output is plain, readable C# with small inlineable methods. You get the code you would have written by hand, without turning mapping into another file you have to keep in sync. -
Call
Use the generated API where the data moves
Call
user.ToUserDto(), pass aList<User>to.ToUserDtoList()for the optimized collection path, or chain.ProjectToUserDto()directly onto anIQueryable<User>for EF Core server-side projection. -
Ship
Ship without mapper surprises
Everything is emitted as plain, readable C# at build time. There is no reflection in the mapping output, so NativeAOT and trimming scenarios stay predictable. The broader Transformations family is built for modern .NET projects that value small, explicit dependencies.
A consistent API for every mapping pair.
Every annotation produces the same predictable surface. No profile class, no startup call, no hidden registry — and no mapper file that quietly drifts when the model evolves.
Take only what you need.
Install the full package when you want the everyday toolkit, or reference individual slices to keep each project layer lean. Mapping is only one part of the family: the same philosophy applies to conversion, strings, dates, data access, web helpers, and resilience.
All-in-one
The meta-package for application projects that want the full toolkit: Core, Data, Web, Dapper, and EntityFramework helpers in one install.
full toolkitObject mapper ✶
The Roslyn source generator featured on this page. Compile-time, NativeAOT-ready, and built around explicit attributes: [MapTo<T>], [MapFrom<T>], [MapProperty], and [IgnoreMap].
Foundation
Strings, collections, dates, holidays, type conversion, batching, resilience, and diagnostics. The package you reach for when you want small helpers without dragging in an application stack.
foundation layerData access
DataRow, DataReader, and DataTable helpers, SQL parameter builders, and CSV export. Boring data plumbing, made explicit around Microsoft.Data.SqlClient.
ASP.NET Core
HTTP context helpers, session and cookie utilities, query-string builders, and SelectList extensions for MVC views. Kept separate so Core stays usable outside ASP.NET.
ORM integration
Resilient Dapper query wrappers with transient SQL-fault detection. A companion Transformations.EntityFramework package covers resilient SaveChanges and related EF Core data workflows.
Text extraction
Multi-format text extraction for RAG pipelines. PDF, DOCX, PPTX, EPUB, RTF, HTML, Markdown, ODT, ZIP and more — with chunking, TableMode, and per-chunk provenance (page number, heading path).
Install once.
Stop repeating boilerplate.
Any Questions?
For you to decide whether this mapper belongs in production code, build pipelines, and NativeAOT workloads.
How can generated code benchmark ahead of hand-written helpers?
There is no magic: the generated methods are small, direct, and marked with [MethodImpl(AggressiveInlining)]. In the benchmarked shape, the JIT can see through the call boundary and optimize the mapping body aggressively. The collection path also uses CollectionsMarshal.SetCount combined with dual-span indexed writes, avoiding the per-element overhead of List.Add.
Does it support nested objects and custom converters?
Yes. Use [MapProperty("TargetProp", SourcePath = "Customer.Address.City")] for dotted-path flattening — the generator emits null-conditional expressions (source.Customer?.Address?.City) for nullable intermediates. Use Converter = nameof(MyStaticMethod) to run a static method as a per-property transform.
What happens when types don't match exactly?
The generator detects common convertible pairs and emits explicit conversion code such as .ToString(), ConvertTo<T>, or int.Parse where appropriate. Incompatible pairs are left unmapped and surfaced as TXMAP001 diagnostics in the IDE, so mismatches do not silently become production behaviour.
Is it NativeAOT and trimming safe?
Yes — the mapping output does not depend on reflection. The generator targets netstandard2.0, emits plain C# at build time, and keeps the output straightforward when projects enable IsAotCompatible.
How does ProjectToUserDto() compare to AutoMapper's ProjectTo?
Transformations generates a static Expression<Func<User, UserDto>> at build time. query.ProjectToUserDto() calls Queryable.Select with that expression, giving EF Core a normal projection without building ProjectTo configuration at query time.
Can I annotate the DTO instead of the domain class?
Yes — [MapFrom<User>] on the DTO side generates the same mapping surface as [MapTo<UserDto>] on the source side, from the other direction. Choose whichever keeps the annotation closest to the code you own.