Skip to content

Mapping rules

Sanam edited this page Oct 7, 2026 · 1 revision

The rules are the same for generated code and for the runtime fallback. The test suite runs every case through both.

Summary

Rule Detail
Input null throws ArgumentNullException with ParamName == "inputObject"
Output Always new TOutput(), then property assignments
Source property Public, instance, not an indexer, public getter. Inherited ones count
Target property Public, instance, not an indexer, public set or init. Inherited ones count
Match Same name (ordinal, case-sensitive) and identical type
Value Assigned, not cloned. Reference types are shared
Everything else Ignored, never an error

Types must be identical

No conversion of any kind, including widening and Nullable<T>. Nullable reference annotations are the one exception, because they do not exist at runtime.

public class TypesIn
{
    public int Count { get; set; }
    public int Total { get; set; }
    public string? Label { get; set; }
    public string title { get; set; } = "";
}

public class TypesOut
{
    public long Count { get; set; }                // int to long: not copied
    public int? Total { get; set; }                // int to int?: not copied
    public string Label { get; set; } = "none";    // string? to string: copied
    public string Title { get; set; } = "none";    // title vs Title: not copied
}

var t = Mapper.Map<TypesIn, TypesOut>(new TypesIn { Count = 5, Total = 7, Label = null, title = "t" });
// Count=0 Total= Label=null Title=none

Label is copied as null even though TypesOut.Label is declared non-nullable. The mapper does not check annotations, so a string? source can put null into a string target.

Setters

public class SetterOut
{
    public int A { get; set; }          // written
    public int B { get; init; }         // written
    public int C { get; private set; }  // not written
    public int D { get; }               // not written
}

var s = Mapper.Map<SetterIn, SetterOut>(new SetterIn { A = 1, B = 2, C = 3, D = 4 });
// A=1 B=2 C=0 D=0

Internal and protected setters are skipped too, even when the call is inside the same assembly. The result does not depend on where you call from.

Records and structs

Records with a parameterless constructor and init properties work:

public record UserRecord
{
    public int Id { get; init; }
    public string Name { get; init; } = "";
}

var r = Mapper.Map<UserEntity, UserRecord>(entity);
// UserRecord { Id = 1, Name = Ada }

A positional record such as record UserRecord(int Id) has no parameterless constructor and does not compile:

error CS0310: 'UserRecord' must be a non-abstract type with a public parameterless constructor in
order to use it as parameter 'TOutput' in the generic type or method 'Mapper.Map<TInput, TOutput>(TInput)'

A struct target works and allocates nothing.

Hidden and overridden properties

When a property is redeclared with new or override, the most-derived declaration decides, and only then are its accessors checked:

public class HidingBase { public int X { get; set; } }
public class Hiding : HidingBase { public new int X { get; private set; } }

var h = Mapper.Map<HasX, Hiding>(new HasX { X = 5 });
// h.X == 0, ((HidingBase)h).X == 0

The base X has a public setter, but Hiding.X hides it, so nothing is written. An override that declares only get is read-only for the same reason.

Interfaces

An interface type only exposes the properties declared on that interface, not the ones it inherits:

public interface IEntity { int Id { get; } }
public interface IUser : IEntity { string Name { get; } }

IUser user = new UserImpl { Id = 7, Name = "Ada" };
Mapper.Map<IUser, UserDto>(user);              // Id=0 Name=Ada
Mapper.Map<UserImpl, UserDto>((UserImpl)user); // Id=7 Name=Ada

Pass the concrete type when you need the inherited members.

Not copied, never an error

Fields, static properties, indexers, write-only properties, properties with a non-public getter, and properties with no match on the other side.

Clone this wiki locally