Summary
Add mapper-level converter registration so a DynamoMapper can reuse custom T <-> AttributeValue conversions anywhere an exact CLR type appears in that mapper's object graph.
This is a compile-time source-generator feature. It should complement the existing field-level ToMethod / FromMethod escape hatch, not replace it.
This issue supersedes #22. That earlier issue describes a field-level Converter = typeof(...) design, but the desired design here is mapper-level registration with directional resolution and fallback to existing code paths.
Problem
Today, reusable converter types are not supported.
The current workaround is field-specific static methods configured with ToMethod / FromMethod, which works for one property at a time but does not solve the "always use this conversion for this CLR type everywhere in this mapper" case.
That becomes painful when the same unsupported type appears in multiple places, for example:
- top-level properties
- nested object properties
- nullable wrappers
- list elements
- dictionary values
Example:
public readonly record struct OrderStatus(string Value);
If a mapper wants to store OrderStatus as a DynamoDB string everywhere it appears, there is currently no mapper-level reusable way to register that conversion once and have it apply consistently across the mapper graph.
Current implementation context
The current implementation supports only field-level custom static methods:
DynamoFieldAttribute exposes ToMethod / FromMethod, but no converter-type property.
- The generator short-circuits custom handling only when both field-level methods are present.
- Unsupported member types currently fall back to existing diagnostics such as
DM0001.
This issue should introduce a separate reusable converter mechanism rather than extending DynamoField for per-field converter types.
Proposed runtime API
Add a repeated mapper-level attribute:
[DynamoMapper]
[DynamoConverter(typeof(OrderStatusConverter))]
public static partial class OrderMapper
{
public static partial Dictionary<string, AttributeValue> ToItem(Order order);
public static partial Order FromItem(Dictionary<string, AttributeValue> item);
}
Add directional converter interfaces:
public interface IDynamoToConverter<T>
{
AttributeValue ToAttributeValue(T value);
}
public interface IDynamoFromConverter<T>
{
T FromAttributeValue(AttributeValue value);
}
public interface IDynamoConverter<T> : IDynamoToConverter<T>, IDynamoFromConverter<T>;
Example converter:
public sealed class OrderStatusConverter : IDynamoConverter<OrderStatus>
{
public AttributeValue ToAttributeValue(OrderStatus value) => new() { S = value.Value };
public OrderStatus FromAttributeValue(AttributeValue value) => new(value.S);
}
Resolution rules
Resolution must be directional.
For ToItem:
- field-level
ToMethod / FromMethod override wins for that property
- otherwise use a registered
IDynamoToConverter<T> for the exact underlying CLR type
- otherwise use the existing built-in
To path
- otherwise emit a generator error for that property
For FromItem:
- field-level
ToMethod / FromMethod override wins for that property
- otherwise use a registered
IDynamoFromConverter<T> for the exact underlying CLR type
- otherwise use the existing built-in
From path
- otherwise emit a generator error for that property
Important details:
- matching is exact type match only
- a registered converter wins over built-in handling for the direction it supports
- the opposite direction may still fall back to built-in handling if no converter exists for that side
- converters apply to the entire mapper object graph, including nested object properties
Nullability
Do not introduce new null semantics.
Converters should plug into the existing null handling paths.
That means:
- registration is for the underlying non-null type, e.g.
OrderStatus
OrderStatus? should flow through the existing nullable handling path
- no separate converter registration for
OrderStatus? is required
Supported composition
A registered converter should work anywhere the overall shape is already supported today, including:
- scalar properties
- nullable scalar properties
- nested object properties
List<T>, arrays, IEnumerable<T>
Dictionary<string, T>
A converter must not relax existing shape rules.
Examples:
List<StrongId> should work if StrongId has a converter
Dictionary<string, StrongId> should work if StrongId has a converter
List<List<StrongId>> remains unsupported if nested lists are unsupported today
- currently invalid nested/container shapes remain invalid
HashSet<T> / ISet<T> should not gain converter-based support in v1
Converter validation rules
Registered converter types should be validated at generator time.
A valid converter type must be:
- concrete
- non-abstract
- accessible from generated mapper code
- constructible via an accessible parameterless constructor
- implementing at least one valid converter interface
Interface rules:
- fixed method names are required:
ToAttributeValue(T value)
FromAttributeValue(AttributeValue value)
- a converter class may target exactly one underlying CLR type
- all implemented converter interfaces on the class must refer to that same CLR type
These should be generator errors:
- type implements no valid converter interface
- type mixes converter interfaces for different CLR types
- type is inaccessible
- type is abstract
- type has no accessible parameterless constructor
Multiple registrations
Multiple registrations for the same CLR type should be allowed only when their directions do not overlap.
Valid:
[DynamoConverter(typeof(OrderStatusToConverter))]
[DynamoConverter(typeof(OrderStatusFromConverter))]
when one implements only IDynamoToConverter<OrderStatus> and the other implements only IDynamoFromConverter<OrderStatus>.
Invalid:
- two registrations both implementing
IDynamoToConverter<OrderStatus>
- two registrations both implementing
IDynamoFromConverter<OrderStatus>
Direction overlap for the same CLR type should be a generator error.
Valid but unused registrations should be ignored silently.
Code generation expectations
Generated code should instantiate converters with new MyConverter() and cache them in static fields on the generated mapper.
This implies v1 supports parameterless, stateless converter types only.
Diagnostics
This feature should distinguish between:
Registration diagnostics
Bad converter registration should fail on the registration/type, for example:
- invalid converter interface shape
- multiple CLR types in one converter class
- direction overlap for the same CLR type
- inaccessible converter type
- missing accessible parameterless constructor
Usage diagnostics
If a converter registration is valid, but a specific property still cannot be mapped in one direction because:
- no converter exists for that direction, and
- built-in mapping also cannot handle it
then the diagnostic should be reported on the property usage, not the registration.
Why this is mapper-level only
Field-level converter registration is not needed for this feature because DynamoMapper already has field-specific customization via ToMethod / FromMethod.
This proposal covers the separate use case of:
register one reusable conversion for a CLR type and use it everywhere that type appears in this mapper.
Out of scope for v1
- field-level converter registration
- DI-based activation
- runtime registration
- assembly scanning
- inheritance/interface lookup
- open generic converters
- stateful converters
- changing existing container/set shape rules
- converter support for DynamoDB set handling (
HashSet<T>, ISet<T>)
Acceptance criteria
- A mapper can register one or more converter types with
[DynamoConverter(typeof(...))]
- A registered converter applies everywhere the exact underlying CLR type appears in that mapper graph
- Field-level
ToMethod / FromMethod still overrides mapper-level converter registration
- One-way mappers can use one-way converters
- Two-way mappers can mix converter-based handling in one direction with built-in handling in the other direction
- Nullable wrappers use existing null handling and do not require nullable-specific registrations
- Supported container shapes can compose with converters for element/value types
- Unsupported shapes remain unsupported
- Direction overlap for the same CLR type produces a generator error
- Invalid converter registrations produce generator errors
- Valid but unused registrations are ignored silently
- The issue body contains enough design detail to implement the feature without re-deciding the core behavior
Suggested implementation areas
Likely implementation work spans:
- runtime API additions for
DynamoConverterAttribute and converter interfaces
- generator parsing/binding of mapper-level converter registrations
- converter validation and diagnostics
- directional type-strategy resolution updates
- code rendering for cached converter usage
- verify tests for happy paths, precedence, direction overlap, fallback, and diagnostics
- documentation updates for the new mapper-level converter model
Summary
Add mapper-level converter registration so a
DynamoMappercan reuse customT <-> AttributeValueconversions anywhere an exact CLR type appears in that mapper's object graph.This is a compile-time source-generator feature. It should complement the existing field-level
ToMethod/FromMethodescape hatch, not replace it.This issue supersedes #22. That earlier issue describes a field-level
Converter = typeof(...)design, but the desired design here is mapper-level registration with directional resolution and fallback to existing code paths.Problem
Today, reusable converter types are not supported.
The current workaround is field-specific static methods configured with
ToMethod/FromMethod, which works for one property at a time but does not solve the "always use this conversion for this CLR type everywhere in this mapper" case.That becomes painful when the same unsupported type appears in multiple places, for example:
Example:
If a mapper wants to store
OrderStatusas a DynamoDB string everywhere it appears, there is currently no mapper-level reusable way to register that conversion once and have it apply consistently across the mapper graph.Current implementation context
The current implementation supports only field-level custom static methods:
DynamoFieldAttributeexposesToMethod/FromMethod, but no converter-type property.DM0001.This issue should introduce a separate reusable converter mechanism rather than extending
DynamoFieldfor per-field converter types.Proposed runtime API
Add a repeated mapper-level attribute:
Add directional converter interfaces:
Example converter:
Resolution rules
Resolution must be directional.
For
ToItem:ToMethod/FromMethodoverride wins for that propertyIDynamoToConverter<T>for the exact underlying CLR typeTopathFor
FromItem:ToMethod/FromMethodoverride wins for that propertyIDynamoFromConverter<T>for the exact underlying CLR typeFrompathImportant details:
Nullability
Do not introduce new null semantics.
Converters should plug into the existing null handling paths.
That means:
OrderStatusOrderStatus?should flow through the existing nullable handling pathOrderStatus?is requiredSupported composition
A registered converter should work anywhere the overall shape is already supported today, including:
List<T>, arrays,IEnumerable<T>Dictionary<string, T>A converter must not relax existing shape rules.
Examples:
List<StrongId>should work ifStrongIdhas a converterDictionary<string, StrongId>should work ifStrongIdhas a converterList<List<StrongId>>remains unsupported if nested lists are unsupported todayHashSet<T>/ISet<T>should not gain converter-based support in v1Converter validation rules
Registered converter types should be validated at generator time.
A valid converter type must be:
Interface rules:
ToAttributeValue(T value)FromAttributeValue(AttributeValue value)These should be generator errors:
Multiple registrations
Multiple registrations for the same CLR type should be allowed only when their directions do not overlap.
Valid:
when one implements only
IDynamoToConverter<OrderStatus>and the other implements onlyIDynamoFromConverter<OrderStatus>.Invalid:
IDynamoToConverter<OrderStatus>IDynamoFromConverter<OrderStatus>Direction overlap for the same CLR type should be a generator error.
Valid but unused registrations should be ignored silently.
Code generation expectations
Generated code should instantiate converters with
new MyConverter()and cache them in static fields on the generated mapper.This implies v1 supports parameterless, stateless converter types only.
Diagnostics
This feature should distinguish between:
Registration diagnostics
Bad converter registration should fail on the registration/type, for example:
Usage diagnostics
If a converter registration is valid, but a specific property still cannot be mapped in one direction because:
then the diagnostic should be reported on the property usage, not the registration.
Why this is mapper-level only
Field-level converter registration is not needed for this feature because DynamoMapper already has field-specific customization via
ToMethod/FromMethod.This proposal covers the separate use case of:
Out of scope for v1
HashSet<T>,ISet<T>)Acceptance criteria
[DynamoConverter(typeof(...))]ToMethod/FromMethodstill overrides mapper-level converter registrationSuggested implementation areas
Likely implementation work spans:
DynamoConverterAttributeand converter interfaces