Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions LayeredCraft.DynamoMapper.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<File Path="docs\examples\custom-converters.md" />
<File Path="docs\examples\index.md" />
<File Path="docs\examples\lambda-functions.md" />
<File Path="docs\examples\nested-mapping.md" />
<File Path="docs\examples\single-table-design.md" />
</Folder>
<Folder Name="/docs/getting-started/">
Expand All @@ -55,6 +56,7 @@
<Folder Name="/examples/">
<Project Path="examples/DynamoMapper.FieldLevelOverride/DynamoMapper.FieldLevelOverride.csproj" />
<Project Path="examples/DynamoMapper.MapperConstructor/DynamoMapper.MapperConstructor.csproj" />
<Project Path="examples/DynamoMapper.Nested/DynamoMapper.Nested.csproj" />
<Project Path="examples/DynamoMapper.SimpleExample/DynamoMapper.SimpleExample.csproj" />
</Folder>
<Folder Name="/git/">
Expand Down
22 changes: 21 additions & 1 deletion docs/advanced/diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Diagnostics

Documentation coming soon. See [Phase 1 Requirements](../roadmap/phase-1.md) for detailed specifications.
This page lists the diagnostics emitted by the generator and when they occur.

## Usage Diagnostics

| Id | Title | Trigger |
| --- | --- | --- |
| DM0001 | Type cannot be mapped to an AttributeValue | A property type is not supported for mapping. |
| DM0003 | Collection element type not supported | A collection element type is unsupported (non-primitive, non-nested). |
| DM0004 | Dictionary key must be string | A map property uses a non-string key type. |
| DM0005 | Incompatible DynamoKind override for collection | `DynamoKind` override does not match the inferred collection kind. |
| DM0006 | Circular reference detected in nested type | Nested object graphs contain a cycle (direct or indirect). |
| DM0007 | Unsupported nested member type | A nested property type cannot be mapped. |
| DM0008 | Invalid dot-notation path | A dot-notation override points to a missing property. |
| DM0101 | No mapper methods found | A `[DynamoMapper]` class has no `To*` or `From*` partial methods. |
| DM0102 | Mapper methods use different POCO types | `ToItem`/`FromItem` use different model types. |
| DM0103 | Multiple constructors marked with `[DynamoMapperConstructor]` | More than one constructor is attributed. |

## Notes

- Nested mapping cycles are detected during analysis and reported as `DM0006`.
- Dot-notation overrides are validated against the model graph and reported as `DM0008`.
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Nested object and nested collection mapping in the source generator.
- Mapper registry lookup with inline fallback for nested types.
- Diagnostics for nested cycles and invalid dot-notation paths (DM0006-DM0008).
- `examples/DynamoMapper.Nested` for end-to-end nested mapping scenarios.

### Planned
- Phase 1: Attribute-based mapping
- Phase 2: Fluent DSL configuration
Expand Down
1 change: 1 addition & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ These example projects show DynamoMapper features end-to-end.
- `examples/DynamoMapper.FieldLevelOverride` - Field-level overrides via `[DynamoField]`
- `examples/DynamoMapper.MapperConstructor` - Constructor/record support and
`[DynamoMapperConstructor]`
- `examples/DynamoMapper.Nested` - Nested objects and nested collections
62 changes: 62 additions & 0 deletions docs/examples/nested-mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Nested Mapping Example

The `examples/DynamoMapper.Nested` project demonstrates:

- Nested object mapping (inline and mapper-based)
- Nested collections (`List<T>`, arrays, `Dictionary<string, T>`)
- Dot-notation overrides for nested paths
- Cycle detection diagnostics

## Example: Nested Objects

```csharp
using System.Collections.Generic;
using Amazon.DynamoDBv2.Model;
using DynamoMapper.Runtime;

[DynamoMapper]
public static partial class OrderMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Order source);
public static partial Order FromItem(Dictionary<string, AttributeValue> item);
}

public class Order
{
public string Id { get; set; }
public Address ShippingAddress { get; set; }
}

public class Address
{
public string Line1 { get; set; }
public string City { get; set; }
}
```

