You have written the same mapping code hundreds of times. Entity to DTO, DTO to view model, command to domain object. You could write it by hand every time and guarantee performance, or you could reach for AutoMapper and trade control for convenience. For years, that was the only real choice. But since AutoMapper moved to a commercial licence in 2025, many teams have started asking whether reflection-based mapping is worth paying for at all — especially when a source generator can do the same job at compile time, for free, with performance identical to hand-written code.
Mapperly is that source generator. It sits in your build pipeline as a Roslyn source generator, reads your partial method signatures, and emits the mapping implementations before your code ever runs. No reflection, no expression trees, no runtime overhead. The generated code is readable, debuggable, and trimming-safe. With nearly 20 million NuGet downloads and over 4,000 GitHub stars, it has moved well past "interesting experiment" into genuine production adoption.
Here is how it works, when to use it, and the pitfalls waiting for you if you skip the documentation.
Getting started
Install the package:
dotnet add package Riok.Mapperly
Define a mapper by creating a partial class decorated with [Mapper] and declaring partial methods for each mapping you need:
using Riok.Mapperly.Abstractions;
[Mapper]
public partial class OrderMapper
{
public partial OrderDto ToDto(Order order);
public partial Order FromDto(OrderDto dto);
}
That is it. At build time, Mapperly generates the implementation of ToDto and FromDto with direct property assignments. No configuration profiles, no service registration, no startup cost. The generated code lives in your project's obj folder and you can inspect it at any time.
How the source generator works
When the compiler encounters your [Mapper] partial class, the Mapperly generator analyses the source and target types, matches properties by name and type, and emits a straightforward C# implementation. For a simple mapping, the generated code looks like what you would write yourself:
public partial class OrderMapper
{
public partial OrderDto ToDto(Order order)
{
var target = new OrderDto();
target.Id = order.Id;
target.CustomerName = order.CustomerName;
target.Total = order.Total;
target.CreatedAt = order.CreatedAt;
return target;
}
}
This is not a simplified illustration — it is genuinely what the generator produces. There is no hidden abstraction layer, no dictionary lookups, no reflection calls. The compiler can inline, optimise, and trim this code exactly as it would any code you wrote by hand.
Property mapping and flattening
Mapperly automatically flattens nested properties using Pascal case conventions. If your source type has Customer.FullName and your DTO has CustomerFullName, the mapping resolves without configuration:
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
public List<LineItem> Items { get; set; }
}
public class Customer
{
public string FullName { get; set; }
public string Email { get; set; }
}
public class OrderDto
{
public int Id { get; set; }
public string CustomerFullName { get; set; }
public string CustomerEmail { get; set; }
public int ItemsCount { get; set; }
}
When auto-flattening cannot resolve a property — perhaps because the naming does not follow convention — use MapProperty:
[Mapper]
public partial class OrderMapper
{
[MapProperty(nameof(Order.Customer.FullName), nameof(OrderDto.BuyerName))]
public partial OrderDto ToDto(Order order);
}
You can also ignore properties explicitly:
[Mapper]
public partial class OrderMapper
{
[MapperIgnoreTarget(nameof(OrderDto.InternalScore))]
[MapperIgnoreSource(nameof(Order.AuditTrail))]
public partial OrderDto ToDto(Order order);
}
IQueryable projections for Entity Framework Core
This is where Mapperly genuinely shines compared to hand-written mapping. Define a projection method that takes and returns IQueryable, and Mapperly generates an expression tree that Entity Framework Core can translate directly to SQL:
[Mapper]
public static partial class ProductMapper
{
public static partial IQueryable<ProductDto> ProjectToDto(this IQueryable<Product> query);
}
public class ProductService(AppDbContext db)
{
public async Task<List<ProductDto>> GetActiveProductsAsync()
{
return await db.Products
.Where(p => p.IsActive)
.ProjectToDto()
.OrderBy(p => p.Name)
.ToListAsync();
}
}
Only the columns present in ProductDto are selected from the database. No over-fetching, no materialising the full entity just to throw half the properties away. If you have been writing manual Select projections for every query, this alone justifies the migration.
// TIP
Projection mappings have limitations — they do not support object factories, reference handling, or deep cloning. Keep your projection DTOs flat and simple.
Enum mappings
Mapperly supports mapping enums by value (default) or by name. You can configure this per-mapping or for the entire mapper:
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public partial class StatusMapper
{
public partial ExternalStatus ToExternal(InternalStatus status);
}
For case-insensitive name matching — common when mapping from external APIs:
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, EnumMappingIgnoreCase = true)]
public partial class StatusMapper
{
public partial InternalStatus FromExternal(ExternalStatus status);
}
You can also map individual values explicitly when names do not align:
[Mapper]
public partial class StatusMapper
{
[MapEnumValue(ExternalStatus.Completed, InternalStatus.Done)]
[MapEnumValue(ExternalStatus.InProgress, InternalStatus.Active)]
public partial InternalStatus FromExternal(ExternalStatus status);
}
Derived type mappings
When working with inheritance hierarchies, use MapDerivedType to tell Mapperly how to handle each concrete type:
[Mapper]
public partial class NotificationMapper
{
[MapDerivedType<EmailNotification, EmailNotificationDto>]
[MapDerivedType<SmsNotification, SmsNotificationDto>]
public partial NotificationDto ToDto(Notification notification);
}
Mapperly generates a switch expression that checks the runtime type and delegates to the appropriate mapping method. If you add a new derived type and forget to register it, the generated code will fall through to a default case — so watch for that.
Dependency injection and instance mappers
Mapperly supports both static and instance-based mappers. For scenarios where your mapping logic needs injected services — say, a currency converter or a localisation provider — use an instance mapper:
[Mapper]
public partial class InvoiceMapper
{
private readonly ICurrencyConverter _converter;
public InvoiceMapper(ICurrencyConverter converter) => _converter = converter;
public partial InvoiceDto ToDto(Invoice invoice);
private decimal ConvertAmount(decimal amount) => _converter.ToGbp(amount);
}
Mapperly will use your ConvertAmount method when it encounters a decimal-to-decimal mapping where the property names match. Register the mapper in the DI container as you would any other service:
builder.Services.AddScoped<InvoiceMapper>();
// NOTE
If you prefer static mappers for simpler scenarios, Mapperly works just as well with static partial class declarations. Use instance mappers only when you genuinely need injected dependencies.
Strict mapping and compile-time safety
One of the strongest arguments for Mapperly over reflection-based mappers is compile-time validation. By default, Mapperly generates diagnostics for unmapped properties. You can make this even stricter:
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Both)]
public partial class StrictOrderMapper
{
public partial OrderDto ToDto(Order order);
}
With RequiredMappingStrategy.Both, any source property without a matching target — or any target property without a matching source — produces a compiler warning. If you prefer build failures instead of warnings, promote Mapperly diagnostics to errors in your project file:
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);RMG012;RMG020</WarningsAsErrors>
</PropertyGroup>
This catches mapping drift at build time rather than at 2 AM when a null shows up in production because someone added a property to the entity but forgot to update the mapper profile.
Performance
Benchmarks consistently show Mapperly performing on par with hand-written mapping code and meaningfully faster than reflection-based alternatives. In a benchmark mapping 100,000 objects, Mapperly completed in 28.6ms compared to AutoMapper's 36.4ms — roughly 22% faster with 19% less memory allocation. For single-object mappings, AutoMapper is approximately 1.4-1.8x slower.
These are not dramatic numbers for a single mapping call. But when you are mapping objects inside a hot loop, a request pipeline, or a batch processing job, the difference compounds. More importantly, Mapperly's generated code is fully eligible for JIT inlining and other runtime optimisations that reflection-based code cannot benefit from.
The real performance story is not just throughput — it is startup cost. Reflection-based mappers must build and validate their configuration at application startup. Mapperly has zero startup cost because all mapping code exists as compiled IL before the application launches. For serverless workloads and container-based deployments where cold start time matters, this is a genuine advantage.
Trimming and Native AOT
Because Mapperly generates plain C# code with no reflection, it is fully compatible with .NET trimming and Native AOT compilation. There is nothing to preserve, no dynamic types to annotate, no surprises when the trimmer removes code that a reflection-based mapper needed at runtime.
If you are targeting PublishAot = true in .NET 10, Mapperly works without modification. The same is not true for most reflection-based alternatives, which require significant annotation work or are simply incompatible.
Common pitfalls
Forgetting to make the class partial. Mapperly needs the partial keyword on both the class and the mapping methods. The compiler error you get without it is clear enough, but it catches people who copy examples without reading carefully.
Expecting runtime configuration. There is no CreateMap<>() called at startup, no profile scanning, no convention overrides at runtime. All configuration is declarative via attributes. If you need mapping behaviour to change based on runtime state, you need a different tool — or a wrapper that selects between multiple generated mappers.
Ignoring the generated code. One of Mapperly's greatest strengths is transparency. When a mapping does something unexpected, open the generated file in obj/Debug/net10.0/generated/Riok.Mapperly/ and read it. The answer is always there.
Assuming IQueryable projections support everything. Expression tree generation for EF Core has inherent limitations. Constructor mappings with unmatched optional parameters, custom value converters, and reference handling are not supported in projection mode. Keep your projection DTOs simple.
Not configuring unmapped property handling. By default, unmapped properties generate compiler warnings that are easy to miss. Set RequiredMappingStrategy to Both and promote the diagnostics to errors if you want genuine compile-time safety.
Summary
- Mapperly is a Roslyn source generator that emits object mapping code at compile time — no reflection, no runtime cost
- Generated code performs identically to hand-written mapping and is fully readable and debuggable
- IQueryable projections generate expression trees that EF Core translates directly to SQL, selecting only the columns you need
- Compile-time validation catches unmapped properties before your code runs, not after
- Full compatibility with .NET trimming and Native AOT — no annotations or workarounds required
- With nearly 20 million NuGet downloads and active development, it is a mature, production-ready library
- For new projects, Mapperly is the default choice unless you have a specific need for runtime-configurable mapping