Skip to content

feat(mappers): add nested object mapping support with reusable helper methods#72

Merged
ncipollina merged 18 commits intomainfrom
feature/format-nested-mappings
Feb 18, 2026
Merged

feat(mappers): add nested object mapping support with reusable helper methods#72
ncipollina merged 18 commits intomainfrom
feature/format-nested-mappings

Conversation

@j-d-ha
Copy link
Collaborator

@j-d-ha j-d-ha commented Feb 17, 2026

🚀 Pull Request

📋 Summary

This PR introduces comprehensive support for mapping nested objects in DynamoMapper, enabling efficient serialization and deserialization of complex object graphs. The implementation uses reusable helper methods to handle nested type mappings, reducing code duplication and improving maintainability.

Key Features

  • Helper Method Infrastructure: New HelperMethodRegistry and HelperMethodInfo types manage reusable helper methods for nested object mappings
  • Multi-Level Nesting: Full support for deeply nested object hierarchies (Level1 → Level2 → Level3 → Level4, etc.)
  • Enhanced Property Analysis: Integrated PropertyAnalyzer to track nullability, requiredness, getters, setters, and default values for nested properties
  • Optimized Dictionary Initialization: Dictionary capacity is pre-calculated based on the number of properties to reduce allocations
  • Expression-Bodied Members: Cleaner code generation using arrow syntax for helper methods
  • Accurate Requiredness Handling: Nested properties correctly handle required vs optional semantics with proper fallback management

Technical Implementation

  1. HelperMethodEmitter: Generates ToItem_* and FromItem_* helper methods for nested types
  2. HelperMethodRegistry: Ensures the same nested type reuses the same helper method (no duplication)
  3. NestedObjectTypeAnalyzer: Enhanced to perform deep property analysis with nullability and requiredness tracking
  4. PropertyMappingCodeRenderer: Refactored to leverage helper methods and enhanced property analysis

Example Generated Code

For a mapper with nested Address objects, the generator now creates:

// Helper method for Address type
private static global::System.Collections.Generic.Dictionary<string, global::Amazon.DynamoDBv2.Model.AttributeValue> ToItem_Address(global::MyNamespace.Address address) =>
    new global::System.Collections.Generic.Dictionary<string, global::Amazon.DynamoDBv2.Model.AttributeValue>(3)
        .SetString("Street", address.Street)
        .SetString("City", address.City)
        .SetString("ZipCode", address.ZipCode);

// Reused in parent mapper
.SetMap("BillingAddress", customer.BillingAddress != null ? ToItem_Address(customer.BillingAddress) : null)

✅ Checklist

  • My changes build cleanly
  • I've added/updated relevant tests (multi-level nesting, requiredness handling)
  • I've updated documentation (inline code comments and XML docs)
  • I've followed the coding style for this project
  • I've tested the changes locally with snapshot tests

🧪 Related Issues or PRs

This PR builds foundational support for nested object mapping, which will be essential for Phase 2 of the DynamoMapper roadmap (collections and complex types).


💬 Notes for Reviewers

Files to Review

  • Core Infrastructure: HelperMethodRegistry.cs, HelperMethodInfo.cs, HelperMethodEmitter.cs
  • Property Analysis: NestedObjectTypeAnalyzer.cs (enhanced with PropertyAnalyzer integration)
  • Code Rendering: PropertyMappingCodeRenderer.cs (refactored for helper method support)
  • Test Coverage: NestedObjectVerifyTests.cs (multi-level nesting, requiredness tests)

Design Decisions

  1. Helper methods over inline expansion: Reduces code size and improves readability when the same nested type is used multiple times
  2. Expression-bodied members: Aligns with modern C# style guidelines and reduces visual noise
  3. Pre-calculated dictionary capacity: Measurable performance improvement by avoiding dictionary resizes
  4. Requiredness tracking: Ensures generated code handles required vs optional properties correctly, preventing null reference exceptions

