Skip to content

feat: v1.4 — IQueryable projections, cycle-safe mapping, deep-clone (B3 + B6 + B11)#17

Merged
MarcelRoozekrans merged 15 commits into
mainfrom
feat/mapping-v1.4-extensions
May 23, 2026
Merged

feat: v1.4 — IQueryable projections, cycle-safe mapping, deep-clone (B3 + B6 + B11)#17
MarcelRoozekrans merged 15 commits into
mainfrom
feat/mapping-v1.4-extensions

Conversation

@MarcelRoozekrans

Copy link
Copy Markdown
Contributor

Summary

v1.4 promotes three backlog items, all opt-in via new [Map<,>] properties.

Flag Backlog Effect
Projection = true B3 Emits a static Expression<Func<TSrc, TDst>> Projection property for use with EF Core's IQueryable.Select(...). Nested mappings auto-inline into the expression tree.
CycleSafe = true B6 Rewrites Map(src) into a tracker-allocating entry that forwards to an internal Map(src, IDictionary<object, object> tracker) recursive overload. Back-refs resolve via the cache.
DeepClone = true B11 Emits per-property literal clone code for every reachable reference type where no explicit nested [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 + CycleSafe combination: 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 in docs/backlog.md.

Architecture

  • B3: New ProjectionExpressionEmitter walks the per-decl PropertyMatch and emits a literal Expression<Func<,>> initializer. Nested [Map<,>] mappings are inlined.
  • B6: MapEmitter.EmitCycleSafeMapPair emits the paired entry + internal recursive overload. Tracker is Dictionary<object, object> with ReferenceEqualityComparer.Instance.
  • B11: New DeepCloneEmitter walks the reachable-type graph at compile time, emits new T { Field = src.Field } initializers for cloneable types, fires ZAMP019 on uncloneable, fires ZAMP020 on cycles without CycleSafe.

Test results — local pre-push

  • Generator tests: 81/81 (was 67 at v1.3 — +14).
  • Runtime tests: 42/42 (was 34 — +8 including 3 allocation-budget gates).
  • AOT smoke: exit 0, exercises all three v1.4 features.
  • Build: 0 W / 0 E across net8/9/10.

Backlog

After v1.4: B7 (out-of-scope-by-design, public policy statement), B12 (DeepClone+CycleSafe integration follow-up).

Test plan

  • CI green: build, aot-smoke, api-compat, lint-commits.
  • PublicAPI delta: 6 new lines (Projection/CycleSafe/DeepClone get/init on MapAttribute<,>). No PublicAPI.Shipped.txt change.

🤖 Generated with Claude Code

MarcelRoozekrans and others added 15 commits May 23, 2026 11:54
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>
@MarcelRoozekrans
MarcelRoozekrans merged commit 0240dcf into main May 23, 2026
3 checks passed
@MarcelRoozekrans
MarcelRoozekrans deleted the feat/mapping-v1.4-extensions branch May 23, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant