MapperPillow 1.0.0
dotnet add package MapperPillow --version 1.0.0
NuGet\Install-Package MapperPillow -Version 1.0.0
<PackageReference Include="MapperPillow" Version="1.0.0" />
<PackageVersion Include="MapperPillow" Version="1.0.0" />
<PackageReference Include="MapperPillow" />
paket add MapperPillow --version 1.0.0
#r "nuget: MapperPillow, 1.0.0"
#:package MapperPillow@1.0.0
#addin nuget:?package=MapperPillow&version=1.0.0
#tool nuget:?package=MapperPillow&version=1.0.0
MapperPillow
A comfortable, convention-first object mapper for .NET β compile-time and zero-ceremony.
π Languages: English Β· EspaΓ±ol β Full guide: docs/guia-de-uso.md
MapperPillow maps one object to another without the boilerplate. Properties that match by name map on their own; you only touch configuration for the unusual cases. Under the hood it is a Roslyn source generator + C# interceptors, so mapping code is generated at compile time β no runtime reflection, AOT-friendly, and you can open and read exactly what runs.
using MapperPillow;
var dto = user.MapTo<UserDto>();
No mapper instance. No dependency injection. No Profile classes. No startup call.
Why
AutoMapper became commercially licensed at v15. MapperPillow is a free (MIT) alternative with an easy migration path β familiar to use, but simpler, and with one thing AutoMapper can't do: it tells you about unmapped members at build time, not at runtime.
Features
- Zero ceremony β a single
MapTo<T>()extension, discovered automatically. - Compile-time β mapping is generated code; no reflection on the hot path.
- Conventions that just work β same-name properties, collections, arrays,
nested objects, collection-valued properties, enums, positional records, and
flattening (
Order.Customer.NameβCustomerName). - Build-time safety β the
MP0001diagnostic names any destination member left unmapped. Opt into treating it as an error for strict projects. - Auditable β the generated mapping is plain C# you can inspect and step through.
Requirements
net8.0,net9.0ornet10.0. All three are verified end-to-end, including trimmed and Native AOT publishes.- A .NET SDK with C# 12 or later (C# interceptors).
Getting started
1. Add the package
dotnet add package MapperPillow
The package carries the source generator with it (analyzers/dotnet/cs), so one
reference is all you need β no separate analyzer wiring.
Status: 1.0.0 is the first stable release. It targets
net8.0,net9.0andnet10.0, and every release is verified against the real.nupkgend-to-end β including trimmed and Native AOT publishes. See docs/RELEASING.md for how a release is cut β every version is published from CI through NuGet Trusted Publishing, so no long-lived credential can ship a package under this name.
2. Enable interceptors β nothing to do
The package opts your project into the interceptors namespace for you, from the
build/MapperPillow.targets it ships. There is no setup step.
If you reference the projects instead of the package, add it by hand:
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);MapperPillow.Generated</InterceptorsNamespaces>
</PropertyGroup>
This is not optional. The generator always emits its interceptor file, and without the namespace the compiler rejects it with CS9137 β the build fails. It does not quietly fall back to reflection.
To disable compile-time generation entirely (an escape hatch, not a supported mode β you get the non-equivalent reflection fallback, and trimmed/AOT builds will throw):
<MapperPillowEnableInterceptors>false</MapperPillowEnableInterceptors>
3. Map
using MapperPillow;
var source = new User { Id = 7, Name = "Ada Lovelace", Email = "ada@calculus.dev" };
UserDto dto = source.MapTo<UserDto>();
That's it. There is nothing to register.
Usage
Basic mapping
Properties are matched by name; assignable types are copied.
public sealed class User { public int Id { get; set; } public string Name { get; set; } }
public sealed class UserDto { public int Id { get; set; } public string Name { get; set; } }
var dto = user.MapTo<UserDto>();
Migrating from AutoMapper: the Map<T> alias
If your code reads mapper.Map<T>(x), MapperPillow offers a familiar Map<T>() so
the change is minimal:
var dto = user.Map<UserDto>(); // same result as MapTo<UserDto>()
Both are intercepted identically at compile time β migrated code gets the generated
path, not reflection. MapTo is the recommended form in new code.
Collections and arrays
The same call maps sequences β List<T>, arrays, and IEnumerable/IList/
ICollection/IReadOnlyList/IReadOnlyCollection<T> destinations:
List<UserDto> dtos = users.MapTo<List<UserDto>>();
UserDto[] array = users.MapTo<UserDto[]>();
Nested objects
A property whose type is another mappable object is mapped recursively, with a null guard:
public sealed class Customer { public string Name { get; set; } public Address Address { get; set; } }
public sealed class CustomerDto { public string Name { get; set; } public AddressDto Address { get; set; } }
var dto = customer.MapTo<CustomerDto>();
// dto.Address is a mapped AddressDto β or null if the source Address was null.
Flattening
A destination member with no direct match is resolved against a nested source path by splitting its PascalCase name:
public sealed class Order { public int Id { get; set; } public Customer Customer { get; set; } }
public sealed class OrderDto { public int Id { get; set; } public string CustomerName { get; set; } } // β Customer.Name
var dto = order.MapTo<OrderDto>();
// dto.CustomerName == order.Customer.Name (null intermediates yield default)
Per-member configuration
Conventions cover the common cases; for the rest, annotate the destination type β the call stays clean and everything stays compile-time.
public sealed class OrderDto
{
public int Id { get; set; }
[MapFrom("Customer.Name")] // explicit source path (may be nested)
public string Buyer { get; set; }
[MapIgnore] // never mapped; not reported by MP0001
public string Notes { get; set; }
[MapConvert(typeof(CentsToDollars))] // custom IValueConverter<int, string>
public string Price { get; set; }
}
Build-time safety: the MP0001 diagnostic
If a destination member can't be mapped, you get a warning at the MapTo call β
naming the member β instead of a silent gap discovered at runtime:
warning MP0001: MapTo<OrderDto> leaves destination member(s) unmapped: 'Notes'
To make unmapped members a hard build error, add to your .editorconfig:
[*.cs]
dotnet_diagnostic.MP0001.severity = error
Build-time safety: the MP0002 diagnostic
A compile-time mapper should never quietly become a runtime one. When the generator cannot produce code for a call site, it says so β and says why:
warning MP0002: MapTo<Dst> cannot be generated at compile time (no compile-time
mapping could be built from 'Src' to 'Dst'); it falls back to runtime reflection,
which is not trimming/Native AOT safe and supports neither flattening, [MapFrom],
[MapConvert], nor constructor-based destinations
That fallback is a safety net, not a second implementation: it only copies public
properties matching by name with an assignable type. Treat MP0002 as a bug report
about the call site, and escalate it if you want a guarantee:
[*.cs]
dotnet_diagnostic.MP0002.severity = error
ProjectTo for IQueryable
Mapping entities you already loaded is the expensive way to build a DTO. ProjectTo
pushes the mapping into the query, so the database returns only the columns you asked
for:
var dtos = db.Orders
.Where(o => o.Total > 100)
.ProjectTo<OrderDto>()
.ToList();
The generator emits Queryable.Select(q, src => new OrderDto { ... }), which the C#
compiler turns into the expression tree β so there is no runtime pipeline building
expressions, and flattening becomes a JOIN rather than a second round trip.
Operators composed after ProjectTo stay in the same query.
ProjectTo has no reflection fallback β it needs the generator, and says so with
MP0002.
Build-time safety: the MP0003 diagnostic
A few things that map fine with MapTo are not translatable by a database, and the
failure is not always loud. Measured against EF Core:
| Construct | What the provider actually does |
|---|---|
[MapConvert] converter |
Silently evaluates it on the client. The query works, but the database never computes the member β so nothing composed after the ProjectTo can filter or order by it. |
string β enum |
Throws. Enum.Parse is not client-evaluated at all. |
enum β string |
Translated fine. Not flagged. |
MP0003 warns at the call site, naming the member and which of the two it is:
warning MP0003: ProjectTo<OrderDto> projects member(s) the query provider cannot
translate: 'Total' (a [MapConvert] converter is evaluated on the client, so the
database never computes the member)
The member is still emitted β dropping it would hand you a DTO with a silently
missing value, which is worse. Map it after materialising with MapTo, or exclude it
with [MapIgnore].
Trimming and Native AOT
MapperPillow is built for it β the generated interceptors are plain typed assignments, so there is nothing for a trimmer to get wrong.
The one hazard is the reflection fallback: a trimmer cannot see through reflection over
an object, so it may remove the properties the fallback needs and leave you with a
silently half-mapped object. So PublishTrimmed and PublishAot builds remove the
fallback entirely. A call site that needed it throws immediately instead β and
MP0002 already told you about it at build time.
To keep the fallback in a trimmed build (you then own preserving the mapped types):
<MapperPillowEnableReflectionFallback>true</MapperPillowEnableReflectionFallback>
How it works
Every source.MapTo<TDestination>() call is discovered by a Roslyn incremental
generator, which plans the mapping (direct, nested, or flattened) and emits a
compile-time interceptor that replaces the call with typed assignment code. There
is no runtime reflection for covered call sites. See
DESIGN.md for the full architecture and roadmap.
Current limitations
MapperPillow is young. ProjectTo works and is verified against EF Core, but its
translatable subset is not yet enforced at build time: [MapConvert] converters and
string β enum compile into the projection and then fail in the provider.
Anything the generator can't handle falls back to a reflection-based mapper. That
fallback is intentionally minimal β name + assignable type only β so it does not
reproduce flattening, [MapFrom], [MapConvert], conversions, or constructor-based
destinations. Every call site that lands on it is reported as MP0002; treat those
warnings as work to do, not as a supported mode.
For the full picture of what's done and what's left (including NuGet packaging), see ROADMAP.md.
Building from source
git clone <your-fork-url> MapperPillow
cd MapperPillow
dotnet build MapperPillow.slnx
dotnet test MapperPillow.slnx
dotnet run --project samples/MapperPillow.Sample
Project layout:
src/MapperPillow Runtime surface (MapTo / Map) β net8.0/9.0/10.0
src/MapperPillow.Generator Roslyn source generator β netstandard2.0
tests/MapperPillow.Tests Behavioral tests β net8.0/9.0/10.0
tests/MapperPillow.Generator.Tests In-memory generator tests β net10.0
samples/MapperPillow.Sample Runnable example β net10.0
eng/Verify-Package.ps1 End-to-end package verification
Verifying the package
dotnet test cannot tell you that the package shipped without its generator, or
that every call site quietly fell back to reflection β both stay green until a
consumer publishes trimmed. This does check that, by consuming the real .nupkg
the way a user would:
pwsh ./eng/Verify-Package.ps1 # every target framework, including Native AOT
pwsh ./eng/Verify-Package.ps1 -SkipAot # skip the slow native link step
CI runs it on every push (.github/workflows/ci.yml).
License
MIT β see LICENSE. MapperPillow is an independent, clean-room project and is not affiliated with or derived from AutoMapper.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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 is compatible. 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. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.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 | 81 | 9/3/2026 |
First stable release. See https://github.com/4l3x31s/MapperPillow/releases for the full notes.