Repository navigation
Description
Summary
Mapping a nested collection can intermittently throw:
Context.Items are only available when using a Map overload that takes Action<IMappingOperationOptions>!
Consider using Context.TryGetItems instead.
even when the application never touches Context.Items anywhere. The message is misleading: ThrowInvalidMap is shared by Items and by CheckDefault, and the actual accessor here is TypeDepth via ResolutionContext.OverTypeDepth.
Cause
Two things combine:
-
CollectionMapperemits theOverMaxDepthcheck from the containing type map (memberMap.TypeMap,CollectionMapper.cs:66), but decides whether to emitResolutionContext.CheckContextfrom the element type map (CollectionMapper.cs:76). With a containing map that hasMaxDepth > 0and a plain element map, no context check is emitted andOverTypeDepthruns against the default context. -
Collection execution plans are cached per type pair —
MapRequest.Equals/GetHashCodedeliberately excludeMemberMap(Internal/TypePair.cs:7-8) — whileGenerateObjectMapperExpressionbakes the member map into the compiled plan. The first member map to request a givenList<X> -> IList<Y>pair therefore wins the cache for every other call site of those types.
So if a parent map with MaxDepth compiles the shared collection plan first, any later caller of that same type pair running under the default context throws. Whichever request arrives first after startup decides, which is why this reproduces intermittently, survives restarts and redeploys, and does not reproduce under AssertConfigurationIsValid() or a synthetic warmup map (those compile the plan in the safe order).
Regression range
The mismatch in CollectionMapper is long standing, but it was only reachable for maps with an explicitly configured MaxDepth. It became reachable for many more configurations in 0afaf1e ("Fix GHSA-rvv3-g6hj-g44x: apply default MaxDepth of 64 for self-referential types"), which auto-assigns MaxDepth = 64 whenever CheckForCycles enables PreserveReferences. That shipped in 15.1.1 and 16.1.1. Reverting only that hunk locally makes the repro below pass.
Repro
Two parent types sharing one List<Service> -> IList<ServiceDto> member, one of them self-referential:
public class Service { public string Description { get; set; } }
public class ServiceDto { public string Description { get; set; } }
// self-referential => auto PreserveReferences + MaxDepth 64
public class RecursiveLine { public RecursiveLine Parent { get; set; } public List<Service> Services { get; set; } }
public class RecursiveLineDto { public RecursiveLineDto Parent { get; set; } public IList<ServiceDto> Services { get; set; } }
public class PlainLine { public List<Service> Services { get; set; } }
public class PlainLineDto { public IList<ServiceDto> Services { get; set; } }
var mapper = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Service, ServiceDto>();
cfg.CreateMap<RecursiveLine, RecursiveLineDto>().ForMember(d => d.Services, o => o.MapAtRuntime());
cfg.CreateMap<PlainLine, PlainLineDto>().ForMember(d => d.Services, o => o.MapAtRuntime());
}).CreateMapper();
mapper.Map<RecursiveLineDto>(new RecursiveLine { Services = [new Service()] }); // ok, compiles the shared plan
mapper.Map<PlainLineDto>(new PlainLine { Services = [new Service()] }); // throwsReverse the two calls and both succeed. MapAtRuntime() here just forces the non-inlined path; in real applications the same path is reached whenever inlining is disabled for the member.
Reported stack shape from an affected application:
lambda_method(Closure, List`1, IList`1, ResolutionContext)
AutoMapper.ResolutionContext.OverTypeDepth(TypeMap)
AutoMapper.ResolutionContext.get_TypeDepth()
AutoMapper.ResolutionContext.CheckDefault()
AutoMapper.ResolutionContext.ThrowInvalidMap()
Workarounds
- Call the affected map with the
Action<IMappingOperationOptions>overload (forces a non-default context). - Set an explicit
MaxDepthon the affected maps. - Pin to 16.0.x / 15.1.0.
Fix
Emit the context check when either the element map or the containing type map requires it, so the guard and the depth check agree. The underlying cache-key issue (member-specific state baked into a type-pair-keyed plan) is worth addressing separately.
Activity
Merge pull request #4651 from LuckyPennySoftware/fix/collection-maxde…
github-actions commentedon Nov 1, 2026
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.