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
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Comment thread
j-d-ha marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2025.3.3",
"commands": [
"jb"
],
"rollForward": false
}
}
}
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,6 @@ FodyWeavers.xsd

.idea

.config

# macOS
.DS_Store
.DS_Store?
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.3.0</VersionPrefix>
<VersionPrefix>1.4.0</VersionPrefix>
<!-- SPDX license identifier for MIT -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<!-- Other useful metadata -->
Expand Down
62 changes: 33 additions & 29 deletions docs/usage/customization-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,15 +501,30 @@ Hooks execute in a deterministic, predictable order:

### Zero-Cost Abstraction

Unimplemented hooks compile away completely:
The generator only emits hook calls when it detects a hook declaration in your mapper class. When no hooks are declared, `ToItem` is generated as a compact expression body with no overhead:

```csharp
// If no hooks are implemented:
// Generated when no hooks are declared:
public static partial Dictionary<string, AttributeValue> ToItem(Product source) =>
new Dictionary<string, AttributeValue>(1)
.SetGuid("productId", source.ProductId, false, true);
```

When a hook is declared (even without an implementation), the generator switches to block-body form and emits the call:

```csharp
// Generated when AfterToItem is declared:
public static partial Dictionary<string, AttributeValue> ToItem(Product source)
{
var item = new Dictionary<string, AttributeValue>(1);
item.SetGuid("productId", source.ProductId, false, true);
AfterToItem(source, item);
return item;
}
```

If you declare a hook but never implement it, C# `partial void` semantics remove the call at compile time β€” zero runtime overhead.

### No Reflection

Hooks are statically bound at compile time. No runtime discovery or reflection overhead.
Expand All @@ -520,34 +535,9 @@ Generated code reuses the same item dictionary instance across hook calls.

## DSL Integration (Phase 2)

In Phase 2, hooks can be configured via DSL (though partial methods remain the recommended approach):
> **Not yet implemented.** DSL-based hook configuration is planned for Phase 2.

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

static partial void Configure(DynamoMapBuilder<Order> map)
{
map.BeforeToItem((source, item) =>
{
// Limited DSL hook support
item.SetString("pk", $"CUSTOMER#{source.CustomerId}");
});
}

