Skip to content

Intermittent "Context.Items are only available..." when a collection plan is shared with a map that has MaxDepth #4650

Closed

Description

@jbogard

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:

  1. CollectionMapper emits the OverMaxDepth check from the containing type map (memberMap.TypeMap, CollectionMapper.cs:66), but decides whether to emit ResolutionContext.CheckContext from the element type map (CollectionMapper.cs:76). With a containing map that has MaxDepth > 0 and a plain element map, no context check is emitted and OverTypeDepth runs against the default context.

  2. Collection execution plans are cached per type pair — MapRequest.Equals/GetHashCode deliberately exclude MemberMap (Internal/TypePair.cs:7-8) — while GenerateObjectMapperExpression bakes the member map into the compiled plan. The first member map to request a given List<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()] });         // throws

Reverse 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 MaxDepth on 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

  1. added a commit that references this issue on Sep 4, 2026
    9b500d5
  2. github-actions commented on Nov 1, 2026

    @github-actions

    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.

  3. locked as resolved and limited conversation to collaborators on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions