feat: v1.4 — IQueryable projections, cycle-safe mapping, deep-clone (B3 + B6 + B11)#17
Merged
Merged
Conversation
Working notes captured during brainstorming. Bundle graduates B3 (IQueryable projections), B6 (cycle detection), B11 (UseDeepCloning) from docs/backlog.md. B7 (private member mapping) explicitly dropped to preserve the documented out-of-scope-by-design policy stance. Will consolidate into the canonical design doc once the brainstorm locks all sub-decisions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three opt-in additive features on [Map<TSrc, TDst>]:
Projection = true → IQueryable projection emit (B3).
Static Expression<Func<TSrc, TDst>> property.
Nested mappings auto-inlined.
ZAMP017 on EF-untranslatable features.
CycleSafe = true → cycle-safe deep mapping (B6).
Tracker-threading Dictionary<object, object>
with ReferenceEqualityComparer.
ZAMP018 on non-CycleSafe nested references.
DeepClone = true → per-property deep clone emit (B11).
Fully transitive type-graph walk.
ZAMP019 on uncloneable types.
ZAMP020 on cyclic type graphs without CycleSafe.
All three composable. B7 (private member mapping) explicitly dropped
to preserve the documented out-of-scope-by-design policy.
The decisions log at 2026-05-23-mapping-v1.4-decisions-log.md retains
the considered-and-rejected rationale for each sub-decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13-task TDD plan, organised into 6 phases: Phase A (Tasks 1-2): attribute properties + ZAMP017-ZAMP020 descriptors. Phase B (Tasks 3-5): B3 IQueryable projection emit + tests. Phase C (Tasks 6-7): B6 cycle-safe tracker emit + runtime tests. Phase D (Tasks 8-9): B11 deep-clone emit + ZAMP019/ZAMP020 + runtime tests. Phase E (Tasks 10-11): AOT smoke extension + allocation budgets. Phase F (Tasks 12-13): docs + backlog cleanup + push + admin-merge. Each task lists exact files, code skeletons, build/test commands, snapshot-regen notes (where applicable), and commit message. Follows the same shape as the prior v1.3 extensions plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….4 runtime surface) Three new opt-in init-only boolean properties. Generator wiring lands in subsequent tasks. Existing v1.3 [Map<,>] declarations stay byte- identical when no new flag is set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ingDecl Four new error-severity diagnostics. MappingDecl gains three optional positional bool params (Projection, CycleSafe, DeepClone) populated from the corresponding named args on [Map<,>]. No emit changes yet — generator emitters consume these in subsequent tasks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When [Map(Projection = true)] coexists with [MappingCulture], [BeforeMap]/[AfterMap] hooks, or [PolymorphicMap<,>] on the enclosing class, fires ZAMP017 (Error). EF Core's LINQ translator cannot handle any of these constructs. Transitive checking (nested mappings) lands with the projection emit in Task 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p(Projection=true)] (B3) New ProjectionExpressionEmitter builds the expression-tree literal as a static property on the declaring class. Nested same-class [Map<,>] mappings auto-inline into the expression initializer (EF Core cannot translate cross-mapper Map(...) calls), and nested mapped collections are inlined as Enumerable.Select(...).ToList()/ToArray() with an inline initializer selector. Supported in this iteration: direct member access, flattened dotted paths (collapsed to plain "." so expression trees can hold them), nested-object inline, nested-collection inline, and standard conversions (identity / implicit-or-explicit cast / single-arg ctor / Parse / Enum.Parse). Transitive ZAMP017 walk deferred: when a nested [Map<,>] lives in the SAME MapperClass, the outer decl already shares the class's Culture, Hooks, and PolymorphicDecls, so the existing direct ZAMP017 check catches it. Cross-class transitive checking would require a cross-class MapperClass registry the IncrementalGenerator pipeline does not currently expose; documented as a TODO in MappingGenerator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Task 5 runtime test (ProjectionTests + TestFixtures.cs) and
fixes a Projection-emit bug surfaced by it: ProjectionExpressionEmitter
was using object-initializer syntax (new Dst { Id = ..., Name = ... }),
which fails to compile against positional records because they have no
parameterless constructor. Switched to named-argument constructor
invocation (Expression.New form), which EF Core translates equivalently
to Expression.MemberInit and works uniformly for positional records and
POCOs alike. Verified Projection snapshots regenerated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eSafe=true)] (B6) [Map(CycleSafe=true)] now generates a paired emit: public Map(src) allocates a Dictionary<object, object>(ReferenceEqualityComparer.Instance), forwards to internal Map(src, tracker) which caches built destinations and threads the tracker through nested mapping calls. ZAMP018 fires when a CycleSafe mapping references a non-CycleSafe nested mapping - transitive enforcement, no auto-propagation (rationale in decisions log D-7). CONSTRAINT: cycle-safe emit requires Dst to have a public parameterless ctor + settable properties (mutable-builder pattern). Positional records are not supported. [TODO: ZAMP021 diagnostic for this constraint - defer until a real fixture surfaces the case.] Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[Map(DeepClone=true)] replaces per-property assignment with literal clone emit for every reachable reference type where no explicit nested [Map<,>] takes over. ZAMP019 fires on uncloneable types (no public ctor / no settable members); ZAMP020 fires when the type graph cycles without CycleSafe=true. Combining DeepClone+ CycleSafe routes through the existing cycle-safe pair emit so the tracker resolves cycles at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AOT smoke now exercises Projection, CycleSafe, and DeepClone end-to-end in the published AOT-compiled binary. AllocationBudgetTests gain 3 gates: cycle-safe entry-tracker allocation (~512B), deep-clone 50-element list (~4096B), and zero-alloc projection-property access. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new feature pages (iqueryable-projection.md, cycle-safe-mapping.md, deep-clone.md) following the existing flat docs layout and template. diagnostics.md extends ZAMP001-ZAMP016 with ZAMP017-ZAMP020 entries in the same Severity / Trigger / Triggering code / Fix shape used for the v1.3 diagnostics. basic-mapping.md adds a [Map<,>] Properties section covering the three new opt-in switches. README.md (the docs index) registers the three new pages. iqueryable-projection.md notes the AOT constraint: the Projection expression tree survives AOT publish, but invoking it via Queryable.AsQueryable requires JIT (IL2026/IL3050). EF Core consumers get this naturally; AOT consumers should pre-compile to Func<,> at app start and use LINQ-to-Objects. Also includes the D-3 decisions-log update for the locked ZAMP017 feature-incompatibility set (hooks, culture, polymorphic). Note: the repo uses a flat docs/ layout (single diagnostics.md, no core-concepts/ subdir, no attributes.md), so the plan's docs/core-concepts/ + docs/diagnostics/ER0017.md structure was adapted to extend the existing files in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B7 stays as the deliberate out-of-scope-by-design policy statement. B12 (DeepClone+CycleSafe integration follow-up) added in the final review cleanup before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v1.4 promotes three backlog items, all opt-in via new
[Map<,>]properties.Projection = trueExpression<Func<TSrc, TDst>> Projectionproperty for use with EF Core'sIQueryable.Select(...). Nested mappings auto-inline into the expression tree.CycleSafe = trueMap(src)into a tracker-allocating entry that forwards to an internalMap(src, IDictionary<object, object> tracker)recursive overload. Back-refs resolve via the cache.DeepClone = true[Map<,>]exists.Four new diagnostics: ZAMP017 (Projection + incompatible feature), ZAMP018 (CycleSafe missing on transitive nested), ZAMP019 (DeepClone reaches uncloneable type), ZAMP020 (DeepClone + cyclic type graph without CycleSafe).
B7 (private member mapping) explicitly NOT shipped — preserves the documented out-of-scope-by-design policy.
Known deferral
DeepClone + CycleSafecombination: the CycleSafe routing fork wins. Reachable reference-typed properties WITHOUT an explicit nested[Map<,>]are NOT deep-cloned in this combination. User must declare explicit nested[Map<,>]for each (CycleSafe's ZAMP018 enforces coverage). Full integration tracked as B12 indocs/backlog.md.Architecture
ProjectionExpressionEmitterwalks the per-declPropertyMatchand emits a literalExpression<Func<,>>initializer. Nested[Map<,>]mappings are inlined.MapEmitter.EmitCycleSafeMapPairemits the paired entry + internal recursive overload. Tracker isDictionary<object, object>withReferenceEqualityComparer.Instance.DeepCloneEmitterwalks the reachable-type graph at compile time, emitsnew T { Field = src.Field }initializers for cloneable types, fires ZAMP019 on uncloneable, fires ZAMP020 on cycles without CycleSafe.Test results — local pre-push
Backlog
After v1.4: B7 (out-of-scope-by-design, public policy statement), B12 (DeepClone+CycleSafe integration follow-up).
Test plan
build,aot-smoke,api-compat,lint-commits.MapAttribute<,>). No PublicAPI.Shipped.txt change.🤖 Generated with Claude Code