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:

  1. An exact ForMember match for that property name, if registered.
  2. A ForPath match nested under that property name, if registered.
  3. Otherwise, auto-map from a source property with the same name: direct assignment if the types match exactly, or a recursive Map call if a map is registered for the two (possibly different) types — including when both are List<T> and a map is registered for the item types.
  4. 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:

  • MapperConfig is backed by a ConcurrentDictionary, so registering (or reading) maps concurrently is safe.
  • Mapper holds 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 null source property value is skipped, not assigned. If the destination property already has a non-null default (e.g. a field initializer), mapping a null source value will not overwrite it. This differs from AutoMapper's default behavior (which does overwrite with null).
  • 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's Activator.CreateInstance. Positional records (record Foo(int A)) and classes requiring constructor arguments throw a clear InvalidOperationException naming 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 null element inside a source collection passed to Map<List<T>> throws NullReferenceException — there's no single obviously-correct behavior (skip it? map it to null?) so it's left as a hard failure rather than a silent guess.