// Partial method hooks are still supported and recommended for complex logic
static partial void AfterToItem(Order source, Dictionary<string, AttributeValue> item)
{
item.SetString("sk", $"ORDER#{source.OrderId}");
item.SetString("recordType", "Order");
}
}
```

Note: DSL hooks have limited expression support. Partial method hooks are more powerful and flexible.
In Phase 2, hooks will also be configurable via a fluent DSL. Partial method hooks will remain the recommended approach for complex logic.

## Best Practices

Expand Down Expand Up @@ -616,8 +606,22 @@ Note: DSL hooks have limited expression support. Partial method hooks are more p
The generator validates hook signatures and emits diagnostics for common errors:

- **DM0401**: Hook signature doesn't match expected format
```csharp
// Wrong: too many parameters
static partial void AfterToItem(Product source, Dictionary<string, AttributeValue> item, string extra);
```

- **DM0402**: Hook method is not static
```csharp
// Wrong: missing static
partial void AfterToItem(Product source, Dictionary<string, AttributeValue> item);
```

- **DM0403**: Hook parameter types don't match entity type
```csharp
// Wrong: first parameter is string, not Product
static partial void AfterToItem(string source, Dictionary<string, AttributeValue> item);
```

## See Also

Expand Down
16 changes: 12 additions & 4 deletions skills/dynamo-mapper/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: dynamo-mapper
description: Use this skill when you need to write or explain DynamoMapper mappings for DynamoDB `AttributeValue` items in C#. It covers how to declare mapper classes, how `DynamoMapper`, `DynamoField`, `DynamoIgnore`, and `DynamoMapperConstructor` behave, what types and nested shapes are supported, how custom conversion really works, and how to troubleshoot DynamoMapper diagnostics and common gotchas without relying on stale docs.
description: Use this skill when you need to write or explain DynamoMapper mappings for DynamoDB `AttributeValue` items in C#. It covers how to declare mapper classes, how `DynamoMapper`, `DynamoField`, `DynamoIgnore`, and `DynamoMapperConstructor` behave, how lifecycle hooks work, what types and nested shapes are supported, how custom conversion really works, and how to troubleshoot DynamoMapper diagnostics and common gotchas without relying on stale docs.
---

# DynamoMapper
Expand All @@ -16,6 +16,8 @@ Use this skill when generating or explaining DynamoMapper code.
- One-way mappers are valid: `To*` only or `From*` only.
- Domain models usually stay clean except for optional `[DynamoMapperConstructor]` on a constructor.
- Nested object mapping is implemented and tested.
- Lifecycle hooks are implemented and validated (`BeforeToItem`, `AfterToItem`,
`BeforeFromItem`, `AfterFromItem`).
- Some public docs are stale; use `references/gotchas.md` when behavior seems surprising.

## Choose a path
Expand All @@ -25,28 +27,34 @@ Use this skill when generating or explaining DynamoMapper code.
- Read `references/type-matrix.md` for supported types, collection rules, nested shapes, and hard
limits.
- Read `references/diagnostics.md` for generator diagnostics and the most likely fixes.
- Read `references/hooks.md` for hook signatures, call order, generation behavior, and
hook-specific diagnostics.
- Read `references/gotchas.md` for stale-doc traps and the non-obvious rules most likely to cause
bad guidance.

## Default workflow

1. Identify whether the task is mapper authoring, supported-type lookup, or diagnostics.
2. Read the matching reference file before making assumptions.
3. If the task touches nested mapping, converters, or hooks, check `references/gotchas.md` before
3. If the task touches hooks, read `references/hooks.md` first.
4. If the task touches nested mapping or converters, check `references/gotchas.md` before
answering.
4. Keep answers concrete and code-oriented.
5. Keep answers concrete and code-oriented.

## High-risk misunderstandings

- Do not tell the user to decorate every POCO property; configuration belongs on the mapper class.
- Do not assume methods must be named exactly `ToItem` and `FromItem`; the `To`/`From` prefix
matters, but the generator also expects the recognized model/dictionary signatures.
- Check `references/gotchas.md` before teaching hooks or custom converter signatures.
- Do not invent hook signatures; all hooks must be `static partial void` with exact parameter
shapes.
- `AfterFromItem` requires `ref` on the entity parameter.
- Do not assume every unsupported converter setup becomes a DynamoMapper diagnostic; some become normal C# compile errors.

## Reference map

- `references/core-usage.md`
- `references/type-matrix.md`
- `references/diagnostics.md`
- `references/hooks.md`
- `references/gotchas.md`
24 changes: 23 additions & 1 deletion skills/dynamo-mapper/references/core-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ Use mapper-class static methods through `[DynamoField(ToMethod = ..., FromMethod
- stale docs describe the wrong converter signatures
- bad converter wiring may fail as normal C# compile errors instead of DynamoMapper diagnostics

## Lifecycle hooks

Hooks are optional extension points on the mapper class for pre/post mapping logic.

- `BeforeToItem(T source, Dictionary<string, AttributeValue> item)`
- `AfterToItem(T source, Dictionary<string, AttributeValue> item)`
- `BeforeFromItem(Dictionary<string, AttributeValue> item)`
- `AfterFromItem(Dictionary<string, AttributeValue> item, ref T entity)`

Rules:

- hooks must be `static partial void`
- hook names are exact (`BeforeToItem`, `AfterToItem`, `BeforeFromItem`, `AfterFromItem`)
- hooks can be declared/implemented in another part of the same partial mapper class
- one-way mappers only emit hooks for the generated direction
- no `To*` hooks means `To*` can stay expression-bodied

Order:

- `To*`: create item -> `BeforeToItem` -> property mapping -> `AfterToItem` -> return item
- `From*`: `BeforeFromItem` -> property mapping/object construction -> `AfterFromItem` -> return

## Requiredness and defaults

- missing required root scalar values throw at runtime
Expand All @@ -104,4 +126,4 @@ Use mapper-class static methods through `[DynamoField(ToMethod = ..., FromMethod
- Put configuration on the mapper, not the POCO.
- Use `[DynamoField]` before inventing extra mapping layers.
- Use `[DynamoMapperConstructor]` when constructor choice is ambiguous.
- Do not recommend lifecycle hooks as current behavior.
- Use lifecycle hooks for DynamoDB-specific concerns such as PK/SK composition, TTL, and metadata.
9 changes: 9 additions & 0 deletions skills/dynamo-mapper/references/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
- `DM0101` no mapper methods found -> add a valid `To*` or `From*` method
- `DM0102` mismatched model types -> make both directions use the same model type
- `DM0103` multiple constructor attributes -> leave only one `[DynamoMapperConstructor]`
- `DM0401` invalid hook signature -> use exact hook name/signature and `void` return type
- `DM0402` hook not static -> declare hook as `static`
- `DM0403` hook parameter type mismatch -> use the mapper model type `T` and expected dictionary/ref

## Hook diagnostics (all warnings)

- `DM0401` covers wrong parameter count, wrong `ref` usage, non-void return type, or non-partial hook
- `DM0402` is emitted when a hook method is not static
- `DM0403` is emitted when hook parameter types do not match mapper model type/dictionary shape

## Important non-diagnostic failure mode

Expand Down
6 changes: 3 additions & 3 deletions skills/dynamo-mapper/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- Do not tell users to decorate every domain-model property.
- Do not require methods to be named exactly `ToItem` and `FromItem`.
- Do not teach lifecycle hooks as currently implemented behavior.
- Do not invent lifecycle hook signatures from memory.
- Do not use the old property-level converter signatures from stale docs.
- Do not assume every converter mistake becomes a DynamoMapper diagnostic.

Expand All @@ -22,13 +22,13 @@
## Stale-doc corrections

- nested mapping is supported
- hook docs are stale for current behavior
- lifecycle hooks are implemented with strict signature validation
- static converter docs are stale on signatures and constraints
- some prose docs mention diagnostics that do not exist

## If unsure

- prefer simple mapper classes
- prefer supported scalar and collection shapes
- avoid promising hook behavior
- use the exact four hook names and signatures when hooks are needed
- avoid inventing converter signatures from memory
42 changes: 42 additions & 0 deletions skills/dynamo-mapper/references/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Hooks

## Supported lifecycle hooks

Hooks are optional `static partial void` methods on the mapper class.

- `BeforeToItem(T source, Dictionary<string, AttributeValue> item)`
- `AfterToItem(T source, Dictionary<string, AttributeValue> item)`
- `BeforeFromItem(Dictionary<string, AttributeValue> item)`
- `AfterFromItem(Dictionary<string, AttributeValue> item, ref T entity)`

Where `T` is the mapper model type.

## Generation behavior

- hooks are discovered by exact method names above
- hooks can be declared and implemented in separate parts of the same partial mapper class
- one-way mappers are supported; only hooks for generated directions are emitted
- no `To*` hooks keeps `To*` expression-bodied
- any `To*` hook switches `To*` generation to block body

## Execution order

- `To*`: create dictionary -> `BeforeToItem` -> map members -> `AfterToItem` -> return
- `From*`: `BeforeFromItem` -> map/construct model -> `AfterFromItem` -> return

## Validation diagnostics

- `DM0401` invalid hook signature
- wrong parameter count
- wrong `ref` usage (`AfterFromItem` must use `ref` on entity)
- non-void return type
- non-partial hook method
- `DM0402` hook is not static
- `DM0403` hook parameter types do not match mapper model/dictionary requirements

These diagnostics are warnings.

## Safe guidance

- use hooks for DynamoDB-specific concerns (PK/SK composition, TTL, metadata, normalization)
- keep hook logic focused; avoid placing domain business workflows in hooks
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ DM0009 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error |
DM0101 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
DM0102 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
DM0103 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|------
DM0401 | LayeredCraft.DynamoMapper.Usage | Warning | Hook signature doesn't match expected format
DM0402 | LayeredCraft.DynamoMapper.Usage | Warning | Hook method is not static
DM0403 | LayeredCraft.DynamoMapper.Usage | Warning | Hook parameter types don't match entity type
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ internal static class DiagnosticDescriptors
{
private const string UsageCategory = "LayeredCraft.DynamoMapper.Usage";

// Diagnostic ID ranges:
// DM000x: Property and type mapping diagnostics
// DM010x: Mapper and model-shape diagnostics
// DM040x: Hook declaration diagnostics

internal static readonly DiagnosticDescriptor CannotConvertFromAttributeValue =
new(
"DM0001",
Expand Down Expand Up @@ -115,4 +120,34 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Error,
true
);

internal static readonly DiagnosticDescriptor InvalidHookSignature =
Comment thread
j-d-ha marked this conversation as resolved.
new(
"DM0401",
"Hook signature doesn't match expected format",
"The method '{0}' does not match the expected hook signature for '{1}'",
UsageCategory,
DiagnosticSeverity.Warning,
true
);

internal static readonly DiagnosticDescriptor HookNotStatic =
new(
"DM0402",
"Hook method is not static",
"The hook method '{0}' must be declared as static",
UsageCategory,
DiagnosticSeverity.Warning,
true
);

internal static readonly DiagnosticDescriptor HookParameterTypeMismatch =
Comment thread
j-d-ha marked this conversation as resolved.
new(
"DM0403",
"Hook parameter types don't match entity type",
"The hook method '{0}' parameter types must match the entity type '{1}'",
UsageCategory,
DiagnosticSeverity.Warning,
true
);
}
Loading
Loading