Skip to content

Execution plans are cached per type pair but bake in member-specific state #4653

Description

@jbogard

Follow-up to #4650 / #4651, which fixed one symptom of this. The underlying sharing is still there.

The mismatch

MapRequest carries a MemberMap, but deliberately excludes it from equality (src/AutoMapper/Internal/TypePair.cs:7-8):

public bool Equals(MapRequest other) => RequestedTypes.Equals(other.RequestedTypes) && RuntimeTypes.Equals(other.RuntimeTypes);
public override int GetHashCode() => HashCode.Combine(RequestedTypes, RuntimeTypes);

_executionPlans is keyed by that MapRequest (Configuration/MapperConfiguration.cs:297), while GenerateObjectMapperExpression (:234) compiles mapRequest.MemberMap into the plan it caches — it is passed to mapper.MapExpression(...) and to NullCheckSource(...).

So for any type pair handled by an object mapper rather than a type map, the first member map to request that pair wins the cache for every other call site of those types, including top level Map calls that carry no member map at all.

Why it matters

Two consequences, one fixed and one not:

  1. Fixed in Emit the context check for the containing type map in CollectionMapper (fixes #4650) #4651. CollectionMapper emitted the OverMaxDepth check from memberMap.TypeMap while deriving the context guard from the element map, so a plan compiled for a member of a MaxDepth map threw for every later caller under the default context. The guard now agrees with the depth check, but the depth check itself is still whichever member map compiled first.

  2. Not fixed. NullCheckSource is given mapRequest.MemberMap, so per-member and per-profile null handling (AllowNull / AllowsNullDestinationValues) is also baked into a shared plan. A member map that allows nulls and one that does not, over the same type pair, appear to get whichever behavior compiled first. I have not built a repro for this — it is a read of the code, not a confirmed bug, and it is worth confirming or ruling out before deciding on a fix.

The general shape is that the failure depends on warmup order rather than on configuration, which makes it non-deterministic across process starts and very hard to diagnose from a stack trace.

Options

  • Include MemberMap in MapRequest equality. Correct, but multiplies plan count and compilation cost for every distinct member over the same type pair.
  • Key on the member-specific state that actually reaches the plan rather than on the member map identity, so members that generate identical code still share.
  • Stop baking member state into object mapper plans and pass it at call time.

The middle option seems the most promising, but it needs a survey of everything GenerateObjectMapperExpression and the object mappers read off memberMap.

Activity

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