Known Limitations

  • This PR focuses on nested objects with scalar properties only (consistent with Phase 1 scope)
  • Collections of nested objects and recursive types are planned for Phase 2

Introduced `HelperMethodEmitter` for generating reusable helper methods for nested object mappings:
- `ToItem` and `FromItem` helper methods are now extracted into separate methods.
- Updated `HelperMethodRegistry` to manage these reusable methods.
- Enhanced `MapperClassInfo` to include helper methods for better organization.

Updated test snapshots to reflect these changes.
…pacity

- Updated `FormatToItemChainedCalls` to preallocate dictionary capacity based on `.Set*` method calls.
- Introduced `CountSetMethodCalls` to calculate the number of `.Set*` calls for capacity estimation.
- Adjusted generated `ToItem` methods in test snapshots to use preallocated dictionary capacity.
- Introduced `NestedObject_MultipleLevels` test to verify mappings for deeply nested objects.
- Added sample classes (`Level1` to `Level4`) to test `ToItem` and `FromItem` methods.
- Updated test assertions to validate mappings across multiple levels.
…d mappings

- Extended `HelperMethodRegistry` to manage registration of reusable helper methods.
- Refactored `RenderInlineNestedToItem` and `RenderInlineNestedFromItem` to utilize the helper registry.
- Enhanced support for iterative rendering of helper methods in `MapperEmitter`.
- Updated snapshots to align with new helper methods for deeply nested mapping scenarios.
… address mappings

- Refactored `ToItem_Customer` and `FromItem_Customer` to delegate address mappings to new helper methods.
- Added `ToItem_Address` and `FromItem_Address` for better modularity and reuse.
- Updated dictionary initialization to reflect more accurate capacity.
…mbers

- Replaced block-bodied methods with arrow syntax for cleaner code.
- Updated snapshots to align with the new method formatting.
- Removed unnecessary braces and adjusted formatting accordingly.
…edness support

- Integrated `PropertyAnalyzer` for detailed property analysis in `NestedObjectTypeAnalyzer`.
- Added support to track nullability, requiredness, getters, setters, and default values.
- Enhanced handling of requiredness in nested object mappings with fallback management.
- Updated `NestedPropertySpec` to include new analysis properties.
- Refactored code rendering logic to leverage enhanced property analysis.
- Added a test case to validate correct requiredness handling for nested properties.
- Updated test snapshots to reflect changes in generated mapping code.
…ction assignments

- Updated nested object mappings to separate properties into initialization vs post-construction setup.
- Enhanced handling of `Requiredness.InferFromNullability` for dynamic property assignment.
- Refactored `RenderInlineNestedFromItem` to support conditional property assignments post-construction.
- Adjusted helper method generation to align with new assignment logic.
- Updated test snapshots to reflect changes in requiredness handling and assignment patterns.
Copy link
Contributor

@ncipollina ncipollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback from Claude:


High Priority

  1. Incremental pipeline equality broken — MapperInfo stores mutable GeneratorContext and HelperMethodRegistry in a record used by the incremental pipeline. Since these are reference types without value equality, the generator will re-execute on every keystroke, defeating caching. This is the most architecturally significant concern.
  2. Unbounded while(true) loop in MapperEmitter.Generate (line 55-81) — If a bug causes helper methods to register under different names, this becomes an infinite loop inside the compiler/IDE. Add a maxIterations safety bound.

Medium Priority

  1. Missing brace depth tracking — HelperMethodEmitter.SplitPropertiesRespectingNesting tracks () and <> depth but ignores {}. Any future code producing commas inside braces will silently corrupt output.
  2. .Set over-counting — CountSetMethodCalls counts every .Set substring including those inside arguments (e.g., source.Settings), inflating dictionary capacity in generated code.
  3. Unconditional null-checks for non-nullable types — RenderInlineNestedToItem always emits is null ? checks even for struct/required properties, generating dead code and potential compiler warnings.
  4. Duplicated type-name sanitization — Three near-identical implementations of type-name extraction across HelperMethodEmitter, HelperMethodRegistry, and PropertyMappingCodeRenderer.

