Guide
Mapping, configuration, collections, nested objects, dependency injection, and thread-safety — in depth.
Mapping basics
IMapper.Map<TDestination>(source) creates a new
TDestination and populates its public properties in this order,
per property:
- An exact
ForMembermatch for that property name, if registered. - A
ForPathmatch nested under that property name, if registered. - Otherwise, auto-map from a source property with the same name: direct assignment if the types match exactly, or a recursive
Mapcall if a map is registered for the two (possibly different) types — including when both areList<T>and a map is registered for the item types. - If none of the above apply, the destination property is left at its default value.
See Examples → Basic mapping for a runnable version of the auto-map case.
Configuration & profiles
Mappings are registered by calling IMapperConfig.CreateMap<TSource, TDestination>(),
normally from inside a Profile subclass's Configure method.
AddAutoMapperLite discovers every concrete Profile in the given
assembly automatically — you don't register profiles individually.
public class MyMappingProfile : Profile
{
public override void Configure(IMapperConfig config)
{
config.CreateMap<Country, CountryViewModel>();
config.CreateMap<Organization, OrganizationViewModel>();
}
}
You can split configuration across multiple Profile classes — all of them in the scanned assembly are applied.
Collections
Two distinct, related capabilities:
Mapping a list directly
var result = mapper.Map<List<SimpleDestination>>(sourceList);
A List<T> destination property
As long as a map is registered for the item types, a List<T>,
collection-interface (IList<T>, ICollection<T>,
IReadOnlyList<T>, IReadOnlyCollection<T>,
IEnumerable<T>), or array (T[]) destination property
is auto-mapped like any other property, from a List<T> source
property — see
Examples → Nested collection properties.
Advanced usage
ForMember and ForPath must return the mapped value
ForMember/ForPath functions must return a value already
assignable to the destination property's type. The library does not implicitly
convert types inside a custom mapping function — see
Examples → Nested object mapping for the
correct pattern (closing over a Mapper built from the same config).
Deep nested paths
ForPath targets a destination property several levels deep, e.g.
dest => dest.A.B.C. Intermediate objects are instantiated
automatically. Multiple ForPath calls under the same top-level
property are all applied to the same nested instance — see
Examples → Deep nested paths.
Dependency injection
AddAutoMapperLite registers both IMapperConfig and
IMapper as singletons. This is intentional and safe:
MapperConfigis backed by aConcurrentDictionary, so registering (or reading) maps concurrently is safe.Mapperholds no per-request state beyond the injected config — there is nothing to isolate per request, so a singleton avoids allocating a new mapper instance for every DI scope.
Thread-safety
Both mapping configuration and mapping itself are safe under concurrent use.
This is verified by dedicated concurrency tests in the repository's test suite
(thousands of parallel Map calls and parallel CreateMap
registrations), not just assumed from the implementation.
Limitations
These are deliberate scope boundaries, verified by tests, not bugs:
-
A
nullsource property value is skipped, not assigned. If the destination property already has a non-null default (e.g. a field initializer), mapping anullsource value will not overwrite it. This differs from AutoMapper's default behavior (which does overwrite withnull). -
Auto-mapping requires an exact type match (or a registered map).
Two different enum types with identical member names,
enum → string, and numeric widening (int → long, etc.) are all silently left at the destination's default — not converted, not thrown. There is no convention/coercion engine; see the comparison page for how this differs from AutoMapper and Mapster. -
Destination types need a public parameterless constructor — instances
are created via a compiled constructor call (
Expression.New), not reflection'sActivator.CreateInstance. Positional records (record Foo(int A)) and classes requiring constructor arguments throw a clearInvalidOperationExceptionnaming the type. Records with a parameterless constructor and{ get; init; }properties work fine — a compiled property assignment can set an init accessor even though the C# compiler wouldn't let you call it directly outside the constructor. -
A
nullelement inside a source collection passed toMap<List<T>>throwsNullReferenceException— there's no single obviously-correct behavior (skip it? map it tonull?) so it's left as a hard failure rather than a silent guess.