Skip to content

Commit d63dd77

Browse files
authored
feat(source-generators): add nested object and collection mapping (#63)
* Add nested mapping support * fix(source-generators): improve nested collection diagnostics * docs: document nested mapping
1 parent b736593 commit d63dd77

119 files changed

Lines changed: 4182 additions & 30 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LayeredCraft.DynamoMapper.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<File Path="docs\examples\custom-converters.md" />
3434
<File Path="docs\examples\index.md" />
3535
<File Path="docs\examples\lambda-functions.md" />
36+
<File Path="docs\examples\nested-mapping.md" />
3637
<File Path="docs\examples\single-table-design.md" />
3738
</Folder>
3839
<Folder Name="/docs/getting-started/">
@@ -55,6 +56,7 @@
5556
<Folder Name="/examples/">
5657
<Project Path="examples/DynamoMapper.FieldLevelOverride/DynamoMapper.FieldLevelOverride.csproj" />
5758
<Project Path="examples/DynamoMapper.MapperConstructor/DynamoMapper.MapperConstructor.csproj" />
59+
<Project Path="examples/DynamoMapper.Nested/DynamoMapper.Nested.csproj" />
5860
<Project Path="examples/DynamoMapper.SimpleExample/DynamoMapper.SimpleExample.csproj" />
5961
</Folder>
6062
<Folder Name="/git/">

docs/advanced/diagnostics.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
11
# Diagnostics
22

3-
Documentation coming soon. See [Phase 1 Requirements](../roadmap/phase-1.md) for detailed specifications.
3+
This page lists the diagnostics emitted by the generator and when they occur.
4+
5+
## Usage Diagnostics
6+
7+
| Id | Title | Trigger |
8+
| --- | --- | --- |
9+
| DM0001 | Type cannot be mapped to an AttributeValue | A property type is not supported for mapping. |
10+
| DM0003 | Collection element type not supported | A collection element type is unsupported (non-primitive, non-nested). |
11+
| DM0004 | Dictionary key must be string | A map property uses a non-string key type. |
12+
| DM0005 | Incompatible DynamoKind override for collection | `DynamoKind` override does not match the inferred collection kind. |
13+
| DM0006 | Circular reference detected in nested type | Nested object graphs contain a cycle (direct or indirect). |
14+
| DM0007 | Unsupported nested member type | A nested property type cannot be mapped. |
15+
| DM0008 | Invalid dot-notation path | A dot-notation override points to a missing property. |
16+
| DM0101 | No mapper methods found | A `[DynamoMapper]` class has no `To*` or `From*` partial methods. |
17+
| DM0102 | Mapper methods use different POCO types | `ToItem`/`FromItem` use different model types. |
18+
| DM0103 | Multiple constructors marked with `[DynamoMapperConstructor]` | More than one constructor is attributed. |
19+
20+
## Notes
21+
22+
- Nested mapping cycles are detected during analysis and reported as `DM0006`.
23+
- Dot-notation overrides are validated against the model graph and reported as `DM0008`.

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Nested object and nested collection mapping in the source generator.
12+
- Mapper registry lookup with inline fallback for nested types.
13+
- Diagnostics for nested cycles and invalid dot-notation paths (DM0006-DM0008).
14+
- `examples/DynamoMapper.Nested` for end-to-end nested mapping scenarios.
15+
1016
### Planned
1117
- Phase 1: Attribute-based mapping
1218
- Phase 2: Fluent DSL configuration

docs/examples/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ These example projects show DynamoMapper features end-to-end.
88
- `examples/DynamoMapper.FieldLevelOverride` - Field-level overrides via `[DynamoField]`
99
- `examples/DynamoMapper.MapperConstructor` - Constructor/record support and
1010
`[DynamoMapperConstructor]`
11+
- `examples/DynamoMapper.Nested` - Nested objects and nested collections

docs/examples/nested-mapping.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Nested Mapping Example
2+
3+
The `examples/DynamoMapper.Nested` project demonstrates:
4+
5+
- Nested object mapping (inline and mapper-based)
6+
- Nested collections (`List<T>`, arrays, `Dictionary<string, T>`)
7+
- Dot-notation overrides for nested paths
8+
- Cycle detection diagnostics
9+
10+
## Example: Nested Objects
11+
12+
```csharp
13+
using System.Collections.Generic;
14+
using Amazon.DynamoDBv2.Model;
15+
using DynamoMapper.Runtime;
16+
17+
[DynamoMapper]
18+
public static partial class OrderMapper
19+
{
20+
public static partial Dictionary<string, AttributeValue> ToItem(Order source);
21+
public static partial Order FromItem(Dictionary<string, AttributeValue> item);
22+
}
23+
24+
public class Order
25+
{
26+
public string Id { get; set; }
27+
public Address ShippingAddress { get; set; }
28+
}
29+
30+
public class Address
31+
{
32+
public string Line1 { get; set; }
33+
public string City { get; set; }
34+
}
35+
```
36+
37+
## Example: Nested Collection
38+
39+
```csharp
40+
public class Catalog
41+
{
42+
public string Id { get; set; }
43+
public Dictionary<string, Product> Products { get; set; }
44+
}
45+
46+
public class Product
47+
{
48+
public string Name { get; set; }
49+
public decimal Price { get; set; }
50+
}
51+
```
52+
53+
## Dot-Notation Overrides
54+
55+
```csharp
56+
[DynamoMapper]
57+
[DynamoField("ShippingAddress.Line1", AttributeName = "addr_line1")]
58+
[DynamoField("ShippingAddress.City", AttributeName = "addr_city")]
59+
public static partial class OrderMapper { }
60+
```
61+
62+
See `examples/DynamoMapper.Nested/Program.cs` for the full walkthrough.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ See the [Quick Start Guide](getting-started/quick-start.md) for a complete tutor
6262
- **Allocation-Free** - Efficient dictionary operations, no LINQ or unnecessary allocations
6363
- **Clean Domain Models** - No attributes required on your domain classes
6464
- **Convention-First** - Sensible defaults with selective overrides
65+
- **Nested Mapping** - Inline or mapper-based support for nested objects and collections
6566
- **Single-Table Friendly** - Built-in support for DynamoDB single-table patterns
6667
- **Comprehensive Diagnostics** - Clear compile-time errors with actionable messages
6768

docs/usage/basic-mapping.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,51 @@ When generating `FromItem`, DynamoMapper chooses between:
3939
- **Constructor-based construction**: `new T(arg1, arg2, ...)` (optionally combined with an object
4040
initializer for settable/`init` properties).
4141

42+
## Nested Object Mapping
43+
44+
DynamoMapper supports nested objects and nested collections when the nested types are mappable.
45+
46+
### How Nested Mapping Is Chosen
47+
48+
For each nested object property, the generator uses this decision order:
49+
50+
1. **Dot-notation overrides**: if there are overrides for the nested path, inline mapping is used.
51+
2. **Mapper-based**: if a mapper exists for the nested type and it defines the required direction
52+
(`ToItem` and/or `FromItem`), the nested mapper is used.
53+
3. **Inline mapping**: otherwise, the nested type is inlined into the parent mapper.
54+
55+
### Nested Collections
56+
57+
Collections with nested element types are supported for:
58+
59+
- `List<T>` / `IEnumerable<T>` and arrays (`T[]`)
60+
- `Dictionary<string, T>`
61+
62+
Nested collections of sets (SS/NS/BS) are not supported.
63+
64+
### Cycles
65+
66+
Nested object graphs cannot contain cycles. Cycles emit `DM0006`.
67+
68+
### Example
69+
70+
```csharp
71+
public class Order
72+
{
73+
public string Id { get; set; }
74+
public Address ShippingAddress { get; set; }
75+
public List<LineItem> Items { get; set; }
76+
}
77+
78+
public class Address
79+
{
80+
public string Line1 { get; set; }
81+
public string City { get; set; }
82+
}
83+
```
84+
85+
See `examples/DynamoMapper.Nested` for a complete example.
86+
4287
## Constructor Mapping Rules (`FromItem`)
4388

4489
Constructor selection is deterministic and follows these priorities.

docs/usage/field-configuration.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,52 @@
11
# Field Configuration
22

3-
Documentation coming soon. See [Phase 1 Requirements](../roadmap/phase-1.md) for detailed specifications.
3+
Field configuration is handled through `[DynamoField]` attributes on the mapper class. These
4+
attributes target model properties and allow you to override defaults without touching your domain
5+
models.
6+
7+
## Basic Usage
8+
9+
```csharp
10+
using DynamoMapper.Runtime;
11+
12+
[DynamoMapper]
13+
[DynamoField(nameof(Product.Name), AttributeName = "productName", Required = true)]
14+
[DynamoField(nameof(Product.Description), OmitIfNull = true, OmitIfEmptyString = true)]
15+
public static partial class ProductMapper
16+
{
17+
public static partial Dictionary<string, AttributeValue> ToItem(Product source);
18+
public static partial Product FromItem(Dictionary<string, AttributeValue> item);
19+
}
20+
```
21+
22+
## Nested Property Overrides (Dot Notation)
23+
24+
Use dot notation to override nested properties without adding attributes to nested types:
25+
26+
```csharp
27+
[DynamoMapper]
28+
[DynamoField("ShippingAddress.Line1", AttributeName = "addr_line1")]
29+
[DynamoField("ShippingAddress.City", AttributeName = "addr_city")]
30+
public static partial class OrderMapper
31+
{
32+
public static partial Dictionary<string, AttributeValue> ToItem(Order source);
33+
public static partial Order FromItem(Dictionary<string, AttributeValue> item);
34+
}
35+
```
36+
37+
Notes:
38+
- Dot-notation overrides force inline mapping for the nested path.
39+
- Invalid paths emit `DM0008`.
40+
41+
## Supported Options
42+
43+
| Option | Description |
44+
| --- | --- |
45+
| `AttributeName` | Overrides the DynamoDB attribute name. |
46+
| `Required` | Controls requiredness during `FromItem`. |
47+
| `Kind` | Forces a specific `DynamoKind`. |
48+
| `OmitIfNull` | Omits null values during `ToItem`. |
49+
| `OmitIfEmptyString` | Omits empty strings during `ToItem`. |
50+
| `ToMethod` | Uses a custom method to serialize a value. |
51+
| `FromMethod` | Uses a custom method to deserialize a value. |
52+
| `Format` | Overrides default format for date/time/enum conversions. |
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
<PropertyGroup>
10+
<NoWarn>$(NoWarn);CS1591</NoWarn>
11+
</PropertyGroup>
12+
<ItemGroup>
13+
<PackageReference Include="AWSSDK.DynamoDBv2"/>
14+
</ItemGroup>
15+
<ItemGroup>
16+
<ProjectReference
17+
Include="..\..\src\LayeredCraft.DynamoMapper.Generators\LayeredCraft.DynamoMapper.Generators.csproj"
18+
ReferenceOutputAssembly="false"
19+
OutputItemType="Analyzer"
20+
/>
21+
<ProjectReference
22+
Include="..\..\src\LayeredCraft.DynamoMapper.Runtime\LayeredCraft.DynamoMapper.Runtime.csproj"
23+
ReferenceOutputAssembly="true"
24+
OutputItemType="Analyzer"
25+
/>
26+
</ItemGroup>
27+
</Project>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Amazon.DynamoDBv2.Model;
2+
using DynamoMapper.Runtime;
3+
4+
namespace DynamoMapper.Nested;
5+
6+
/// <summary>
7+
/// Example: Mapper-based nested object - Author has its own mapper
8+
/// </summary>
9+
public record BlogPost
10+
{
11+
public string Slug { get; set; }
12+
public string Title { get; set; }
13+
public Author Writer { get; set; }
14+
}
15+
16+
public record Author
17+
{
18+
public string Handle { get; set; }
19+
public string DisplayName { get; set; }
20+
public string Bio { get; set; }
21+
}
22+
23+
// Author has its own mapper - BlogPostMapper will use this
24+
[DynamoMapper]
25+
public static partial class AuthorMapper
26+
{
27+
public static partial Dictionary<string, AttributeValue> ToItem(Author source);
28+
29+
public static partial Author FromItem(Dictionary<string, AttributeValue> item);
30+
}
31+
32+
[DynamoMapper]
33+
public static partial class BlogPostMapper
34+
{
35+
public static partial Dictionary<string, AttributeValue> ToItem(BlogPost source);
36+
37+
public static partial BlogPost FromItem(Dictionary<string, AttributeValue> item);
38+
}

0 commit comments

Comments
 (0)