How It Works¶
DynamoMapper is an incremental source generator that produces high-performance, type-safe mapping code at compile time. This guide explains the core concepts, architecture, and design decisions that make DynamoMapper both powerful and focused.
Core Philosophy¶
DynamoMapper is built on three fundamental principles:
- Domain Stays Clean - Your domain models remain free of persistence attributes
- Compile-Time Safety - All mapping code is generated and validated at compile time
- DynamoDB-Focused - Single-purpose library for DynamoDB attribute mapping
Mapping Scope¶
DynamoMapper is a DynamoDB-specific mapping library, not a general-purpose object mapper.
What DynamoMapper Does¶
DynamoMapper supports exactly two mapping directions:
// ToItem: Domain model → DynamoDB item
Dictionary<string, AttributeValue> ToItem(T source);
// FromItem: DynamoDB item → Domain model
T FromItem(Dictionary<string, AttributeValue> item);
What DynamoMapper Does NOT Do¶
Unlike general-purpose mappers (Mapperly, AutoMapper, etc.), DynamoMapper does not support:
- Object-to-object mapping (e.g.,
DTO → Entity) - Collection transformations
- Projection mapping
- Runtime mapping configuration
- Multi-step mapping pipelines
Why this matters: This focused scope enables DynamoMapper to optimize specifically for DynamoDB patterns like single-table design, PK/SK composition, and attribute bags without compromising on performance or clarity.
Source Generation Overview¶
DynamoMapper uses .NET's IIncrementalGenerator API to analyze your code at compile time and generate mapping implementations.
The Generation Pipeline¶
- Discovery Phase
- Locate classes marked with
[DynamoMapper] - Find partial mapping methods (
ToItem,FromItem) -
Collect configuration attributes
-
Analysis Phase
- Resolve target entity types
- Analyze properties (public, readable, writable)
- Apply naming conventions
-
Validate converters and hooks
-
Code Generation Phase
- Generate
ToItemimplementation - Generate
FromItemimplementation -
Emit diagnostics for configuration errors
-
Compilation Phase
- Generated code is compiled with your project
- No runtime dependencies beyond AWS SDK types
Mapper Anatomy¶
Basic Structure¶
using Amazon.DynamoDBv2.Model;
[DynamoMapper(Convention = DynamoNamingConvention.CamelCase)]
public static partial class ProductMapper
{
// Partial method declarations (you provide)
public static partial Dictionary<string, AttributeValue> ToItem(Product source);
public static partial Product FromItem(Dictionary<string, AttributeValue> item);
// Generated implementations (DynamoMapper provides)
// - ToItem implementation
// - FromItem implementation
}
Why Static Partial Classes?¶
- Static - No instance state, no object allocation overhead
- Partial - You declare, generator implements
- Type-safe - Compiler validates everything
Generated Code Example¶
Given this domain model:
public class Product
{
public Guid ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
[DynamoMapper(Convention = DynamoNamingConvention.CamelCase)]
public static partial class ProductMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Product source);
public static partial Product FromItem(Dictionary<string, AttributeValue> item);
}
DynamoMapper generates:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
using DynamoMapper.Runtime;
using System.Collections.Generic;
using Amazon.DynamoDBv2.Model;
namespace MyApp;
public static partial class ProductMapper
{
[global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")]
public static partial global::System.Collections.Generic.Dictionary<string, global::Amazon.DynamoDBv2.Model.AttributeValue> ToItem(global::MyApp.Product source) =>
new Dictionary<string, AttributeValue>(3)
.SetGuid("productId", source.ProductId, false, true)
.SetString("name", source.Name, false, true)
.SetDecimal("price", source.Price, false, true);
[global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")]
public static partial global::MyApp.Product FromItem(global::System.Collections.Generic.Dictionary<string, global::Amazon.DynamoDBv2.Model.AttributeValue> item)
{
var product = new global::MyApp.Product
{
ProductId = item.GetGuid("productId", Requiredness.InferFromNullability),
Name = item.GetString("name", Requiredness.InferFromNullability),
Price = item.GetDecimal("price", Requiredness.InferFromNullability),
};
return product;
}
}
Object Construction (FromItem)¶
When generating FromItem, DynamoMapper chooses between:
- Property-based construction:
new T { Prop = ..., ... } - Constructor-based construction:
new T(arg1, arg2, ...)(optionally with an object initializer)
Constructor-based construction is used for records/record structs with primary constructors and for classes where read-only properties must be populated through a constructor. You can also explicitly choose a constructor using [DynamoMapperConstructor].
See Basic Mapping for the full selection rules.
Key Characteristics¶
- Direct property access - No reflection
- Culture-invariant parsing - Consistent number handling
- Capacity hints - Reduces dictionary reallocations
- Clear, readable code - Easy to debug
Configuration Model¶
DynamoMapper uses a layered configuration model:
1. Mapper-Level Defaults¶
[DynamoMapper(
Convention = DynamoNamingConvention.CamelCase,
OmitNullStrings = true,
DateTimeFormat = "O")]
2. Property-Level Overrides¶
[DynamoMapper]
[DynamoField(nameof(Product.Name), 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);
}
3. Converters¶
Custom conversions are currently supported via static methods on the mapper class:
[DynamoMapper]
[DynamoField(nameof(Product.Status),
ToMethod = nameof(ToStatus),
FromMethod = nameof(FromStatus))]
public static partial class ProductMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Product source);
public static partial Product FromItem(Dictionary<string, AttributeValue> item);
static AttributeValue ToStatus(ProductStatus status) => new() { S = status.ToString() };
static ProductStatus FromStatus(AttributeValue value) => Enum.Parse<ProductStatus>(value.S);
}
4. Customization Hooks¶
static partial void AfterToItem(Product source, Dictionary<string, AttributeValue> item)
{
item["pk"] = new AttributeValue { S = $"PRODUCT#{source.ProductId}" };
item["sk"] = new AttributeValue { S = "METADATA" };
}
Type System¶
Supported Types (Phase 1)¶
DynamoMapper natively supports:
| .NET Type | DynamoDB Type | Notes |
|---|---|---|
string | S (String) | |
int, long, decimal, double | N (Number) | Culture-invariant |
bool | BOOL | |
Guid | S | ToString/Parse |
DateTime, DateTimeOffset, TimeSpan | S | ISO-8601 / constant format |
enum | S | String name |
| Nullable variants | S/N/BOOL | Null checks generated |
| Collections | L/M/SS/NS/BS | Lists, maps, and sets of supported element types |
Custom Types¶
Use static conversion methods for custom types:
public class OrderStatus
{
public string Name { get; }
// ... enumeration pattern
}
[DynamoMapper]
[DynamoField(nameof(Order.Status), ToMethod = nameof(ToStatus), FromMethod = nameof(FromStatus))]
public static partial class OrderMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Order source);
public static partial Order FromItem(Dictionary<string, AttributeValue> item);
static AttributeValue ToStatus(OrderStatus value) => new() { S = value.Name };
static OrderStatus FromStatus(AttributeValue value) => OrderStatus.FromName(value.S);
}
Naming Conventions¶
DynamoMapper applies naming conventions to property names:
// .NET Property → DynamoDB Attribute Name
// Exact
ProductId → ProductId
// CamelCase (recommended)
ProductId → productId
CustomerId → customerId
// SnakeCase
ProductId → product_id
CustomerId → customer_id
Override per-property:
Extensibility Model¶
Hooks: First-Class Extension Points¶
Hooks enable DynamoDB-specific patterns without compromising the focused mapping scope:
// Before property mapping
static partial void BeforeToItem(Product source, Dictionary<string, AttributeValue> item);
// After property mapping - most common
static partial void AfterToItem(Product source, Dictionary<string, AttributeValue> item);
// Before deserialization
static partial void BeforeFromItem(Dictionary<string, AttributeValue> item);
// After object construction
static partial void AfterFromItem(Dictionary<string, AttributeValue> item, ref Product entity);
Common use cases: - PK/SK composition for single-table design - Record type discrimination - TTL attributes - Unmapped attribute bags - Post-hydration normalization
Why Hooks Instead of Pipeline Stages?¶
DynamoMapper uses hooks instead of a general mapping pipeline because:
- Focused scope - Only two mapping directions, not arbitrary transformations
- Zero overhead - Unimplemented hooks compile away completely
- Type safety - Statically bound, no reflection
- DynamoDB patterns - Designed specifically for single-table design
Diagnostics¶
DynamoMapper provides comprehensive compile-time diagnostics:
Error Classes¶
- DM000x - Mapping/usage errors (unsupported types, invalid overrides)
- DM010x - Mapper discovery issues (e.g., missing mapper methods)
Example Diagnostic¶
error DM0201: Static conversion method 'ToStatus' not found on mapper 'OrderMapper'
Location: OrderMapper.cs(12,5)
Fix: Add static method: static AttributeValue ToStatus(OrderStatus value)
Performance Characteristics¶
Compile-Time¶
- Incremental generation - Only affected mappers regenerate
- Deterministic output - Same input always produces same code
- Fast compilation - No expensive analysis
Runtime¶
- Zero reflection - All types resolved at compile time
- Minimal allocations - Dictionary capacity hints, no LINQ
- Inlined conversions - Simple type conversions inlined by JIT
- Culture-invariant parsing - Consistent, fast number handling
Benchmarks (Typical)¶
| Operation | Time | Allocations |
|---|---|---|
| ToItem (5 properties) | ~50ns | 1 (dictionary) |
| FromItem (5 properties) | ~100ns | 1 (entity) |
| With hooks | +5-10ns | 0 additional |
Design Constraints¶
Understanding what DynamoMapper intentionally does NOT support:
No Runtime Configuration¶
// Not supported - all configuration is compile-time
mapper.Configure(x => x.Property("Name").Ignore());
Why: Compile-time configuration enables: - Zero reflection - Faster runtime performance - Compile-time validation
No Dynamic Mapping¶
Why: Type-safe, analyzable, debuggable generated code.
No Nested Object Mapping (Phase 1)¶
// Not supported in Phase 1
public class Order
{
public Customer Customer { get; set; } // Nested object
}
Why: Phase 1 focuses on scalar properties. Use static conversion methods for complex types.
Phase 2: DSL Configuration¶
Phase 2 is planned and will add an optional fluent DSL while maintaining all Phase 1 capabilities:
[DynamoMapper]
public static partial class ProductMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Product source);
public static partial Product FromItem(Dictionary<string, AttributeValue> item);
static partial void Configure(DynamoMapBuilder<Product> map)
{
map.Naming(DynamoNamingConvention.CamelCase);
map.Property(x => x.Status)
.Using(nameof(ToStatus), nameof(FromStatus));
map.Ignore(x => x.ComputedProperty);
}
}
Key points: - DSL is optional - attributes remain fully supported - DSL is compile-time only - no runtime evaluation - DSL and attributes can coexist - DSL takes precedence
Best Practices¶
- Use default conventions - Override only when necessary
- Keep domain clean - No DynamoDB concerns in domain models
- Use hooks for DynamoDB patterns - PK/SK, TTL, record types
- Use static methods for conversions - Simple, co-located, explicit
See Also¶
- Customization Hooks - Extending mapping behavior
- Static Conversion Methods - Custom type conversion
- Phase 1 Requirements - Complete Phase 1 specification
- Phase 2 Requirements - DSL configuration