Low Priority

  1. Re-parsing own output — Rendering a single-line string then re-parsing it character by character to format it. Follow-up refactor candidate.
  2. Minor style inconsistencies — Mix of string.Empty vs "" patterns.

Recommended Follow-ups (quick wins)

  • Add {/} to SplitPropertiesRespectingNesting depth tracking
  • Add maxIterations guard to the helper rendering loop
  • Consolidate type-name extraction into a shared utility
  • Conditionally emit null-checks based on Nullability.IsNullableType

Refactor RFC: Extract transient state from MapperInfo

The GeneratorContext and HelperMethodRegistry should be passed separately to MapperEmitter.Generate() rather than stored in the pipeline record, to restore proper incremental caching behavior. Estimated ~2-3 hours, low risk, internal-only change.

…entations

- Added `Equals` and `GetHashCode` methods to `MapperInfo` for enhanced comparison logic.
- Made `MapperInfo` non-sealed to allow inheritance.
- Introduced `DM0009` diagnostic to detect and report limit exceedance during helper method rendering.
- Updated `MapperEmitter` to apply a safety bound of 1,000 iterations for helper rendering loops.
- Modified the rendering logic to report `DM0009` when the iteration limit is reached.
- Enhanced `DiagnosticDescriptors` with a descriptor for `HelperRenderingLimitExceeded`.
- Updated `HelperMethodEmitter` to account for `{` and `}` in depth calculations.
- Prevented potential mismatches in helper method generation involving block delimiters.
- Added `FindNextSetMethodCall` to streamline `.Set*` method call detection.
- Replaced direct `.Set` index searches with the new utility method for consistency.
- Enhanced support for generic type and nested method parsing in `.Set*` calls.
…ion logic

- Introduced `TypeNameHelper` utility to extract and sanitize type names from fully qualified strings.
- Replaced redundant extraction logic in `HelperMethodEmitter` and `PropertyMappingCodeRenderer`.
- Unified `GenerateToItemHelperName` and `GenerateFromItemHelperName` methods into `GenerateHelperName`.
- Improved maintainability by removing duplicate code for type sanitization.
- Updated relevant method calls to leverage the new `TypeNameHelper` utility.
- Improved dictionary initialization with capacity pre-allocation in `HelperMethodEmitter`.
- Simplified and modularized property mapping rendering logic with reusable helper methods.
- Introduced `RenderToItemPropertyCall` and `RenderPropertyInitAssignment` as reusable utilities.
- Removed redundant formatting methods (`FormatToItemChainedCalls` and `FormatFromItemObjectInitializer`).
- Enhanced handling of nested and scalar property mappings with streamlined syntax.
@j-d-ha
Copy link
Collaborator Author

j-d-ha commented Feb 17, 2026

  1. Incremental pipeline equality broken — MapperInfo stores mutable GeneratorContext and HelperMethodRegistry in a record used by the incremental pipeline. Since these are reference types without value equality, the generator will re-execute on every keystroke, defeating caching. This is the most architecturally significant concern.

Resolved 1, 2, 3, 4, 6, and 7. I did not like the others (at least for now).

@j-d-ha j-d-ha requested a review from ncipollina February 17, 2026 20:59
ncipollina and others added 4 commits February 17, 2026 16:56
…entation

- Changed `MapperInfo` to a sealed record to prevent inheritance.
- Removed `virtual` keyword from the `Equals` method for consistency.
- Updated equality logic to streamline comparison operations.
Copy link
Contributor

@ncipollina ncipollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM

@ncipollina ncipollina merged commit e9bc805 into main Feb 18, 2026
1 check passed
@ncipollina ncipollina deleted the feature/format-nested-mappings branch February 18, 2026 12:57
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.

2 participants

Comments