Pragmatic.Mapping
High-performance, source-generated object-to-object mapping for .NET 10.
The Problem
Section titled “The Problem”Every application converts between entities and DTOs, and the two standard approaches each force a bad tradeoff.
AutoMapper relies on runtime reflection: it hides failures until production, is incompatible with AOT compilation, and makes debugging opaque. Rename a property and the mapping silently breaks — no compiler error, just wrong data at runtime.
Manual mapping is correct but tedious. Every entity/DTO pair needs hand-written code in both directions. Add a property to the entity, forget to update the mapper, and the new property silently gets default. With 30 entities and 60 DTOs, you maintain hundreds of mapping methods by hand.
You shouldn’t have to choose between safety and convenience.
The Solution
Section titled “The Solution”Pragmatic.Mapping eliminates the tradeoff. You declare the mapping relationship with an attribute, and the source generator writes the mapping code at compile time:
- Visible — generated code lives in
obj/, fully debuggable and step-through-able. No magic. - Type-safe — property mismatches are caught at build time with
PRAG03xxdiagnostics, not in production. - Zero-reflection — no runtime discovery, no startup cost, AOT-compatible.
- Minimal footprint — no DI registration, static entry points; the only runtime dependency is the small
Pragmatic.Ensureguard library.
Rename a property and the build fails with a precise message — instead of shipping silent data loss.
How It Works
Section titled “How It Works”[MapFrom<User>] Pragmatic.SourceGeneratorpublic partial class UserDto ---> generates partial class:{ - FromEntity(User entity) public Guid Id { get; init; } - Selector (Func<User, UserDto>) public string Name { get; init; } - Projection (Expression<>)} - Extension: user.ToUserDto() - Extension: users.ToUserDto()- You decorate a
partialclass/record/struct with[MapFrom<T>]or[MapTo<T>]. - The Pragmatic source generator analyzes the properties at compile time.
- It emits a partial class with strongly-typed static methods plus a companion extensions class.
- No DI registration needed — everything is static.
When to use it
Section titled “When to use it”Use Pragmatic.Mapping whenever you map between entities and DTOs and want compile-time safety + AOT without hand-writing mappers. Then pick the path that fits each case:
| Scenario | Use |
|---|---|
| EF Core query → API response | [GenerateProjection] + Projection (SQL-level, best performance) |
| In-memory collection transform | .Select(Dto.Selector) (compiled delegate) |
| Single entity with hooks/custom logic | FromEntity() + CustomizeMapping() |
| Write path (DTO → entity) | [MapTo<T>] + ToEntity() |
| Update an existing entity | [MapTo<T>] + ApplyTo() |
| Hot path, value types | record struct DTO |
| Scalar-only mapping for mutations | [GenerateBodyOnlyVariant] + FromEntityBodyOnly() |
Installation
Section titled “Installation”dotnet add package Pragmatic.Mappingdotnet add package Pragmatic.Mapping.EFCore # optional: EF Core projection helpersThe mapping generator ships as a Roslyn analyzer inside the Pragmatic.Mapping package — referencing
the package is enough, no separate analyzer wiring. (Building inside this monorepo instead? See
Monorepo Structure.)
Quick Start
Section titled “Quick Start”Given an entity, declare a DTO with [MapFrom<T>]:
using Pragmatic.Mapping.Attributes;
[MapFrom<Guest>][GenerateProjection]public partial class GuestDto{ public Guid Id { get; init; } public string FirstName { get; init; } = ""; public string LastName { get; init; } = ""; public string? Phone { get; init; }
[MapIgnore] public string FullName => $"{FirstName} {LastName}";}The generator produces a static factory, extension methods, an in-memory delegate, and an EF Core projection — pick whichever the call site needs:
var dto = GuestDto.FromEntity(guest); // static factoryvar dto = guest.ToGuestDto(); // extension methodvar dtos = guests.ToGuestDto(); // IEnumerable / List / array overloads
var dtos = guests.Select(GuestDto.Selector); // in-memory LINQ (compiled delegate)
var dtos = await db.Guests // EF Core → translates to a SQL SELECT .Where(g => g.Email != null) .Select(GuestDto.Projection) .ToListAsync();No DI registration — everything is static. This example is from the Showcase
(examples/showcase/src/Showcase.Booking/Guests/Dtos/GuestDto.cs). Full first-mapping walkthrough:
Getting Started.
Know this one gotcha
Section titled “Know this one gotcha”FromEntity() maps in-memory objects: navigation properties you did not .Include() are null,
so the DTO silently gets nulls/empty collections with no error or warning.
// WRONG — Guest and Property are null in the DTO (not Included)var dto = ReservationSummaryDto.FromEntity(await db.Reservations.FindAsync(id));
// RIGHT — Projection translates to a SQL JOIN, no Include neededvar dto = await db.Reservations .Where(r => r.PersistenceId == id) .Select(ReservationSummaryDto.Projection) .FirstOrDefaultAsync();Prefer Projection for EF Core queries, or .Include() explicitly before FromEntity(). The
generator exposes the required navigations as Dto.RequiredNavigations. Details:
Common Mistakes.
Status
Section titled “Status”Stable within the 0.8 preview — the attribute surface and generated API are settled; see the roadmap for what is still moving before 1.0.
| Concepts | Architecture, core concepts, and decision guide |
| Getting Started | Your first mapping from entity to DTO |
| Attributes Reference | Every attribute, property-matching rules, customization hooks, EF Core helpers, mutation helpers, diagnostics |
| Projections | SQL-translatable Expression mappings and Include detection |
| Custom Converters | IValueConverter<TSource, TTarget> and manual property mapping |
| Feature Matrix | FromEntity vs Selector vs Projection — full comparison |
| Common Mistakes | The most frequent mapping pitfalls |
| Troubleshooting | Problem/solution guide with diagnostics reference |
Cross-module integration
Section titled “Cross-module integration”Pragmatic.Mapping is the DTO layer across the ecosystem: [MapFrom<T>] DTOs are the input/output of
Persistence mutations and queries,
Endpoints responses, and Actions;
[GenerateProjection] DTOs are projected before Caching.
Samples
Section titled “Samples”samples/Pragmatic.Mapping.Samples/ — 17 runnable scenarios covering all 8 attributes, 4-level
nesting, nullable intermediates, self-referencing round-trip, converter combinations, property-name
mismatches, multi-level flattening, ApplyTo, the BodyOnly variant, MapConstructor, and
bidirectional mapping with target paths.
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer (ships with the package)
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Mapping is MIT-licensed.