## Example: Nested Collection

```csharp
public class Catalog
{
public string Id { get; set; }
public Dictionary<string, Product> Products { get; set; }
}

public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
```

## Dot-Notation Overrides

```csharp
[DynamoMapper]
[DynamoField("ShippingAddress.Line1", AttributeName = "addr_line1")]
[DynamoField("ShippingAddress.City", AttributeName = "addr_city")]
public static partial class OrderMapper { }
```

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

Expand Down
45 changes: 45 additions & 0 deletions docs/usage/basic-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,51 @@ When generating `FromItem`, DynamoMapper chooses between:
- **Constructor-based construction**: `new T(arg1, arg2, ...)` (optionally combined with an object
initializer for settable/`init` properties).

## Nested Object Mapping

DynamoMapper supports nested objects and nested collections when the nested types are mappable.

### How Nested Mapping Is Chosen

For each nested object property, the generator uses this decision order:

1. **Dot-notation overrides**: if there are overrides for the nested path, inline mapping is used.
2. **Mapper-based**: if a mapper exists for the nested type and it defines the required direction
(`ToItem` and/or `FromItem`), the nested mapper is used.
3. **Inline mapping**: otherwise, the nested type is inlined into the parent mapper.

### Nested Collections

Collections with nested element types are supported for:

- `List<T>` / `IEnumerable<T>` and arrays (`T[]`)
- `Dictionary<string, T>`

Nested collections of sets (SS/NS/BS) are not supported.

### Cycles

Nested object graphs cannot contain cycles. Cycles emit `DM0006`.

### Example

```csharp
public class Order
{
public string Id { get; set; }
public Address ShippingAddress { get; set; }
public List<LineItem> Items { get; set; }
}

public class Address
{
public string Line1 { get; set; }
public string City { get; set; }
}
```

See `examples/DynamoMapper.Nested` for a complete example.

## Constructor Mapping Rules (`FromItem`)

Constructor selection is deterministic and follows these priorities.
Expand Down
51 changes: 50 additions & 1 deletion docs/usage/field-configuration.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
# Field Configuration

Documentation coming soon. See [Phase 1 Requirements](../roadmap/phase-1.md) for detailed specifications.
Field configuration is handled through `[DynamoField]` attributes on the mapper class. These
attributes target model properties and allow you to override defaults without touching your domain
models.

## Basic Usage

```csharp
using DynamoMapper.Runtime;

[DynamoMapper]
[DynamoField(nameof(Product.Name), AttributeName = "productName", Required = true)]
[DynamoField(nameof(Product.Description), OmitIfNull = true, OmitIfEmptyString = true)]
public static partial class ProductMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Product source);
public static partial Product FromItem(Dictionary<string, AttributeValue> item);
}
```

## Nested Property Overrides (Dot Notation)

Use dot notation to override nested properties without adding attributes to nested types:

```csharp
[DynamoMapper]
[DynamoField("ShippingAddress.Line1", AttributeName = "addr_line1")]
[DynamoField("ShippingAddress.City", AttributeName = "addr_city")]
public static partial class OrderMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Order source);
public static partial Order FromItem(Dictionary<string, AttributeValue> item);
}
```

Notes:
- Dot-notation overrides force inline mapping for the nested path.
- Invalid paths emit `DM0008`.

## Supported Options

