Table of Contents

Getting started

dotnet add package Mapperion

One map

using Mapperion;

var configuration = new MapperConfiguration(cfg =>
    cfg.CreateMap<Order, OrderDto>());

IMapper mapper = configuration.CreateMapper();
OrderDto dto = mapper.Map<Order, OrderDto>(order);

Members are matched by name, then by name ignoring case, then by flattening: a destination member called CustomerAddressCity finds Customer.Address.City on the source, with a null check at each step. Configured prefixes and suffixes are stripped before matching.

A pair with no CreateMap is an error, always. Mapperion never maps types you did not declare.

Configuring a member

cfg.CreateMap<Order, OrderDto>()
   .ForMember(d => d.Total, o => o.MapFrom(s => s.Lines.Sum(l => l.Price)))
   .ForMember(d => d.Internal, o => o.Ignore())
   .ForMember(d => d.Note, o => o.Condition(s => s.Note.Length > 0));

ForPath reaches inside the destination, and the objects along the way are created as needed:

cfg.CreateMap<Delivery, DeliveryDto>()
   .ForPath(d => d.Address.Street, o => o.MapFrom(s => s.Street));

IncludeMembers builds one destination out of several nested source objects. The map is consulted first; only what it leaves unresolved is offered to the included members, in order:

cfg.CreateMap<Application, ApplicationDto>()
   .IncludeMembers(s => s.Applicant, s => s.Employment);

When the two sides spell names differently

A row read straight out of a database or a JSON payload often spells its members first_name while the destination spells them FirstName. Ignoring case does not help, because they differ by a character rather than by capitalisation. Tell each side which spelling it uses and the conventions do the rest:

cfg.SourceMemberNamingConvention = LowerUnderscoreNamingConvention.Instance;
cfg.CreateMap<CustomerRow, CustomerDto>();

Flattening crosses the two spellings, so a destination ShipToCityName still reaches ship_to.city_name.

A spelling the library does not ship is one property. Worth knowing what the room is: member names are CLR identifiers, so the only separator that can appear in one is the underscore, and a convention of your own is there for its variants, such as the doubled separator some code generators emit.

public sealed class DoubleUnderscoreNamingConvention : INamingConvention
{
    public string? SeparatorCharacter => "__";
}

ExactMatchNamingConvention as the source convention reads names exactly as written and turns flattening off with them: CustomerName then only matches a source member of that name, never Customer.Name.

Checking the configuration

configuration.AssertIsValid();

It reports every problem at once rather than the first: destination members nothing maps to, nested pairs with no map declared, and loops with nothing to stop them. Worth calling in a test, so a configuration mistake fails the build rather than a request.

With dependency injection

dotnet add package Mapperion.Extensions.DependencyInjection
services.AddMapperion(typeof(SomeProfile).Assembly);

That scans for Profile classes, registers IMapper, and resolves converters and resolvers from the container, so a resolver can take its own dependencies.

Register the resolver itself as well as whatever it depends on. The container is asked for it by type, and a type nobody registered is not something it can build — the mapper then falls back to a public parameterless constructor, which is exactly what a resolver with dependencies does not have:

services.AddScoped<ITenantContext, TenantContext>();
services.AddScoped<AuditedByResolver>();
services.AddMapperion(typeof(SomeProfile).Assembly);

AutoMapper asks the same of you, so nothing here changes on the way over.

When mapping is the hot path

mapper.Map<Order, OrderDto>(order) is a generic method reached through an interface, and the runtime works out its type arguments on every call. On a small map that is most of what the call costs — more, on the flat benchmark, than everything Mapster spends in total.

Two ways out, in the order worth trying them.

MapperFor, when the same pair is mapped more than once:

Func<Order, OrderDto> toDto = mapper.MapperFor<Order, OrderDto>();

foreach (Order order in orders)
{
    results.Add(toDto(order));
}

None of that dispatch depends on the object being mapped, so in a loop it is the same answer found over and over. This asks for it once, and what comes back is an ordinary Func that also drops into a Select. Hold it for as long as the loop, or as a field beside the mapper; asking for one per call costs more than it saves.

Measured on the flat benchmark, this is the one arrangement that comes in under Mapster.

MapFast, for a single call with nowhere to keep a function:

OrderDto dto = mapper.MapFast<Order, OrderDto>(order);

Same result, same map. It skips the interface but still looks the plan up each time, so where there is a loop, MapperFor is the better answer. Both recognise the mapper the library builds and fall back to the interface for anything else, so a decorator or a test double still works.

The source generator, when mapping really is the thing your program spends its time on. It writes the mapping as ordinary C# while you build, and runs at the speed of code you would have written by hand — a far bigger difference than MapFast can give back.

Neither is worth reaching for by default. The saving is nanoseconds per object, which is nothing beside almost anything else a request does; Map is the one to write until a profiler says otherwise. Performance has the numbers.

When a map fails

A failure names the member it happened at, including the path through nested maps and the position in a collection:

Mapping Batch -> BatchDto failed at 'Readings[2].Ratio'. See the inner exception.

MappingException.MemberPath carries the same path for code that wants to read it.