Repository navigation
Mapping rules
The rules are the same for generated code and for the runtime fallback. The test suite runs every case through both.
| 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 |
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=noneLabel 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.
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=0Internal 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 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.
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 == 0The 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.
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=AdaPass the concrete type when you need the inherited members.
Fields, static properties, indexers, write-only properties, properties with a non-public getter, and properties with no match on the other side.
DotNetMapper
Guides
Links