diff --git a/src/AutoMapper/Mappers/CollectionMapper.cs b/src/AutoMapper/Mappers/CollectionMapper.cs index 33b7bd4a65..b33434324b 100644 --- a/src/AutoMapper/Mappers/CollectionMapper.cs +++ b/src/AutoMapper/Mappers/CollectionMapper.cs @@ -141,7 +141,9 @@ void UseDestinationValue() Expression CheckContext() { var elementTypeMap = configuration.ResolveTypeMap(sourceElementType, destinationElementType); - return elementTypeMap == null ? null : ExpressionBuilder.CheckContext(elementTypeMap); + var checkElement = elementTypeMap == null ? null : ExpressionBuilder.CheckContext(elementTypeMap); + // OverMaxDepth uses the containing type map, so honor its context requirements too + return checkElement ?? (memberMap?.TypeMap is { } containingTypeMap ? ExpressionBuilder.CheckContext(containingTypeMap) : null); } } } diff --git a/src/UnitTests/Bug/CollectionMaxDepthContext.cs b/src/UnitTests/Bug/CollectionMaxDepthContext.cs new file mode 100644 index 0000000000..de78da6eb2 --- /dev/null +++ b/src/UnitTests/Bug/CollectionMaxDepthContext.cs @@ -0,0 +1,44 @@ +namespace AutoMapper.UnitTests.Bug; + +public class CollectionMaxDepthContext +{ + public class Service { public string Description { get; set; } } + public class ServiceDto { public string Description { get; set; } } + + // Self-referential, so CheckForCycles auto-enables PreserveReferences and MaxDepth + public class RecursiveLine { public RecursiveLine Parent { get; set; } public List Services { get; set; } } + public class RecursiveLineDto { public RecursiveLineDto Parent { get; set; } public IList Services { get; set; } } + + // Plain type mapping the same List -> IList pair + public class PlainLine { public List Services { get; set; } } + public class PlainLineDto { public IList Services { get; set; } } + + static IMapper CreateMapper() => new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap().ForMember(d => d.Services, o => o.MapAtRuntime()); + cfg.CreateMap().ForMember(d => d.Services, o => o.MapAtRuntime()); + }).CreateMapper(); + + static RecursiveLine NewRecursiveLine() => new() { Services = [new Service { Description = "recursive" }] }; + static PlainLine NewPlainLine() => new() { Services = [new Service { Description = "plain" }] }; + + // The collection execution plan is cached per type pair, ignoring the member map it was built + // for. When the recursive line compiles it first, the plan carries a max depth check for the + // containing type map, which needs a non default context in every other caller too. + [Fact] + public void Should_map_the_plain_line_after_the_recursive_line() + { + var mapper = CreateMapper(); + mapper.Map(NewRecursiveLine()).Services.Count.ShouldBe(1); + mapper.Map(NewPlainLine()).Services.Count.ShouldBe(1); + } + + [Fact] + public void Should_map_the_recursive_line_after_the_plain_line() + { + var mapper = CreateMapper(); + mapper.Map(NewPlainLine()).Services.Count.ShouldBe(1); + mapper.Map(NewRecursiveLine()).Services.Count.ShouldBe(1); + } +}