| Option | Description |
| --- | --- |
| `AttributeName` | Overrides the DynamoDB attribute name. |
| `Required` | Controls requiredness during `FromItem`. |
| `Kind` | Forces a specific `DynamoKind`. |
| `OmitIfNull` | Omits null values during `ToItem`. |
| `OmitIfEmptyString` | Omits empty strings during `ToItem`. |
| `ToMethod` | Uses a custom method to serialize a value. |
| `FromMethod` | Uses a custom method to deserialize a value. |
| `Format` | Overrides default format for date/time/enum conversions. |
27 changes: 27 additions & 0 deletions examples/DynamoMapper.Nested/DynamoMapper.Nested.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
ο»Ώ<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.DynamoDBv2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\..\src\LayeredCraft.DynamoMapper.Generators\LayeredCraft.DynamoMapper.Generators.csproj"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
/>
<ProjectReference
Include="..\..\src\LayeredCraft.DynamoMapper.Runtime\LayeredCraft.DynamoMapper.Runtime.csproj"
ReferenceOutputAssembly="true"
OutputItemType="Analyzer"
/>
</ItemGroup>
</Project>
38 changes: 38 additions & 0 deletions examples/DynamoMapper.Nested/MapperBasedNested.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Amazon.DynamoDBv2.Model;
using DynamoMapper.Runtime;

namespace DynamoMapper.Nested;

/// <summary>
/// Example: Mapper-based nested object - Author has its own mapper
/// </summary>
public record BlogPost
{
public string Slug { get; set; }
public string Title { get; set; }
public Author Writer { get; set; }
}

public record Author
{
public string Handle { get; set; }

Check warning on line 18 in examples/DynamoMapper.Nested/MapperBasedNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Handle' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string DisplayName { get; set; }

Check warning on line 19 in examples/DynamoMapper.Nested/MapperBasedNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'DisplayName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string Bio { get; set; }

Check warning on line 20 in examples/DynamoMapper.Nested/MapperBasedNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Bio' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}

// Author has its own mapper - BlogPostMapper will use this
[DynamoMapper]
public static partial class AuthorMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Author source);

public static partial Author FromItem(Dictionary<string, AttributeValue> item);
}

[DynamoMapper]
public static partial class BlogPostMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(BlogPost source);

public static partial BlogPost FromItem(Dictionary<string, AttributeValue> item);
}
35 changes: 35 additions & 0 deletions examples/DynamoMapper.Nested/MultiLevelNested.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Amazon.DynamoDBv2.Model;
using DynamoMapper.Runtime;

namespace DynamoMapper.Nested;

/// <summary>
/// Example: Multi-level nested objects - Company -> Department -> Manager
/// </summary>
public record Company
{
public string Id { get; set; }

Check warning on line 11 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string Name { get; set; }

Check warning on line 12 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public Department HeadOffice { get; set; }

Check warning on line 13 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'HeadOffice' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}

public record Department
{
public string Code { get; set; }

Check warning on line 18 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Code' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string Name { get; set; }

Check warning on line 19 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public Manager Lead { get; set; }

Check warning on line 20 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'Lead' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}

public record Manager
{
public string EmployeeId { get; set; }

Check warning on line 25 in examples/DynamoMapper.Nested/MultiLevelNested.cs

View workflow job for this annotation

GitHub Actions / build / build

Non-nullable property 'EmployeeId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string FullName { get; set; }
}

[DynamoMapper]
public static partial class CompanyMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Company source);

public static partial Company FromItem(Dictionary<string, AttributeValue> item);
}
29 changes: 29 additions & 0 deletions examples/DynamoMapper.Nested/NestedCollectionDictionary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Amazon.DynamoDBv2.Model;
using DynamoMapper.Runtime;

namespace DynamoMapper.Nested;

/// <summary>
/// Example: Dictionary with nested object values (inline generation)
/// </summary>
public record EmployeeDirectory
{
public string DepartmentId { get; set; }
public string DepartmentName { get; set; }
public Dictionary<string, Employee> Employees { get; set; }
}

public record Employee
{
public string Name { get; set; }
public string Title { get; set; }
public decimal Salary { get; set; }
}

[DynamoMapper]
public static partial class EmployeeDirectoryMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(EmployeeDirectory source);

public static partial EmployeeDirectory FromItem(Dictionary<string, AttributeValue> item);
}
Loading