Skip to main content

Getting Started

ZeroAlloc.Mapping is a Roslyn source-generator-driven object mapper for .NET 8, 9, and 10. It emits strongly-typed mapping methods at compile time, so the success path runs with zero heap allocation beyond the destination instance itself and is fully native-AOT compatible. Fallible mappings integrate with ZeroAlloc.Results so smart-constructor failures surface as a structured Result<T, MappingError> instead of a thrown exception.

Installation​

dotnet add package ZeroAlloc.Mapping

The generator runs automatically as part of dotnet build. There is no DI registration, no startup wire-up, and no runtime reflection — every dispatch you call was emitted into your assembly during compilation.

Your First Mapping​

Three steps from a clean project to a working source-generated mapper.

Step 1 — Define source and destination types​

Records are the family idiom; plain classes work just as well. The generator only cares that the destination has a constructor (or settable properties) the emitter can reach.

public sealed record OrderRequest(int Id, string Notes);
public sealed record Order(int Id, string Notes);

Step 2 — Declare the mapper​

Apply [Map<TSrc, TDst>] to a static partial class. The generator fills in the partial.

using ZeroAlloc.Mapping;

[Map<OrderRequest, Order>]
public static partial class AppMappings { }

Step 3 — Use the generated method​

var order = AppMappings.Map(new OrderRequest(42, "rush"));
// order.Id == 42, order.Notes == "rush"

That's the full happy-path loop. Every other feature in this doc set is a refinement on top of these three steps.

What Got Generated?​

Build the project and the generator drops something close to this into your assembly:

partial class AppMappings
{
public static global::Order Map(global::OrderRequest src)
{
global::System.ArgumentNullException.ThrowIfNull(src);
var __dst = new global::Order(
Id: src.Id,
Notes: src.Notes
);
return __dst;
}

// ... + 4 collection overloads (List<T>, T[], IEnumerable<T>, IReadOnlyList<T>)
}

A single direct constructor call, no intermediate buffers, no boxing. Property names line up by case-sensitive match; mismatches raise a compile-time diagnostic rather than silently dropping data.

tip

Don't want the four auto-emitted collection overloads on a particular mapper? Add [SkipCollectionOverloads] next to [Map<,>]. See Collections for the full story.

Fallible Mapping with [TryMap]​

When destination construction can fail — typically because a property is a smart-constructor value object that throws on invalid input — use [TryMap<TSrc, TDst>] instead. The generator emits a method that returns Result<TDst, MappingError> from ZeroAlloc.Results and converts the throwing constructor into a structured failure.

public sealed record SignUpRequest(string Email);
public sealed record User(Email Email);

public readonly record struct Email
{
public Email(string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("empty", nameof(value));
Value = value;
}
public string Value { get; }
}

[TryMap<SignUpRequest, User>]
public static partial class AuthMappings { }

Both paths return the same Result shape:

// Success path
var success = AuthMappings.TryMap(new SignUpRequest("user@example.com"));
// success.IsSuccess == true
// success.Value.Email.Value == "user@example.com"

// Failure path — empty email triggers ArgumentException in Email's smart ctor
var failure = AuthMappings.TryMap(new SignUpRequest(""));
// failure.IsSuccess == false
// failure.Error.Code == "mapping.constructor.threw"
// failure.Error.PropertyPath == "(root)"

The error carries a stable Code, the PropertyPath where the failure occurred, and the inner exception. See Advanced for the full MappingError tree (collection-element failures carry indexed paths like Items[3].Email, nested constructors carry dotted paths, etc.) and for tips on bridging into application-level result pipelines.

Auto-Generated Collection Overloads​

Every [Map<,>] automatically emits four collection overloads alongside the scalar one, so you rarely need to write a Select(...).ToList() by hand:

List<Order> orders = AppMappings.Map(new List<OrderRequest> { new(1, "a"), new(2, "b") });
Order[] arr = AppMappings.Map(new[] { new OrderRequest(3, "c") });

IEnumerable<T> and IReadOnlyList<T> overloads round out the set. See Collections for nested-element rules, capacity hints, and EF Core integration.

Where to Next​

  • Customisation — property renames, ignores, conversions, and custom expressions: Basic Mapping.
  • Diagnostics — every emission rule is enforced via ZAMP001–ZAMP016. Each code has a fix-it example: Diagnostics.
  • Cookbook — start with the canonical CQRS recipe: Command → Domain.