Converters
A converter is a class that converts a .NET type to and from DynamoDB JSON or low-level Document
object. A custom converter allows working with unsupported types or overriding the default converter behavior.
Converters, along with with DynamoDB JSON parsing, are one of the most critical components from a performance perspective.
All EfficientDynamoDb built-in converters are optimized separately for both Document
and JSON conversion, in order to allocate no additional memory.
Built-in converters
EfficientDynamoDb does not require specifying a converter explicitly for the following built-in types:
- Classes
- Strings
- Numbers:
byte
,short
,int
,long
,decimal
,float
,double
,ushort
,uint
,ulong
- Enums (saved as numbers)
- DateTimes (saved in ISO8601 format)
- Guids
- Booleans
- Collections: arrays, lists, dictionaries, sets (including their read-only and mutable interfaces)
- Binary data:
byte[]
,Memory<byte>
,ReadOnlyMemory<byte>
AttributeValue
structs (low-level API representation of the DynamoDB attribute)
In addition, you can use one of the following converters to change the default behavior:
StringEnumDdbConverter<T>
- saves enums as strings instead of numbers.DateTimeDdbConverter
- allows to customizeDateTime
formatting parameters:Format
,DateTimeStyles
andCultureInfo
.SdkDateTimeDdbConverter
- makesDateTime
behavior backward compatible with the official AWS SDK.
Applying converters
For a property
[DynamoDbProperty("address", typeof(CompositeAddressConverter))]
public Address Address { get; set; }
For a type
[DynamoDbConverter(typeof(CompositeAddressConverter))]
public struct Address { ... }
For a context
var config = new DynamoDbContextConfig(regionEndpoint, awsCredentials)
{
Converters = new[] {new CompositeAddressConverter()}
};
If a converter can't be instantiated in advance and depends on the target value type, a custom converter factory can be implemented by inheriting from the DdbConverterFactory
class and registering it with the context the same way as other custom converters.
For example, a string enum converter factory can be defined like this:
public sealed class StringEnumDdbConverterFactory : DdbConverterFactory
{
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
public override DdbConverter CreateConverter(Type typeToConvert, DynamoDbContextMetadata metadata)
{
return (DdbConverter) Activator.CreateInstance(typeof(StringEnumDdbConverter<>).MakeGenericType(typeToConvert));
}
}
Basic converter
To create a custom converter:
- Inherit from
DdbConverter<TValue>
class. - Implement both
Read
andWrite
methods.
public class CompositeAddressConverter : DdbConverter<Address>
{
// Converts DynamoDb attribute to the .NET type
public override Address Read(in AttributeValue attributeValue)
{
var parts = attributeValue.AsString().Split('#');
return new Address(parts[0], parts[1]);
}
// Converts .NET type to the DynamoDb attribute
public override AttributeValue Write(ref Address address)
{
return new StringAttributeValue($"{address.Country}#{address.Street}");
}
}
Check out the working with documents chapter to better understand how to work with attribute values.
Direct JSON converter
Not all .NET types map nicely to DynamoDb attributes. Creation of intermediate AttributeValue
struct can involve unnecessary allocations that can be avoided by reading/writing directly into JSON buffer.
In case when a custom type can't be converted to the AttributeValue
without allocations, it is possible to implement two additional low-level Read
and Write
methods that work with JSON buffers.
During deserialization/serialization of entities to JSON, more optimized low-level implementations will be called.
public class CustomIntConverter : DdbConverter<int>
{
// Efficient zero-allocation JSON to int conversion
public override int Read(ref DdbReader reader)
{
if (!Utf8Parser.TryParse(reader.JsonReader.ValueSpan, out int value, out _))
throw new DdbException($"Couldn't parse int ddb value from '{reader.JsonReader.GetString()}'.");
return value;
}
// Efficient zero-allocation int to JSON conversion
public override void Write(in DdbWriter writer, ref int value)
{
writer.JsonWriter.WriteStartObject();
writer.JsonWriter.WriteString(DdbTypeNames.Number, value);
writer.JsonWriter.WriteEndObject();
}
public override int Read(in AttributeValue attributeValue) => attributeValue.AsNumberAttribute().ToInt();
public override AttributeValue Write(ref int value) => new NumberAttributeValue(value.ToString());
}
EfficientDynamoDb uses System.Text.Json
for all JSON manipulations.
JSON reading
When a low-level read is called, DdbReader.JsonReader
is already pointed to the JSON value. Current attribute type is automatically parsed and can be accessed using DdbReader.AttributeType
property.
The reader.JsonReader.HasValueSequence
is guaranteed to be false at this point, so it's safe to use reader.JsonReader.ValueSpan
to access the JSON buffer.
The DdbReader.JsonReader.Read
method should not be explicitly called unless you are writing a converter for a non-scalar DynamoDB data type - i.e., a map, list or set.
JSON writing
When a low-level write is called, a converter has to write DynamoDb JSON, including the attribute type.
DdbWriter
class provides various simplified overloads that write attribute types automatically. But in case if suitable overload does not exist, the attribute type has to be written manually like in the CustomIntConverter
example above.
Sparse converters
Sparse converters don't save certain values and completely remove an attribute instead. It is a powerful concept that can be used for various purposes like size savings or to conditionally include an entity in the GSI.
By default, all built-in converters act as sparse converters when it comes to handling null
values, meaning that null
properties are never saved and the entire attribute is deleted.
To add an additional sparse condition, ShouldWrite
method has to be overridden. For example. here is a simple sparse int converter:
public class SparseIntConverter : DdbConverter<int>
{
public override bool ShouldWrite(ref int value) => value != 0;
...
}
Note: Sparse converters don't remove attributes when they are part of a Dictionary
class.
Set converters
Both string and number sets store values as strings in the DB.
To store a custom type inside a set, a converter should implement the ISetValueConverter<T>
interface:
public class CustomDdbConverter : DdbConverter<CustomType>, ISetValueConverter<CustomType>
{
public string WriteStringValue(ref CustomType value) => value.ToString();
// Optionally implement direct write method
public void WriteStringValue(in DdbWriter ddbWriter, ref CustomType value) =>
ddbWriter.JsonWriter.WriteStringValue(value.AsSpan());
}
Dictionary key converters
To store a custom type as a dictionary key, a converter should implement the IDicitonaryKeyConverter<T>
interface:
public class CustomDdbConverter : DdbConverter<CustomType>, ISetValueConverter<CustomType>
{
public string WriteStringValue(ref CustomType value) => value.ToString();
// Optionally implement direct write method
public void WritePropertyName(in DdbWriter ddbWriter, ref CustomType value) =>
ddbWriter.JsonWriter.WritePropertyName(value.AsSpan());
}