diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/Attributes/DynamoConverterAttributeHandler.cs b/src/Clients/Goa.Clients.Dynamo.Generator/Attributes/DynamoConverterAttributeHandler.cs
new file mode 100644
index 00000000..dbd4e90c
--- /dev/null
+++ b/src/Clients/Goa.Clients.Dynamo.Generator/Attributes/DynamoConverterAttributeHandler.cs
@@ -0,0 +1,44 @@
+using Microsoft.CodeAnalysis;
+using Goa.Clients.Dynamo.Generator.Models;
+
+namespace Goa.Clients.Dynamo.Generator.Attributes;
+
+///
+/// Handles the DynamoConverterAttribute.
+///
+public class DynamoConverterAttributeHandler : IAttributeHandler
+{
+ public string AttributeTypeName => "Goa.Clients.Dynamo.DynamoConverterAttribute";
+
+ public bool CanHandle(AttributeData attributeData)
+ {
+ return attributeData.AttributeClass?.ToDisplayString() == AttributeTypeName;
+ }
+
+ public AttributeInfo? ParseAttribute(AttributeData attributeData)
+ {
+ if (!CanHandle(attributeData))
+ {
+ return null;
+ }
+
+ // Extract converter type from constructor arg
+ if (attributeData.ConstructorArguments.Length > 0 &&
+ attributeData.ConstructorArguments[0].Value is INamedTypeSymbol converterType)
+ {
+ return new DynamoConverterAttributeInfo
+ {
+ AttributeData = attributeData,
+ AttributeTypeName = AttributeTypeName,
+ ConverterTypeName = converterType.ToDisplayString()
+ };
+ }
+
+ return null;
+ }
+
+ public void ValidateAttribute(AttributeInfo attributeInfo, ISymbol symbol, Action reportDiagnostic)
+ {
+ // No additional validation needed at this stage
+ }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/JsonMapperGenerator.cs b/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/JsonMapperGenerator.cs
new file mode 100644
index 00000000..a44966d1
--- /dev/null
+++ b/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/JsonMapperGenerator.cs
@@ -0,0 +1,1490 @@
+using Microsoft.CodeAnalysis;
+using Goa.Clients.Dynamo.Generator.Models;
+using Goa.Clients.Dynamo.Generator.TypeHandlers;
+
+namespace Goa.Clients.Dynamo.Generator.CodeGeneration;
+
+///
+/// Generates DynamoJsonMapper classes for directly reading/writing DynamoDB JSON wire format
+/// to/from entities, bypassing intermediate DynamoRecord allocations.
+///
+public class JsonMapperGenerator : ICodeGenerator
+{
+ private readonly TypeHandlerRegistry _typeHandlerRegistry;
+ private int _variableCounter;
+
+ public JsonMapperGenerator(TypeHandlerRegistry typeHandlerRegistry)
+ {
+ _typeHandlerRegistry = typeHandlerRegistry;
+ }
+
+ public string GenerateCode(IEnumerable types, GenerationContext context)
+ {
+ var typesByNamespace = types.GroupBy(t => t.Namespace).Where(x => x.Any()).ToList();
+
+ if (!typesByNamespace.Any())
+ return string.Empty;
+
+ var builder = new CodeBuilder();
+ builder.AppendLine("#nullable enable");
+ builder.AppendLine("using System;");
+ builder.AppendLine("using System.Buffers.Text;");
+ builder.AppendLine("using System.Collections.Generic;");
+ builder.AppendLine("using System.Globalization;");
+ builder.AppendLine("using System.Text.Json;");
+
+ foreach (var ns in typesByNamespace)
+ {
+ var targetNamespace = string.IsNullOrEmpty(ns.Key) ? "Generated" : ns.Key;
+ builder.AppendLine();
+ builder.AppendLine($"namespace {targetNamespace}");
+ builder.OpenBrace();
+ {
+ builder.OpenBraceWithLine("public static class DynamoJsonMapper");
+
+ foreach (var type in ns)
+ {
+ builder.AppendLine($"// {type.FullName}");
+ GenerateTypeMapper(builder, type, context);
+ }
+
+ builder.CloseBrace();
+ }
+ builder.CloseBrace();
+ }
+
+ return builder.ToString();
+ }
+
+ private void GenerateTypeMapper(CodeBuilder builder, DynamoTypeInfo type, GenerationContext context)
+ {
+ var dynamoModelAttr = type.Attributes.OfType().FirstOrDefault();
+
+ // Skip abstract types that don't have concrete subtypes and don't have [DynamoModel] directly
+ if (type.IsAbstract && !HasConcreteSubtypes(type, context) && dynamoModelAttr == null)
+ return;
+
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(type.Name);
+
+ builder.AppendLine();
+ builder.OpenBraceWithLine($"public static class {normalizedTypeName}");
+
+ GenerateWriteToJson(builder, type, context);
+ GenerateReadFromJson(builder, type, context);
+
+ builder.CloseBrace();
+ }
+
+ // ─── WriteToJson ────────────────────────────────────────────────────
+
+ private void GenerateWriteToJson(CodeBuilder builder, DynamoTypeInfo type, GenerationContext context)
+ {
+ builder.AppendLine();
+ builder.OpenBraceWithLine($"public static void WriteToJson(System.Text.Json.Utf8JsonWriter writer, {type.FullName} model)");
+
+ if (type.IsAbstract && HasConcreteSubtypes(type, context))
+ {
+ GenerateAbstractWriteDispatch(builder, type, context);
+ }
+ else
+ {
+ GenerateConcreteWriteToJson(builder, type, context);
+ }
+
+ builder.CloseBrace();
+ }
+
+ private void GenerateAbstractWriteDispatch(CodeBuilder builder, DynamoTypeInfo type, GenerationContext context)
+ {
+ builder.OpenBraceWithLine("switch (model)");
+
+ if (context.TypeRegistry.TryGetValue(type.FullName, out var concreteTypes))
+ {
+ foreach (var concreteType in concreteTypes)
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(concreteType.Name);
+ builder.AppendLine($"case {concreteType.FullName} concrete:");
+ builder.Indent();
+ builder.AppendLine($"DynamoJsonMapper.{normalizedTypeName}.WriteToJson(writer, concrete);");
+ builder.AppendLine("return;");
+ builder.Unindent();
+ }
+ }
+
+ builder.AppendLine("default:");
+ builder.Indent();
+ builder.AppendLine($"throw new InvalidOperationException($\"Unknown concrete type: {{model.GetType().FullName}} for abstract type {type.FullName}\");");
+ builder.Unindent();
+ builder.CloseBrace();
+ }
+
+ private void GenerateConcreteWriteToJson(CodeBuilder builder, DynamoTypeInfo type, GenerationContext context)
+ {
+ builder.AppendLine("writer.WriteStartObject();");
+
+ // Add type discriminator for inheritance
+ var needsDiscriminator = type.IsAbstract || HasConcreteSubtypes(type, context) || InheritsFromAbstractType(type);
+ if (needsDiscriminator)
+ {
+ var dynamoModelAttr = type.Attributes.OfType().FirstOrDefault()
+ ?? GetInheritedDynamoModelAttribute(type);
+ var typeNameField = (dynamoModelAttr?.TypeName != "Type" ? dynamoModelAttr?.TypeName : null)
+ ?? GetInheritedDynamoModelAttribute(type)?.TypeName
+ ?? "Type";
+
+ builder.AppendLine($"writer.WritePropertyName(\"{typeNameField}\");");
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", \"{type.FullName}\"); writer.WriteEndObject();");
+ }
+
+ var allProperties = GetAllProperties(type);
+ var supportedProperties = allProperties
+ .Where(p => (p.ConverterTypeName != null || _typeHandlerRegistry.CanHandle(p)) && !p.IsIgnored(IgnoreDirection.WhenWriting));
+
+ foreach (var property in supportedProperties)
+ {
+ var attrName = property.GetDynamoAttributeName();
+ GenerateWriteProperty(builder, property, attrName, "model");
+ }
+
+ builder.AppendLine("writer.WriteEndObject();");
+ }
+
+ private void GenerateWriteProperty(CodeBuilder builder, PropertyInfo property, string attrName, string modelVar)
+ {
+ var accessExpr = $"{modelVar}.{property.Name}";
+ var underlyingType = property.UnderlyingType;
+ var isNullable = property.IsNullable;
+
+ // Custom converter handling - delegate entirely to the converter
+ if (property.ConverterTypeName != null)
+ {
+ builder.AppendLine($"writer.WritePropertyName(\"{attrName}\");");
+ builder.AppendLine($"new {property.ConverterTypeName}().Write(writer, {accessExpr});");
+ return;
+ }
+
+ // Check for [UnixTimestamp] attribute
+ var hasUnixTimestamp = property.Attributes.Any(a => a is UnixTimestampAttributeInfo);
+
+ // Dictionary handling
+ if (property.IsDictionary && property.DictionaryTypes.HasValue)
+ {
+ GenerateWriteDictionary(builder, property, attrName, modelVar);
+ return;
+ }
+
+ // Collection handling
+ if (property.IsCollection && property.ElementType != null)
+ {
+ GenerateWriteCollection(builder, property, attrName, modelVar);
+ return;
+ }
+
+ // Complex type handling (non-primitive, non-collection)
+ if (IsComplexType(underlyingType))
+ {
+ GenerateWriteComplexType(builder, property, attrName, modelVar);
+ return;
+ }
+
+ // Primitive handling
+ builder.AppendLine($"writer.WritePropertyName(\"{attrName}\");");
+
+ if (isNullable)
+ {
+ var nullCheck = $"{accessExpr} != null";
+
+ builder.OpenBraceWithLine($"if ({nullCheck})");
+ EmitPrimitiveTypeWrapper(builder, property, isNullable, accessExpr, hasUnixTimestamp);
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ }
+ else
+ {
+ // Non-nullable strings are reference types that could still be null at runtime
+ if (underlyingType.SpecialType == SpecialType.System_String)
+ {
+ builder.OpenBraceWithLine($"if ({accessExpr} != null)");
+ EmitPrimitiveTypeWrapper(builder, property, isNullable, accessExpr, hasUnixTimestamp);
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ }
+ else
+ {
+ EmitPrimitiveTypeWrapper(builder, property, isNullable, accessExpr, hasUnixTimestamp);
+ }
+ }
+ }
+
+ private void EmitPrimitiveTypeWrapper(CodeBuilder builder, PropertyInfo property, bool isNullable, string accessExpr, bool hasUnixTimestamp)
+ {
+ var underlyingType = property.UnderlyingType;
+ var valueAccess = isNullable && underlyingType.SpecialType != SpecialType.System_String ? $"{accessExpr}.Value" : accessExpr;
+
+ // UnixTimestamp DateTime/DateTimeOffset -> N
+ if (hasUnixTimestamp)
+ {
+ var unixAttr = property.Attributes.OfType().First();
+ var isMilliseconds = unixAttr.Format == UnixTimestampFormat.Milliseconds;
+ var method = isMilliseconds ? "ToUnixTimeMilliseconds" : "ToUnixTimeSeconds";
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"N\", ((DateTimeOffset){valueAccess}).{method}().ToString()); writer.WriteEndObject();");
+ return;
+ }
+
+ switch (underlyingType.SpecialType)
+ {
+ case SpecialType.System_String:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}); writer.WriteEndObject();");
+ break;
+
+ case SpecialType.System_Byte:
+ case SpecialType.System_SByte:
+ case SpecialType.System_Int16:
+ case SpecialType.System_UInt16:
+ case SpecialType.System_Int32:
+ case SpecialType.System_UInt32:
+ case SpecialType.System_Int64:
+ case SpecialType.System_UInt64:
+ case SpecialType.System_Decimal:
+ case SpecialType.System_Single:
+ case SpecialType.System_Double:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"N\", {valueAccess}.ToString(CultureInfo.InvariantCulture)); writer.WriteEndObject();");
+ break;
+
+ case SpecialType.System_Boolean:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteBoolean(\"BOOL\", {valueAccess}); writer.WriteEndObject();");
+ break;
+
+ case SpecialType.System_Char:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString()); writer.WriteEndObject();");
+ break;
+
+ case SpecialType.System_DateTime:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString(\"o\")); writer.WriteEndObject();");
+ break;
+
+ default:
+ if (underlyingType.Name == "DateTimeOffset")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString(\"o\")); writer.WriteEndObject();");
+ else if (underlyingType.Name == "Guid")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString()); writer.WriteEndObject();");
+ else if (underlyingType.Name == "TimeSpan")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString()); writer.WriteEndObject();");
+ else if (underlyingType.Name == "DateOnly")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString(\"yyyy-MM-dd\")); writer.WriteEndObject();");
+ else if (underlyingType.Name == "TimeOnly")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString(\"HH:mm:ss.fffffff\")); writer.WriteEndObject();");
+ else if (underlyingType.TypeKind == TypeKind.Enum)
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString()); writer.WriteEndObject();");
+ else
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {valueAccess}.ToString()); writer.WriteEndObject();");
+ break;
+ }
+ }
+
+ private void GenerateWriteComplexType(CodeBuilder builder, PropertyInfo property, string attrName, string modelVar)
+ {
+ var accessExpr = $"{modelVar}.{property.Name}";
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(property.UnderlyingType.Name);
+
+ builder.AppendLine($"writer.WritePropertyName(\"{attrName}\");");
+ builder.OpenBraceWithLine($"if ({accessExpr} != null)");
+ builder.AppendLine("writer.WriteStartObject(); writer.WritePropertyName(\"M\");");
+ builder.AppendLine($"DynamoJsonMapper.{normalizedTypeName}.WriteToJson(writer, {accessExpr});");
+ builder.AppendLine("writer.WriteEndObject();");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ }
+
+ private void GenerateWriteCollection(CodeBuilder builder, PropertyInfo property, string attrName, string modelVar)
+ {
+ var accessExpr = $"{modelVar}.{property.Name}";
+ var elementType = property.ElementType!;
+
+ builder.AppendLine($"writer.WritePropertyName(\"{attrName}\");");
+
+ // Determine if this is a set type (HashSet, ISet, IReadOnlySet)
+ var isSetType = IsSetType(property.Type);
+
+ // String set -> SS
+ if (isSetType && elementType.SpecialType == SpecialType.System_String)
+ {
+ builder.OpenBraceWithLine($"if ({accessExpr} != null)");
+ builder.AppendLine("writer.WriteStartObject(); writer.WritePropertyName(\"SS\"); writer.WriteStartArray();");
+ builder.AppendLine($"foreach (var item in {accessExpr}) writer.WriteStringValue(item);");
+ builder.AppendLine("writer.WriteEndArray(); writer.WriteEndObject();");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ return;
+ }
+
+ // Number set -> NS
+ if (isSetType && IsNumericType(elementType))
+ {
+ builder.OpenBraceWithLine($"if ({accessExpr} != null)");
+ builder.AppendLine("writer.WriteStartObject(); writer.WritePropertyName(\"NS\"); writer.WriteStartArray();");
+ builder.AppendLine($"foreach (var item in {accessExpr}) writer.WriteStringValue(item.ToString(CultureInfo.InvariantCulture));");
+ builder.AppendLine("writer.WriteEndArray(); writer.WriteEndObject();");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ return;
+ }
+
+ // General list -> L
+ builder.OpenBraceWithLine($"if ({accessExpr} != null)");
+ builder.AppendLine("writer.WriteStartObject(); writer.WritePropertyName(\"L\"); writer.WriteStartArray();");
+ builder.OpenBraceWithLine($"foreach (var item in {accessExpr})");
+ EmitWriteElementValue(builder, elementType, "item");
+ builder.CloseBrace();
+ builder.AppendLine("writer.WriteEndArray(); writer.WriteEndObject();");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ }
+
+ ///
+ /// Emits a single type-wrapped value for an element inside a List (L type).
+ ///
+ private void EmitWriteElementValue(CodeBuilder builder, ITypeSymbol elementType, string varName)
+ {
+ if (IsComplexType(elementType))
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(elementType.Name);
+ builder.AppendLine($"writer.WriteStartObject(); writer.WritePropertyName(\"M\");");
+ builder.AppendLine($"DynamoJsonMapper.{normalizedTypeName}.WriteToJson(writer, {varName});");
+ builder.AppendLine("writer.WriteEndObject();");
+ return;
+ }
+
+ switch (elementType.SpecialType)
+ {
+ case SpecialType.System_String:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}); writer.WriteEndObject();");
+ break;
+ case SpecialType.System_Byte:
+ case SpecialType.System_SByte:
+ case SpecialType.System_Int16:
+ case SpecialType.System_UInt16:
+ case SpecialType.System_Int32:
+ case SpecialType.System_UInt32:
+ case SpecialType.System_Int64:
+ case SpecialType.System_UInt64:
+ case SpecialType.System_Decimal:
+ case SpecialType.System_Single:
+ case SpecialType.System_Double:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"N\", {varName}.ToString(CultureInfo.InvariantCulture)); writer.WriteEndObject();");
+ break;
+ case SpecialType.System_Boolean:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteBoolean(\"BOOL\", {varName}); writer.WriteEndObject();");
+ break;
+ case SpecialType.System_Char:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString()); writer.WriteEndObject();");
+ break;
+ case SpecialType.System_DateTime:
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString(\"o\")); writer.WriteEndObject();");
+ break;
+ default:
+ if (elementType.Name == "DateTimeOffset")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString(\"o\")); writer.WriteEndObject();");
+ else if (elementType.Name == "Guid")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString()); writer.WriteEndObject();");
+ else if (elementType.Name == "TimeSpan")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString()); writer.WriteEndObject();");
+ else if (elementType.Name == "DateOnly")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString(\"yyyy-MM-dd\")); writer.WriteEndObject();");
+ else if (elementType.Name == "TimeOnly")
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString(\"HH:mm:ss.fffffff\")); writer.WriteEndObject();");
+ else if (elementType.TypeKind == TypeKind.Enum)
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString()); writer.WriteEndObject();");
+ else
+ builder.AppendLine($"writer.WriteStartObject(); writer.WriteString(\"S\", {varName}.ToString()); writer.WriteEndObject();");
+ break;
+ }
+ }
+
+ private void GenerateWriteDictionary(CodeBuilder builder, PropertyInfo property, string attrName, string modelVar)
+ {
+ var accessExpr = $"{modelVar}.{property.Name}";
+ var dictionaryTypes = property.DictionaryTypes!.Value;
+ var keyType = dictionaryTypes.KeyType;
+ var valueType = dictionaryTypes.ValueType;
+
+ // Only string-keyed dictionaries are supported (DynamoDB limitation)
+ if (keyType.SpecialType != SpecialType.System_String)
+ {
+ builder.AppendLine($"writer.WritePropertyName(\"{attrName}\");");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ return;
+ }
+
+ builder.AppendLine($"writer.WritePropertyName(\"{attrName}\");");
+ builder.OpenBraceWithLine($"if ({accessExpr} != null)");
+ builder.AppendLine("writer.WriteStartObject(); writer.WritePropertyName(\"M\"); writer.WriteStartObject();");
+ builder.OpenBraceWithLine($"foreach (var kvp in {accessExpr})");
+ builder.AppendLine("writer.WritePropertyName(kvp.Key);");
+ EmitWriteTypeWrappedValue(builder, valueType, "kvp.Value");
+ builder.CloseBrace();
+ builder.AppendLine("writer.WriteEndObject(); writer.WriteEndObject();");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("writer.WriteStartObject(); writer.WriteBoolean(\"NULL\", true); writer.WriteEndObject();");
+ builder.CloseBrace();
+ }
+
+ ///
+ /// Emits a type-wrapped value (the {"S": ...} / {"N": ...} etc. wrapper) for a given value expression.
+ /// Used for dictionary values and similar contexts.
+ ///
+ private void EmitWriteTypeWrappedValue(CodeBuilder builder, ITypeSymbol valueType, string valueExpr)
+ {
+ if (IsComplexType(valueType))
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(valueType.Name);
+ builder.AppendLine($"writer.WriteStartObject(); writer.WritePropertyName(\"M\");");
+ builder.AppendLine($"DynamoJsonMapper.{normalizedTypeName}.WriteToJson(writer, {valueExpr});");
+ builder.AppendLine("writer.WriteEndObject();");
+ return;
+ }
+
+ // Reuse the element write logic (same type-wrapper patterns)
+ EmitWriteElementValue(builder, valueType, valueExpr);
+ }
+
+ // ─── ReadFromJson ───────────────────────────────────────────────────
+
+ private void GenerateReadFromJson(CodeBuilder builder, DynamoTypeInfo type, GenerationContext context)
+ {
+ var dynamoModelAttr = type.Attributes.OfType().FirstOrDefault();
+ if (dynamoModelAttr == null)
+ dynamoModelAttr = GetInheritedDynamoModelAttribute(type);
+
+ var inheritsDynamoModel = HasInheritedDynamoModel(type);
+ var shouldGenerate = dynamoModelAttr != null ||
+ (type.IsAbstract && HasConcreteSubtypes(type, context)) ||
+ (!type.IsAbstract && dynamoModelAttr == null && !inheritsDynamoModel) ||
+ inheritsDynamoModel;
+
+ if (!shouldGenerate)
+ return;
+
+ builder.AppendLine();
+ builder.OpenBraceWithLine($"public static {type.FullName} ReadFromJson(ref System.Text.Json.Utf8JsonReader reader)");
+
+ // Reset variable counter for each method
+ _variableCounter = 0;
+
+ if (type.IsAbstract && HasConcreteSubtypes(type, context))
+ {
+ GenerateAbstractReadDispatch(builder, type, context);
+ }
+ else
+ {
+ GenerateConcreteReadFromJson(builder, type);
+ }
+
+ builder.CloseBrace();
+ }
+
+ private void GenerateAbstractReadDispatch(CodeBuilder builder, DynamoTypeInfo type, GenerationContext context)
+ {
+ var dynamoModelAttr = type.Attributes.OfType().FirstOrDefault()
+ ?? GetInheritedDynamoModelAttribute(type);
+ var typeNameField = dynamoModelAttr?.TypeName ?? "Type";
+
+ // For abstract types we need to peek at the type discriminator.
+ // We copy the reader, scan for the discriminator field, then dispatch.
+ builder.AppendLine("// Copy reader to peek at type discriminator");
+ builder.AppendLine("var readerCopy = reader;");
+ builder.AppendLine("string? typeDiscriminator = null;");
+ builder.AppendLine("if (readerCopy.TokenType != JsonTokenType.StartObject)");
+ builder.Indent().AppendLine("readerCopy.Read();").Unindent();
+ builder.OpenBraceWithLine("while (readerCopy.Read())");
+ builder.AppendLine("if (readerCopy.TokenType == JsonTokenType.EndObject) break;");
+ builder.AppendLine("if (readerCopy.TokenType != JsonTokenType.PropertyName) continue;");
+ builder.AppendLine("var peekPropName = readerCopy.GetString();");
+ builder.OpenBraceWithLine($"if (peekPropName == \"{typeNameField}\")");
+ builder.AppendLine("readerCopy.Read(); // StartObject of {\"S\": ...}");
+ builder.AppendLine("readerCopy.Read(); // \"S\"");
+ builder.AppendLine("readerCopy.Read(); // value");
+ builder.AppendLine("typeDiscriminator = readerCopy.GetString();");
+ builder.AppendLine("break;");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("readerCopy.Read(); // move to value");
+ builder.AppendLine("readerCopy.Skip(); // skip the value");
+ builder.CloseBrace();
+ builder.CloseBrace();
+ builder.AppendLine();
+ builder.AppendLine($"if (typeDiscriminator == null)");
+ builder.Indent().AppendLine($"throw new InvalidOperationException(\"Missing {typeNameField} discriminator for abstract type {type.FullName}\");").Unindent();
+ builder.AppendLine();
+ builder.OpenBraceWithLine("return typeDiscriminator switch");
+
+ if (context.TypeRegistry.TryGetValue(type.FullName, out var concreteTypes))
+ {
+ foreach (var concreteType in concreteTypes)
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(concreteType.Name);
+ builder.AppendLine($"\"{concreteType.FullName}\" => DynamoJsonMapper.{normalizedTypeName}.ReadFromJson(ref reader),");
+ }
+ }
+
+ builder.AppendLine($"_ => throw new InvalidOperationException($\"Unknown type: {{typeDiscriminator}} for abstract type {type.FullName}\")");
+ builder.CloseBrace().Append(";");
+ builder.AppendLine();
+ }
+
+ private void GenerateConcreteReadFromJson(CodeBuilder builder, DynamoTypeInfo type)
+ {
+ builder.AppendLine("if (reader.TokenType != JsonTokenType.StartObject)");
+ builder.Indent().AppendLine("reader.Read();").Unindent();
+ builder.AppendLine();
+
+ var constructor = FindBestConstructor(type);
+ var hasInitOnlyProperties = GetAllProperties(type)
+ .Any(p => p.Symbol?.SetMethod is { IsInitOnly: true });
+
+ if (constructor != null && (constructor.Parameters.Any() || hasInitOnlyProperties))
+ {
+ GenerateConstructorReadFromJson(builder, type, constructor);
+ }
+ else
+ {
+ GenerateParameterlessReadFromJson(builder, type);
+ }
+ }
+
+ private void GenerateParameterlessReadFromJson(CodeBuilder builder, DynamoTypeInfo type)
+ {
+ builder.AppendLine($"var result = new {type.FullName}();");
+ builder.OpenBraceWithLine("while (reader.Read())");
+ builder.AppendLine("if (reader.TokenType == JsonTokenType.EndObject) break;");
+ builder.AppendLine();
+
+ var allProperties = GetAllProperties(type);
+ var readableProperties = allProperties
+ .Where(p => (p.ConverterTypeName != null || _typeHandlerRegistry.CanHandle(p)) && !p.IsIgnored(IgnoreDirection.WhenReading) && p.Symbol?.SetMethod != null && IsSupportedForJsonRead(p))
+ .ToList();
+
+ var isFirst = true;
+ foreach (var property in readableProperties)
+ {
+ var attrName = property.GetDynamoAttributeName();
+ var keyword = isFirst ? "if" : "else if";
+ builder.OpenBraceWithLine($"{keyword} (reader.ValueTextEquals(\"{attrName}\"u8))");
+ GenerateReadProperty(builder, property, "result");
+ builder.CloseBrace();
+ isFirst = false;
+ }
+
+ if (readableProperties.Count > 0)
+ {
+ builder.OpenBraceWithLine("else");
+ }
+ builder.AppendLine("reader.Read(); // Move past property name to value");
+ builder.AppendLine("reader.Skip(); // Skip the type wrapper object");
+ if (readableProperties.Count > 0)
+ {
+ builder.CloseBrace();
+ }
+
+ builder.CloseBrace(); // while
+ builder.AppendLine("return result;");
+ }
+
+ private void GenerateConstructorReadFromJson(CodeBuilder builder, DynamoTypeInfo type, IMethodSymbol constructor)
+ {
+ var allProperties = GetAllProperties(type);
+
+ // Build mapping of constructor parameters to properties
+ var ctorParams = constructor.Parameters;
+ var ctorParamProperties = new List<(IParameterSymbol Param, PropertyInfo? Property)>();
+ var ctorParamNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var param in ctorParams)
+ {
+ var matchedProperty = FindPropertyIgnoreCase(type, param.Name);
+ ctorParamProperties.Add((param, matchedProperty));
+ if (matchedProperty != null)
+ ctorParamNames.Add(matchedProperty.Name);
+ }
+
+ // Readable properties: include properties that match constructor params (even without setter)
+ // or properties that have a setter
+ var readableProperties = allProperties
+ .Where(p => (p.ConverterTypeName != null || _typeHandlerRegistry.CanHandle(p))
+ && !p.IsIgnored(IgnoreDirection.WhenReading)
+ && (p.Symbol?.SetMethod != null || ctorParamNames.Contains(p.Name))
+ && IsSupportedForJsonRead(p))
+ .ToList();
+
+ // Additional settable properties: not matched to constructor params, and have a setter
+ var additionalProperties = readableProperties
+ .Where(p => !ctorParamNames.Contains(p.Name) && p.Symbol?.SetMethod != null)
+ .ToList();
+
+ // Declare local variables for all readable properties
+ foreach (var property in readableProperties)
+ {
+ var varName = GetLocalVarName(property.Name);
+ var typeName = GetFullyQualifiedTypeName(property);
+ var defaultExpr = GetDefaultValueExpression(property.Type, property.IsNullable);
+ builder.AppendLine($"{typeName} {varName} = {defaultExpr};");
+ }
+
+ builder.AppendLine();
+ builder.OpenBraceWithLine("while (reader.Read())");
+ builder.AppendLine("if (reader.TokenType == JsonTokenType.EndObject) break;");
+ builder.AppendLine();
+
+ var isFirst = true;
+ foreach (var property in readableProperties)
+ {
+ var attrName = property.GetDynamoAttributeName();
+ var keyword = isFirst ? "if" : "else if";
+ builder.OpenBraceWithLine($"{keyword} (reader.ValueTextEquals(\"{attrName}\"u8))");
+ GenerateReadProperty(builder, property, "result", assignmentTarget: GetLocalVarName(property.Name));
+ builder.CloseBrace();
+ isFirst = false;
+ }
+
+ if (readableProperties.Count > 0)
+ {
+ builder.OpenBraceWithLine("else");
+ }
+ builder.AppendLine("reader.Read(); // Move past property name to value");
+ builder.AppendLine("reader.Skip(); // Skip the type wrapper object");
+ if (readableProperties.Count > 0)
+ {
+ builder.CloseBrace();
+ }
+
+ builder.CloseBrace(); // while
+
+ // Build constructor arguments
+ var readablePropertyNames = new HashSet(readableProperties.Select(p => p.Name));
+ var ctorArgs = new List();
+ foreach (var (param, matchedProperty) in ctorParamProperties)
+ {
+ if (matchedProperty != null && readablePropertyNames.Contains(matchedProperty.Name))
+ {
+ ctorArgs.Add(GetLocalVarName(matchedProperty.Name));
+ }
+ else
+ {
+ // Use default! for non-nullable reference types to suppress nullable warnings
+ var paramType = param.Type;
+ var isParamNullable = paramType.NullableAnnotation == NullableAnnotation.Annotated;
+ ctorArgs.Add(!paramType.IsValueType && !isParamNullable ? "default!" : "default");
+ }
+ }
+
+ var ctorArgString = string.Join(", ", ctorArgs);
+
+ if (additionalProperties.Count > 0)
+ {
+ builder.AppendLine($"return new {type.FullName}({ctorArgString})");
+ builder.AppendLine("{");
+ builder.Indent();
+ foreach (var prop in additionalProperties)
+ {
+ builder.AppendLine($"{prop.Name} = {GetLocalVarName(prop.Name)},");
+ }
+ builder.Unindent();
+ builder.AppendLine("};");
+ }
+ else
+ {
+ builder.AppendLine($"return new {type.FullName}({ctorArgString});");
+ }
+ }
+
+ private void GenerateReadProperty(CodeBuilder builder, PropertyInfo property, string resultVar, string? assignmentTarget = null)
+ {
+ var underlyingType = property.UnderlyingType;
+ var isNullable = property.IsNullable;
+ var hasUnixTimestamp = property.Attributes.Any(a => a is UnixTimestampAttributeInfo);
+
+ // Custom converter handling - delegate entirely to the converter
+ if (property.ConverterTypeName != null)
+ {
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+ builder.AppendLine("reader.Read(); // Move past property name to value");
+ builder.AppendLine($"{target} = new {property.ConverterTypeName}().Read(ref reader);");
+ return;
+ }
+
+ // Dictionary handling
+ if (property.IsDictionary && property.DictionaryTypes.HasValue)
+ {
+ GenerateReadDictionary(builder, property, resultVar, assignmentTarget);
+ return;
+ }
+
+ // Collection handling
+ if (property.IsCollection && property.ElementType != null)
+ {
+ GenerateReadCollection(builder, property, resultVar, assignmentTarget);
+ return;
+ }
+
+ // Complex type handling
+ if (IsComplexType(underlyingType))
+ {
+ GenerateReadComplexType(builder, property, resultVar, assignmentTarget);
+ return;
+ }
+
+ // Primitive handling: read the type wrapper object
+ builder.AppendLine("reader.Read(); // StartObject of type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor (S, N, BOOL, NULL, etc.)");
+
+ if (isNullable)
+ {
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+ // Check if NULL descriptor using zero-allocation UTF-8 comparison
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"NULL\"u8))");
+ builder.AppendLine("reader.Read(); // value (true)");
+ builder.AppendLine($"{target} = null;");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value");
+ EmitReadPrimitiveAssignment(builder, property, resultVar, hasUnixTimestamp, assignmentTarget);
+ builder.CloseBrace();
+ }
+ else
+ {
+ // Non-nullable strings are reference types that could still be null at runtime.
+ // The write path emits {"NULL":true} for these, so the read path must handle it.
+ if (underlyingType.SpecialType == SpecialType.System_String)
+ {
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"NULL\"u8))");
+ builder.AppendLine("reader.Read(); // value (true)");
+ builder.AppendLine($"{target} = string.Empty;");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value");
+ EmitReadPrimitiveAssignment(builder, property, resultVar, hasUnixTimestamp, assignmentTarget);
+ builder.CloseBrace();
+ }
+ else
+ {
+ builder.AppendLine("reader.Read(); // value");
+ EmitReadPrimitiveAssignment(builder, property, resultVar, hasUnixTimestamp, assignmentTarget);
+ }
+ }
+
+ builder.AppendLine("reader.Read(); // EndObject of type wrapper");
+ }
+
+ private void EmitReadPrimitiveAssignment(CodeBuilder builder, PropertyInfo property, string resultVar, bool hasUnixTimestamp, string? assignmentTarget = null)
+ {
+ var underlyingType = property.UnderlyingType;
+ var propAccess = assignmentTarget ?? $"{resultVar}.{property.Name}";
+
+ // UnixTimestamp DateTime/DateTimeOffset -> read N as number
+ if (hasUnixTimestamp)
+ {
+ var unixAttr = property.Attributes.OfType().First();
+ var isMilliseconds = unixAttr.Format == UnixTimestampFormat.Milliseconds;
+ if (underlyingType.Name == "DateTimeOffset")
+ {
+ var method = isMilliseconds ? "FromUnixTimeMilliseconds" : "FromUnixTimeSeconds";
+ builder.AppendLine($"{propAccess} = DateTimeOffset.{method}(long.Parse(reader.GetString()!, CultureInfo.InvariantCulture));");
+ }
+ else
+ {
+ var method = isMilliseconds ? "FromUnixTimeMilliseconds" : "FromUnixTimeSeconds";
+ builder.AppendLine($"{propAccess} = DateTimeOffset.{method}(long.Parse(reader.GetString()!, CultureInfo.InvariantCulture)).UtcDateTime;");
+ }
+ return;
+ }
+
+ // Try Utf8Parser for numeric types to avoid string allocation
+ var utf8Expr = GetUtf8NumericParseExpression(underlyingType, property.Name);
+ if (utf8Expr != null)
+ {
+ builder.AppendLine($"{utf8Expr};");
+ builder.AppendLine($"{propAccess} = {property.Name}Parsed;");
+ return;
+ }
+
+ switch (underlyingType.SpecialType)
+ {
+ case SpecialType.System_String:
+ builder.AppendLine($"{propAccess} = reader.GetString()!;");
+ break;
+ case SpecialType.System_Boolean:
+ builder.AppendLine($"{propAccess} = reader.GetBoolean();");
+ break;
+ case SpecialType.System_Char:
+ builder.AppendLine($"{propAccess} = reader.GetString()![0];");
+ break;
+ case SpecialType.System_DateTime:
+ builder.AppendLine($"{propAccess} = DateTime.ParseExact(reader.GetString()!, \"o\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);");
+ break;
+ default:
+ if (underlyingType.Name == "DateTimeOffset")
+ builder.AppendLine($"{propAccess} = DateTimeOffset.ParseExact(reader.GetString()!, \"o\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);");
+ else if (underlyingType.Name == "Guid")
+ builder.AppendLine($"{propAccess} = Guid.Parse(reader.GetString()!);");
+ else if (underlyingType.Name == "TimeSpan")
+ builder.AppendLine($"{propAccess} = TimeSpan.Parse(reader.GetString()!, CultureInfo.InvariantCulture);");
+ else if (underlyingType.Name == "DateOnly")
+ builder.AppendLine($"{propAccess} = DateOnly.Parse(reader.GetString()!, CultureInfo.InvariantCulture);");
+ else if (underlyingType.Name == "TimeOnly")
+ builder.AppendLine($"{propAccess} = TimeOnly.Parse(reader.GetString()!, CultureInfo.InvariantCulture);");
+ else if (underlyingType.TypeKind == TypeKind.Enum)
+ builder.AppendLine($"{propAccess} = Enum.Parse<{underlyingType.ToDisplayString()}>(reader.GetString()!);");
+ else
+ builder.AppendLine($"{propAccess} = reader.GetString()!;");
+ break;
+ }
+ }
+
+ private void GenerateReadComplexType(CodeBuilder builder, PropertyInfo property, string resultVar, string? assignmentTarget = null)
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(property.UnderlyingType.Name);
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+
+ builder.AppendLine("reader.Read(); // StartObject of type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor (M or NULL)");
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"M\"u8))");
+ builder.AppendLine("reader.Read(); // StartObject of the nested entity");
+ builder.AppendLine($"{target} = DynamoJsonMapper.{normalizedTypeName}.ReadFromJson(ref reader);");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value (true for NULL)");
+ if (property.IsNullable)
+ builder.AppendLine($"{target} = null;");
+ builder.CloseBrace();
+ builder.AppendLine("reader.Read(); // EndObject of type wrapper");
+ }
+
+ private void GenerateReadCollection(CodeBuilder builder, PropertyInfo property, string resultVar, string? assignmentTarget = null)
+ {
+ var elementType = property.ElementType!;
+ var isSetType = IsSetType(property.Type);
+ var elementTypeName = elementType.ToDisplayString();
+
+ // Determine the collection type and DynamoDB wire type
+ if (isSetType && elementType.SpecialType == SpecialType.System_String)
+ {
+ // SS type
+ GenerateReadStringSet(builder, property, resultVar, assignmentTarget);
+ return;
+ }
+
+ if (isSetType && IsNumericType(elementType))
+ {
+ // NS type
+ GenerateReadNumberSet(builder, property, resultVar, elementType, assignmentTarget);
+ return;
+ }
+
+ // L type (general list) — for set types with non-string/non-numeric elements,
+ // wrap in HashSet to satisfy the target type constraint.
+ GenerateReadList(builder, property, resultVar, elementType, assignmentTarget, wrapAsSet: isSetType);
+ }
+
+ private void GenerateReadStringSet(CodeBuilder builder, PropertyInfo property, string resultVar, string? assignmentTarget = null)
+ {
+ var collectionTypeName = GetCollectionTypeName(property.Type);
+ var varName = GetUniqueVarName("ss");
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+
+ builder.AppendLine("reader.Read(); // StartObject of type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor (SS or NULL)");
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"SS\"u8))");
+ builder.AppendLine("reader.Read(); // StartArray");
+ builder.AppendLine($"var {varName}Set = new HashSet();");
+ builder.OpenBraceWithLine($"while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)");
+ builder.AppendLine($"{varName}Set.Add(reader.GetString()!);");
+ builder.CloseBrace();
+ builder.AppendLine($"{target} = {ConvertToTargetCollection(property.Type, property.ElementType!, $"{varName}Set")};");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value (true for NULL)");
+ if (property.IsNullable)
+ builder.AppendLine($"{target} = null;");
+ builder.CloseBrace();
+ builder.AppendLine("reader.Read(); // EndObject of type wrapper");
+ }
+
+ private void GenerateReadNumberSet(CodeBuilder builder, PropertyInfo property, string resultVar, ITypeSymbol elementType, string? assignmentTarget = null)
+ {
+ var elementTypeName = elementType.ToDisplayString();
+ var varName = GetUniqueVarName("ns");
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+
+ builder.AppendLine("reader.Read(); // StartObject of type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor (NS or NULL)");
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"NS\"u8))");
+ builder.AppendLine("reader.Read(); // StartArray");
+ builder.AppendLine($"var {varName}Set = new HashSet<{elementTypeName}>();");
+ builder.OpenBraceWithLine($"while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)");
+
+ var utf8NsExpr = GetUtf8NumericParseExpression(elementType, $"{varName}Elem");
+ if (utf8NsExpr != null)
+ {
+ builder.AppendLine($"{utf8NsExpr};");
+ builder.AppendLine($"{varName}Set.Add({varName}ElemParsed);");
+ }
+ else
+ {
+ var parseExpr = GetNumericParseExpression(elementType, "reader.GetString()!");
+ builder.AppendLine($"{varName}Set.Add({parseExpr});");
+ }
+ builder.CloseBrace();
+ builder.AppendLine($"{target} = {ConvertToTargetCollection(property.Type, elementType, $"{varName}Set")};");
+ builder.CloseBrace();
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value (true for NULL)");
+ if (property.IsNullable)
+ builder.AppendLine($"{target} = null;");
+ builder.CloseBrace();
+ builder.AppendLine("reader.Read(); // EndObject of type wrapper");
+ }
+
+ private void GenerateReadList(CodeBuilder builder, PropertyInfo property, string resultVar, ITypeSymbol elementType, string? assignmentTarget = null, bool wrapAsSet = false)
+ {
+ var elementTypeName = elementType.ToDisplayString();
+ var varName = GetUniqueVarName("l");
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+
+ builder.AppendLine("reader.Read(); // StartObject of type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor (L or NULL)");
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"L\"u8))");
+ builder.AppendLine("reader.Read(); // StartArray");
+ builder.AppendLine($"var {varName}List = new List<{elementTypeName}>();");
+ builder.OpenBraceWithLine($"while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)");
+
+ // Each element in the array is a type-wrapped value
+ EmitReadElementValue(builder, elementType, $"{varName}List");
+
+ builder.CloseBrace(); // while
+ if (wrapAsSet)
+ {
+ // Target is a set type but elements are not string/numeric,
+ // so we need to wrap the list in a HashSet to match the target type.
+ builder.AppendLine($"{target} = new HashSet<{elementTypeName}>({varName}List);");
+ }
+ else
+ {
+ builder.AppendLine($"{target} = {ConvertToTargetCollection(property.Type, elementType, $"{varName}List")};");
+ }
+ builder.CloseBrace(); // if L
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value (true for NULL)");
+ if (property.IsNullable)
+ builder.AppendLine($"{target} = null;");
+ builder.CloseBrace();
+ builder.AppendLine("reader.Read(); // EndObject of type wrapper");
+ }
+
+ ///
+ /// Emits code to read a single type-wrapped element from a list.
+ /// The reader is positioned at the StartObject of the element's type wrapper.
+ ///
+ private void EmitReadElementValue(CodeBuilder builder, ITypeSymbol elementType, string listVar)
+ {
+ if (IsComplexType(elementType))
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(elementType.Name);
+ builder.AppendLine("// Element is {\"M\": {...}}");
+ builder.AppendLine("reader.Read(); // \"M\"");
+ builder.AppendLine("reader.Read(); // StartObject of nested entity");
+ builder.AppendLine($"{listVar}.Add(DynamoJsonMapper.{normalizedTypeName}.ReadFromJson(ref reader));");
+ builder.AppendLine("reader.Read(); // EndObject of element wrapper");
+ return;
+ }
+
+ // Primitive element: {"S": "..."} or {"N": "..."} or {"BOOL": ...}
+ builder.AppendLine("reader.Read(); // type descriptor");
+ builder.AppendLine("reader.Read(); // value");
+
+ switch (elementType.SpecialType)
+ {
+ case SpecialType.System_String:
+ builder.AppendLine($"{listVar}.Add(reader.GetString()!);");
+ break;
+ case SpecialType.System_Boolean:
+ builder.AppendLine($"{listVar}.Add(reader.GetBoolean());");
+ break;
+ case SpecialType.System_Char:
+ builder.AppendLine($"{listVar}.Add(reader.GetString()![0]);");
+ break;
+ case SpecialType.System_DateTime:
+ builder.AppendLine($"{listVar}.Add(DateTime.ParseExact(reader.GetString()!, \"o\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind));");
+ break;
+ case SpecialType.System_Byte:
+ case SpecialType.System_SByte:
+ case SpecialType.System_Int16:
+ case SpecialType.System_UInt16:
+ case SpecialType.System_Int32:
+ case SpecialType.System_UInt32:
+ case SpecialType.System_Int64:
+ case SpecialType.System_UInt64:
+ case SpecialType.System_Decimal:
+ case SpecialType.System_Single:
+ case SpecialType.System_Double:
+ {
+ var utf8Expr = GetUtf8NumericParseExpression(elementType, $"{listVar}Elem");
+ if (utf8Expr != null)
+ {
+ builder.AppendLine($"{utf8Expr};");
+ builder.AppendLine($"{listVar}.Add({listVar}ElemParsed);");
+ }
+ else
+ {
+ builder.AppendLine($"{listVar}.Add({GetNumericParseExpression(elementType, "reader.GetString()!")});");
+ }
+ break;
+ }
+ default:
+ if (elementType.Name == "DateTimeOffset")
+ builder.AppendLine($"{listVar}.Add(DateTimeOffset.ParseExact(reader.GetString()!, \"o\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind));");
+ else if (elementType.Name == "Guid")
+ builder.AppendLine($"{listVar}.Add(Guid.Parse(reader.GetString()!));");
+ else if (elementType.Name == "TimeSpan")
+ builder.AppendLine($"{listVar}.Add(TimeSpan.Parse(reader.GetString()!, CultureInfo.InvariantCulture));");
+ else if (elementType.Name == "DateOnly")
+ builder.AppendLine($"{listVar}.Add(DateOnly.Parse(reader.GetString()!, CultureInfo.InvariantCulture));");
+ else if (elementType.Name == "TimeOnly")
+ builder.AppendLine($"{listVar}.Add(TimeOnly.Parse(reader.GetString()!, CultureInfo.InvariantCulture));");
+ else if (elementType.TypeKind == TypeKind.Enum)
+ builder.AppendLine($"{listVar}.Add(Enum.Parse<{elementType.ToDisplayString()}>(reader.GetString()!));");
+ else
+ builder.AppendLine($"{listVar}.Add(reader.GetString()!);");
+ break;
+ }
+
+ builder.AppendLine("reader.Read(); // EndObject of element wrapper");
+ }
+
+ private void GenerateReadDictionary(CodeBuilder builder, PropertyInfo property, string resultVar, string? assignmentTarget = null)
+ {
+ var dictionaryTypes = property.DictionaryTypes!.Value;
+ var keyType = dictionaryTypes.KeyType;
+ var valueType = dictionaryTypes.ValueType;
+ var keyTypeName = keyType.ToDisplayString();
+ var valueTypeName = valueType.ToDisplayString();
+ var varName = GetUniqueVarName("dict");
+ var target = assignmentTarget ?? $"{resultVar}.{property.Name}";
+
+ builder.AppendLine("reader.Read(); // StartObject of type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor (M or NULL)");
+ builder.OpenBraceWithLine("if (reader.ValueTextEquals(\"M\"u8))");
+ builder.AppendLine("reader.Read(); // StartObject of the map");
+ builder.AppendLine($"var {varName}Map = new Dictionary<{keyTypeName}, {valueTypeName}>();");
+ builder.OpenBraceWithLine($"while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)");
+ builder.AppendLine($"var {varName}Key = reader.GetString()!;");
+
+ // Read the type-wrapped value for this dictionary entry
+ EmitReadDictionaryValue(builder, valueType, $"{varName}Map", $"{varName}Key");
+
+ builder.CloseBrace(); // while
+ builder.AppendLine($"{target} = {varName}Map;");
+ builder.CloseBrace(); // if M
+ builder.OpenBraceWithLine("else");
+ builder.AppendLine("reader.Read(); // value (true for NULL)");
+ if (property.IsNullable)
+ builder.AppendLine($"{target} = null;");
+ builder.CloseBrace();
+ builder.AppendLine("reader.Read(); // EndObject of type wrapper");
+ }
+
+ private void EmitReadDictionaryValue(CodeBuilder builder, ITypeSymbol valueType, string mapVar, string keyVar)
+ {
+ if (IsComplexType(valueType))
+ {
+ var normalizedTypeName = NamingHelpers.NormalizeTypeName(valueType.Name);
+ builder.AppendLine("reader.Read(); // StartObject of value type wrapper");
+ builder.AppendLine("reader.Read(); // \"M\"");
+ builder.AppendLine("reader.Read(); // StartObject of nested entity");
+ builder.AppendLine($"{mapVar}[{keyVar}] = DynamoJsonMapper.{normalizedTypeName}.ReadFromJson(ref reader);");
+ builder.AppendLine("reader.Read(); // EndObject of value type wrapper");
+ return;
+ }
+
+ // Primitive value: read type wrapper
+ builder.AppendLine("reader.Read(); // StartObject of value type wrapper");
+ builder.AppendLine("reader.Read(); // type descriptor");
+ builder.AppendLine("reader.Read(); // value");
+
+ // Try Utf8Parser for numeric dictionary values
+ var dictUtf8Expr = GetUtf8NumericParseExpression(valueType, $"{mapVar}Val");
+ if (dictUtf8Expr != null)
+ {
+ builder.AppendLine($"{dictUtf8Expr};");
+ builder.AppendLine($"{mapVar}[{keyVar}] = {mapVar}ValParsed;");
+ builder.AppendLine("reader.Read(); // EndObject of value type wrapper");
+ return;
+ }
+
+ switch (valueType.SpecialType)
+ {
+ case SpecialType.System_String:
+ builder.AppendLine($"{mapVar}[{keyVar}] = reader.GetString()!;");
+ break;
+ case SpecialType.System_Boolean:
+ builder.AppendLine($"{mapVar}[{keyVar}] = reader.GetBoolean();");
+ break;
+ case SpecialType.System_DateTime:
+ builder.AppendLine($"{mapVar}[{keyVar}] = DateTime.ParseExact(reader.GetString()!, \"o\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);");
+ break;
+ case SpecialType.System_Char:
+ builder.AppendLine($"{mapVar}[{keyVar}] = reader.GetString()![0];");
+ break;
+ default:
+ if (valueType.Name == "DateTimeOffset")
+ builder.AppendLine($"{mapVar}[{keyVar}] = DateTimeOffset.ParseExact(reader.GetString()!, \"o\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);");
+ else if (valueType.Name == "Guid")
+ builder.AppendLine($"{mapVar}[{keyVar}] = Guid.Parse(reader.GetString()!);");
+ else if (valueType.Name == "TimeSpan")
+ builder.AppendLine($"{mapVar}[{keyVar}] = TimeSpan.Parse(reader.GetString()!, CultureInfo.InvariantCulture);");
+ else if (valueType.Name == "DateOnly")
+ builder.AppendLine($"{mapVar}[{keyVar}] = DateOnly.Parse(reader.GetString()!, CultureInfo.InvariantCulture);");
+ else if (valueType.Name == "TimeOnly")
+ builder.AppendLine($"{mapVar}[{keyVar}] = TimeOnly.Parse(reader.GetString()!, CultureInfo.InvariantCulture);");
+ else if (valueType.TypeKind == TypeKind.Enum)
+ builder.AppendLine($"{mapVar}[{keyVar}] = Enum.Parse<{valueType.ToDisplayString()}>(reader.GetString()!);");
+ else
+ builder.AppendLine($"{mapVar}[{keyVar}] = reader.GetString()!;");
+ break;
+ }
+
+ builder.AppendLine("reader.Read(); // EndObject of value type wrapper");
+ }
+
+ // ─── Helper methods ─────────────────────────────────────────────────
+
+ private static IMethodSymbol? FindBestConstructor(DynamoTypeInfo type)
+ {
+ var constructors = type.Symbol.Constructors
+ .Where(c => c.DeclaredAccessibility == Accessibility.Public)
+ // Exclude the synthesized record copy constructor (single parameter of the same type)
+ .Where(c => !(c.Parameters.Length == 1 &&
+ SymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, type.Symbol)))
+ .ToList();
+
+ // Prefer constructor with parameters (primary constructor)
+ var paramConstructor = constructors.FirstOrDefault(c => c.Parameters.Any());
+ if (paramConstructor != null)
+ return paramConstructor;
+
+ // Fallback to parameterless constructor
+ return constructors.FirstOrDefault(c => !c.Parameters.Any());
+ }
+
+ private static PropertyInfo? FindPropertyIgnoreCase(DynamoTypeInfo type, string propertyName)
+ {
+ var current = type;
+ while (current != null)
+ {
+ var property = current.Properties.FirstOrDefault(p =>
+ string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase));
+ if (property != null)
+ return property;
+
+ current = current.BaseType;
+ }
+ return null;
+ }
+
+ private static string GetLocalVarName(string propertyName)
+ {
+ return "__" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
+ }
+
+ ///
+ /// Checks whether a property type is supported for JSON wire format reading.
+ /// Nested collections and dictionaries with non-string keys are not yet supported.
+ ///
+ private static bool IsSupportedForJsonRead(PropertyInfo property)
+ {
+ // Nested collections (e.g., IEnumerable>) are not supported
+ if (property.IsCollection && property.ElementType != null)
+ {
+ var elementType = property.ElementType;
+ if (elementType is IArrayTypeSymbol)
+ return false;
+ if (elementType is INamedTypeSymbol namedElem && namedElem.IsGenericType)
+ {
+ var name = namedElem.Name;
+ if (name is "List" or "IList" or "ICollection" or "IEnumerable" or
+ "HashSet" or "ISet" or "IReadOnlyCollection" or
+ "IReadOnlyList" or "IReadOnlySet" or "Collection" or
+ "Dictionary" or "IDictionary" or "IReadOnlyDictionary")
+ return false;
+ }
+ }
+
+ // Dictionaries with non-string keys (e.g., Dictionary) are not supported
+ if (property.IsDictionary && property.DictionaryTypes.HasValue)
+ {
+ var keyType = property.DictionaryTypes.Value.KeyType;
+ if (keyType.SpecialType != SpecialType.System_String)
+ return false;
+
+ // Also check for nested collection/dictionary values
+ var valueType = property.DictionaryTypes.Value.ValueType;
+ if (valueType is INamedTypeSymbol namedVal && namedVal.IsGenericType)
+ {
+ var name = namedVal.Name;
+ if (name is "List" or "IList" or "ICollection" or "IEnumerable" or
+ "HashSet" or "ISet" or "IReadOnlyCollection" or
+ "IReadOnlyList" or "IReadOnlySet" or "Collection" or
+ "Dictionary" or "IDictionary" or "IReadOnlyDictionary")
+ return false;
+ }
+ if (valueType is IArrayTypeSymbol)
+ return false;
+ }
+
+ return true;
+ }
+
+ private static string GetFullyQualifiedTypeName(PropertyInfo property)
+ {
+ var typeName = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ // FullyQualifiedFormat does not include nullable reference type annotations,
+ // so we need to append '?' manually for nullable reference types.
+ if (property.IsNullable && !property.Type.IsValueType && !typeName.EndsWith("?"))
+ typeName += "?";
+ return typeName;
+ }
+
+ private static string GetDefaultValueExpression(ITypeSymbol type, bool isNullable)
+ {
+ if (isNullable)
+ return "default";
+ if (type.IsValueType)
+ return "default";
+ return "default!";
+ }
+
+ private List GetAllProperties(DynamoTypeInfo type)
+ {
+ var properties = new List();
+ var current = type;
+
+ while (current != null)
+ {
+ properties.AddRange(current.Properties);
+ current = current.BaseType;
+ }
+
+ // Remove duplicates - keep most derived version
+ var uniqueProperties = new Dictionary();
+ foreach (var prop in properties)
+ {
+ if (!uniqueProperties.ContainsKey(prop.Name))
+ {
+ uniqueProperties[prop.Name] = prop;
+ }
+ }
+
+ return uniqueProperties.Values.ToList();
+ }
+
+ private static bool HasConcreteSubtypes(DynamoTypeInfo type, GenerationContext context)
+ {
+ return context.TypeRegistry.ContainsKey(type.FullName);
+ }
+
+ private static bool HasInheritedDynamoModel(DynamoTypeInfo type)
+ {
+ var current = type.BaseType;
+ while (current != null)
+ {
+ if (current.Attributes.Any(a => a is DynamoModelAttributeInfo))
+ return true;
+ current = current.BaseType;
+ }
+ return false;
+ }
+
+ private static DynamoModelAttributeInfo? GetInheritedDynamoModelAttribute(DynamoTypeInfo type)
+ {
+ var current = type.BaseType;
+ while (current != null)
+ {
+ var attr = current.Attributes.OfType().FirstOrDefault();
+ if (attr != null)
+ return attr;
+ current = current.BaseType;
+ }
+ return null;
+ }
+
+ private static bool InheritsFromAbstractType(DynamoTypeInfo type)
+ {
+ var current = type.BaseType;
+ while (current != null)
+ {
+ if (current.IsAbstract)
+ return true;
+ current = current.BaseType;
+ }
+ return false;
+ }
+
+ private static bool IsComplexType(ITypeSymbol type)
+ {
+ if (type.SpecialType != SpecialType.None)
+ return false;
+
+ if (type.Name == "Guid" || type.Name == "TimeSpan" || type.Name == "DateTimeOffset" ||
+ type.Name == "DateOnly" || type.Name == "TimeOnly")
+ return false;
+
+ if (type.TypeKind == TypeKind.Enum)
+ return false;
+
+ // Exclude collections
+ if (type is IArrayTypeSymbol)
+ return false;
+ if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
+ {
+ var name = namedType.Name;
+ if (name == "List" || name == "IList" || name == "ICollection" || name == "IEnumerable" ||
+ name == "HashSet" || name == "ISet" || name == "IReadOnlyCollection" ||
+ name == "IReadOnlyList" || name == "IReadOnlySet" || name == "Collection" ||
+ name == "Dictionary" || name == "IDictionary" || name == "IReadOnlyDictionary" ||
+ name == "Nullable")
+ return false;
+ }
+
+ return type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct || type.IsRecord;
+ }
+
+ private static bool IsNumericType(ITypeSymbol type)
+ {
+ return type.SpecialType switch
+ {
+ SpecialType.System_Byte or SpecialType.System_SByte or
+ SpecialType.System_Int16 or SpecialType.System_UInt16 or
+ SpecialType.System_Int32 or SpecialType.System_UInt32 or
+ SpecialType.System_Int64 or SpecialType.System_UInt64 or
+ SpecialType.System_Decimal or SpecialType.System_Single or SpecialType.System_Double => true,
+ _ => false
+ };
+ }
+
+ private static bool IsSetType(ITypeSymbol type)
+ {
+ if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
+ {
+ var name = namedType.Name;
+ return name == "HashSet" || name == "ISet" || name == "IReadOnlySet";
+ }
+ return false;
+ }
+
+ private static string GetNumericParseExpression(ITypeSymbol elementType, string readerExpr)
+ {
+ return elementType.SpecialType switch
+ {
+ SpecialType.System_Byte => $"byte.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_SByte => $"sbyte.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_Int16 => $"short.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_UInt16 => $"ushort.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_Int32 => $"int.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_UInt32 => $"uint.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_Int64 => $"long.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_UInt64 => $"ulong.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_Decimal => $"decimal.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_Single => $"float.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ SpecialType.System_Double => $"double.Parse({readerExpr}, CultureInfo.InvariantCulture)",
+ _ => $"int.Parse({readerExpr}, CultureInfo.InvariantCulture)"
+ };
+ }
+
+ ///
+ /// Returns a Utf8Parser-based expression that parses a numeric value directly from reader.ValueSpan,
+ /// avoiding the string allocation from reader.GetString().
+ /// Returns null if the type is not supported by Utf8Parser (e.g. decimal, float, double with DynamoDB's string encoding).
+ ///
+ private static string? GetUtf8NumericParseExpression(ITypeSymbol type, string targetExpr)
+ {
+ // Utf8Parser supports: byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal
+ // All parse from UTF-8 byte spans directly, avoiding string allocation.
+ // DynamoDB N values are JSON strings like "123", so reader.ValueSpan gives the raw UTF-8 bytes.
+ return type.SpecialType switch
+ {
+ SpecialType.System_Byte => $"Utf8Parser.TryParse(reader.ValueSpan, out byte {targetExpr}Parsed, out _)",
+ SpecialType.System_SByte => $"Utf8Parser.TryParse(reader.ValueSpan, out sbyte {targetExpr}Parsed, out _)",
+ SpecialType.System_Int16 => $"Utf8Parser.TryParse(reader.ValueSpan, out short {targetExpr}Parsed, out _)",
+ SpecialType.System_UInt16 => $"Utf8Parser.TryParse(reader.ValueSpan, out ushort {targetExpr}Parsed, out _)",
+ SpecialType.System_Int32 => $"Utf8Parser.TryParse(reader.ValueSpan, out int {targetExpr}Parsed, out _)",
+ SpecialType.System_UInt32 => $"Utf8Parser.TryParse(reader.ValueSpan, out uint {targetExpr}Parsed, out _)",
+ SpecialType.System_Int64 => $"Utf8Parser.TryParse(reader.ValueSpan, out long {targetExpr}Parsed, out _)",
+ SpecialType.System_UInt64 => $"Utf8Parser.TryParse(reader.ValueSpan, out ulong {targetExpr}Parsed, out _)",
+ SpecialType.System_Single => $"Utf8Parser.TryParse(reader.ValueSpan, out float {targetExpr}Parsed, out _)",
+ SpecialType.System_Double => $"Utf8Parser.TryParse(reader.ValueSpan, out double {targetExpr}Parsed, out _)",
+ SpecialType.System_Decimal => $"Utf8Parser.TryParse(reader.ValueSpan, out decimal {targetExpr}Parsed, out _)",
+ _ => null
+ };
+ }
+
+ private string GetUniqueVarName(string prefix)
+ {
+ return $"{prefix}{_variableCounter++}";
+ }
+
+ private static string GetCollectionTypeName(ITypeSymbol type)
+ {
+ if (type is INamedTypeSymbol namedType)
+ return namedType.Name;
+ if (type is IArrayTypeSymbol)
+ return "Array";
+ return "List";
+ }
+
+ ///
+ /// Converts a source collection expression to the target collection type.
+ /// The source is always a List<T> (for lists) or HashSet<T> (for sets),
+ /// so we return the source directly when the target is compatible to avoid redundant copies.
+ ///
+ private static string ConvertToTargetCollection(ITypeSymbol targetType, ITypeSymbol elementType, string sourceExpr)
+ {
+ var elementTypeName = elementType.ToDisplayString();
+
+ if (targetType is IArrayTypeSymbol)
+ return $"{sourceExpr}.ToArray()";
+
+ if (targetType is INamedTypeSymbol namedType)
+ {
+ return namedType.Name switch
+ {
+ // Concrete List — assign directly, no copy needed
+ "List" => sourceExpr,
+ // Interface types — use T[] which is smaller than List and implements IList, IReadOnlyList, etc.
+ "IList" or "ICollection" or "IReadOnlyCollection" or "IReadOnlyList" => $"{sourceExpr}.ToArray()",
+ // IEnumerable — assign List directly, no need to copy
+ "IEnumerable" => sourceExpr,
+ // Concrete HashSet — assign directly
+ "HashSet" => sourceExpr,
+ // Interface set types — assign HashSet directly (implements ISet, IReadOnlySet)
+ "ISet" or "IReadOnlySet" => sourceExpr,
+ "Collection" => $"new System.Collections.ObjectModel.Collection<{elementTypeName}>({sourceExpr})",
+ _ => sourceExpr
+ };
+ }
+
+ return sourceExpr;
+ }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/ReaderRegistrationGenerator.cs b/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/ReaderRegistrationGenerator.cs
new file mode 100644
index 00000000..9584712a
--- /dev/null
+++ b/src/Clients/Goa.Clients.Dynamo.Generator/CodeGeneration/ReaderRegistrationGenerator.cs
@@ -0,0 +1,83 @@
+using Goa.Clients.Dynamo.Generator.Models;
+
+namespace Goa.Clients.Dynamo.Generator.CodeGeneration;
+
+///
+/// Generates a static registration class for all concrete DynamoDB model types.
+/// Consumers call DynamoReaderRegistration.Initialize() once at startup
+/// to trigger the static constructor, which registers all types with DynamoItemReaderRegistry.
+///
+public class ReaderRegistrationGenerator : ICodeGenerator
+{
+ public string GenerateCode(IEnumerable types, GenerationContext context)
+ {
+ var registrableTypes = types.Where(ShouldRegister).ToList();
+
+ if (!registrableTypes.Any())
+ return string.Empty;
+
+ var typesByNamespace = registrableTypes.GroupBy(t => t.Namespace).Where(x => x.Any()).ToList();
+
+ if (!typesByNamespace.Any())
+ return string.Empty;
+
+ // Use the first namespace as the containing namespace for the registration class
+ var targetNamespace = typesByNamespace.First().Key;
+ if (string.IsNullOrEmpty(targetNamespace))
+ targetNamespace = "Generated";
+
+ var builder = new CodeBuilder();
+ builder.AppendLine("// ");
+ builder.AppendLine("#nullable enable");
+ builder.AppendLine("using Goa.Clients.Dynamo;");
+ builder.AppendLine();
+ builder.AppendLine($"namespace {targetNamespace}");
+ builder.OpenBrace();
+ {
+ builder.OpenBraceWithLine("internal static class DynamoReaderRegistration");
+ {
+ builder.OpenBraceWithLine("static DynamoReaderRegistration()");
+ {
+ foreach (var type in registrableTypes)
+ {
+ var normalizedName = NamingHelpers.NormalizeTypeName(type.Name);
+ builder.AppendLine($"DynamoItemReaderRegistry.Register<{type.FullName}>(DynamoJsonMapper.{normalizedName}.ReadFromJson);");
+ builder.AppendLine($"DynamoItemWriterRegistry.Register<{type.FullName}>(DynamoJsonMapper.{normalizedName}.WriteToJson);");
+ }
+ }
+ builder.CloseBrace();
+ builder.AppendLine();
+ builder.AppendLine("/// ");
+ builder.AppendLine("/// Ensures all DynamoDB model readers are registered. Call once during application startup.");
+ builder.AppendLine("/// ");
+ builder.AppendLine("public static void Initialize() { }");
+ }
+ builder.CloseBrace();
+ }
+ builder.CloseBrace();
+
+ return builder.ToString();
+ }
+
+ private bool ShouldRegister(DynamoTypeInfo type)
+ {
+ if (type.IsAbstract)
+ return false;
+
+ return type.Attributes.Any(a => a is DynamoModelAttributeInfo) ||
+ HasInheritedDynamoModel(type);
+ }
+
+ private bool HasInheritedDynamoModel(DynamoTypeInfo type)
+ {
+ var current = type.BaseType;
+ while (current != null)
+ {
+ if (current.Attributes.Any(a => a is DynamoModelAttributeInfo))
+ return true;
+
+ current = current.BaseType;
+ }
+ return false;
+ }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/DynamoMapperIncrementalGenerator.cs b/src/Clients/Goa.Clients.Dynamo.Generator/DynamoMapperIncrementalGenerator.cs
index e5532cb2..1b33432a 100644
--- a/src/Clients/Goa.Clients.Dynamo.Generator/DynamoMapperIncrementalGenerator.cs
+++ b/src/Clients/Goa.Clients.Dynamo.Generator/DynamoMapperIncrementalGenerator.cs
@@ -111,6 +111,8 @@ private static void Execute(Compilation compilation, ImmutableArray
+/// Represents DynamoConverter attribute information.
+///
+public class DynamoConverterAttributeInfo : AttributeInfo
+{
+ public string ConverterTypeName { get; set; } = string.Empty;
+}
diff --git a/src/Clients/Goa.Clients.Dynamo.Generator/Models/PropertyInfo.cs b/src/Clients/Goa.Clients.Dynamo.Generator/Models/PropertyInfo.cs
index a6071a05..fd7b87ce 100644
--- a/src/Clients/Goa.Clients.Dynamo.Generator/Models/PropertyInfo.cs
+++ b/src/Clients/Goa.Clients.Dynamo.Generator/Models/PropertyInfo.cs
@@ -50,6 +50,11 @@ public ITypeSymbol UnderlyingType
/// For non-dictionary types, returns null.
///
public (ITypeSymbol KeyType, ITypeSymbol ValueType)? DictionaryTypes { get; set; }
+
+ ///
+ /// The fully qualified type name of a custom DynamoConverter for this property, if any.
+ ///
+ public string? ConverterTypeName { get; set; }
///
/// Gets the effective name for this property in DynamoDB records.
diff --git a/src/Clients/Goa.Clients.Dynamo/DynamoExtensions.cs b/src/Clients/Goa.Clients.Dynamo/DynamoExtensions.cs
index 0e72a588..d244a479 100644
--- a/src/Clients/Goa.Clients.Dynamo/DynamoExtensions.cs
+++ b/src/Clients/Goa.Clients.Dynamo/DynamoExtensions.cs
@@ -218,6 +218,70 @@ public static async IAsyncEnumerable ScanAllAsync(this IDynamoClie
while (true);
}
+ ///
+ /// Executes a typed DynamoDB Query operation with automatic pagination using an async iterator.
+ ///
+ public static async IAsyncEnumerable QueryAllAsync(this IDynamoClient client, string tableName, DynamoItemReader itemReader, Action builder, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var _builder = new QueryBuilder(tableName);
+ builder(_builder);
+ var request = _builder.Build();
+
+ do
+ {
+ var result = await client.QueryAsync(request, itemReader, cancellationToken);
+ if (result.IsError)
+ {
+ throw new DynamoPaginationException(result.FirstError);
+ }
+
+ foreach (var item in result.Value.Items)
+ {
+ yield return item;
+ }
+
+ if (!result.Value.HasMoreResults)
+ {
+ break;
+ }
+
+ request.ExclusiveStartKey = result.Value.LastEvaluatedKey;
+ }
+ while (true);
+ }
+
+ ///
+ /// Executes a typed DynamoDB Scan operation with automatic pagination using an async iterator.
+ ///
+ public static async IAsyncEnumerable ScanAllAsync(this IDynamoClient client, string tableName, DynamoItemReader itemReader, Action builder, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var _builder = new ScanBuilder(tableName);
+ builder(_builder);
+ var request = _builder.Build();
+
+ do
+ {
+ var result = await client.ScanAsync(request, itemReader, cancellationToken);
+ if (result.IsError)
+ {
+ throw new DynamoPaginationException(result.FirstError);
+ }
+
+ foreach (var item in result.Value.Items)
+ {
+ yield return item;
+ }
+
+ if (!result.Value.HasMoreResults)
+ {
+ break;
+ }
+
+ request.ExclusiveStartKey = result.Value.LastEvaluatedKey;
+ }
+ while (true);
+ }
+
///
/// Executes a DynamoDB BatchGetItem operation with automatic retry of unprocessed keys using an async iterator.
///
@@ -227,9 +291,11 @@ public static async IAsyncEnumerable ScanAllAsync(this IDynamoClie
/// An async enumerable that yields items from all tables in the batch get result, retrying unprocessed keys.
public static async IAsyncEnumerable> BatchGetAllAsync(this IDynamoClient client, Action builder, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
+ const int maxAttempts = 10;
var _builder = new BatchGetItemBuilder();
builder(_builder);
var request = _builder.Build();
+ int attempts = 0;
do
{
@@ -252,8 +318,66 @@ public static async IAsyncEnumerable> BatchGe
break;
}
- // Retry with unprocessed keys
+ if (attempts >= maxAttempts)
+ {
+ throw new DynamoPaginationException(
+ ErrorOr.Error.Failure("Goa.DynamoDb.MaxRetriesExceeded", $"BatchGetItem still has unprocessed keys after {maxAttempts} retry attempts."));
+ }
+
+ // Retry with unprocessed keys using exponential backoff with jitter
+ request.RequestItems = result.Value.UnprocessedKeys!;
+ var baseDelay = Math.Min(100 * (1 << attempts), 25_600);
+ var jitter = Random.Shared.Next(0, baseDelay);
+ await Task.Delay(baseDelay + jitter, cancellationToken);
+ attempts++;
+ }
+ while (true);
+ }
+
+ ///
+ /// Executes a typed DynamoDB BatchGetItem operation with automatic retry of unprocessed keys using an async iterator.
+ ///
+ public static async IAsyncEnumerable> BatchGetAllAsync(this IDynamoClient client, DynamoItemReader itemReader, Action builder, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ const int maxAttempts = 10;
+ var _builder = new BatchGetItemBuilder();
+ builder(_builder);
+ var request = _builder.Build();
+ int attempts = 0;
+
+ do
+ {
+ var result = await client.BatchGetItemAsync(request, itemReader, cancellationToken);
+ if (result.IsError)
+ {
+ throw new DynamoPaginationException(result.FirstError);
+ }
+
+ foreach (var tableResponse in result.Value.Responses)
+ {
+ foreach (var item in tableResponse.Value)
+ {
+ yield return new KeyValuePair(tableResponse.Key, item);
+ }
+ }
+
+ if (!result.Value.HasUnprocessedKeys)
+ {
+ break;
+ }
+
+ if (attempts >= maxAttempts)
+ {
+ throw new DynamoPaginationException(
+ ErrorOr.Error.Failure("Goa.DynamoDb.MaxRetriesExceeded", $"BatchGetItem still has unprocessed keys after {maxAttempts} retry attempts."));
+ }
+
+ // Retry with unprocessed keys using exponential backoff with jitter
request.RequestItems = result.Value.UnprocessedKeys!;
+ var baseDelay = Math.Min(100 * (1 << attempts), 25_600);
+ var jitter = Random.Shared.Next(0, baseDelay);
+ await Task.Delay(baseDelay + jitter, cancellationToken);
+ attempts++;
}
while (true);
}
diff --git a/src/Clients/Goa.Clients.Dynamo/DynamoItemReaderRegistry.cs b/src/Clients/Goa.Clients.Dynamo/DynamoItemReaderRegistry.cs
new file mode 100644
index 00000000..d25e6533
--- /dev/null
+++ b/src/Clients/Goa.Clients.Dynamo/DynamoItemReaderRegistry.cs
@@ -0,0 +1,28 @@
+namespace Goa.Clients.Dynamo;
+
+///
+/// Registry for DynamoItemReader delegates, populated by source generators.
+///
+public static class DynamoItemReaderRegistry
+{
+ ///
+ /// Registers a reader delegate for the specified type.
+ ///
+ /// The type the reader deserializes.
+ /// The reader delegate.
+ public static void Register(DynamoItemReader reader) => Cache.Reader = reader;
+
+ ///
+ /// Gets the registered reader delegate for the specified type.
+ ///
+ /// The type to get the reader for.
+ /// The registered reader delegate.
+ /// Thrown when no reader is registered for the type.
+ public static DynamoItemReader Get() => Cache.Reader
+ ?? throw new InvalidOperationException($"No DynamoItemReader registered for {typeof(T).Name}. Ensure the type has [DynamoModel] attribute and the source generator has run.");
+
+ private static class Cache
+ {
+ public static volatile DynamoItemReader? Reader;
+ }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/DynamoItemWriterRegistry.cs b/src/Clients/Goa.Clients.Dynamo/DynamoItemWriterRegistry.cs
new file mode 100644
index 00000000..57fc7671
--- /dev/null
+++ b/src/Clients/Goa.Clients.Dynamo/DynamoItemWriterRegistry.cs
@@ -0,0 +1,28 @@
+namespace Goa.Clients.Dynamo;
+
+///
+/// Registry for DynamoItemWriter delegates, populated by source generators.
+///
+public static class DynamoItemWriterRegistry
+{
+ ///
+ /// Registers a writer delegate for the specified type.
+ ///
+ /// The type the writer serializes.
+ /// The writer delegate.
+ public static void Register(DynamoItemWriter writer) => Cache.Writer = writer;
+
+ ///
+ /// Gets the registered writer delegate for the specified type.
+ ///
+ /// The type to get the writer for.
+ /// The registered writer delegate.
+ /// Thrown when no writer is registered for the type.
+ public static DynamoItemWriter Get() => Cache.Writer
+ ?? throw new InvalidOperationException($"No DynamoItemWriter registered for {typeof(T).Name}. Ensure the type has [DynamoModel] attribute and the source generator has run.");
+
+ private static class Cache
+ {
+ public static volatile DynamoItemWriter? Writer;
+ }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/DynamoServiceClient.cs b/src/Clients/Goa.Clients.Dynamo/DynamoServiceClient.cs
index b4d39675..2db6b367 100644
--- a/src/Clients/Goa.Clients.Dynamo/DynamoServiceClient.cs
+++ b/src/Clients/Goa.Clients.Dynamo/DynamoServiceClient.cs
@@ -1,7 +1,9 @@
+using System.Text.Json;
using ErrorOr;
using Goa.Clients.Core;
using Goa.Clients.Core.Http;
using Goa.Clients.Dynamo.Errors;
+using Goa.Clients.Dynamo.Internal;
using Goa.Clients.Dynamo.Operations.Batch;
using Goa.Clients.Dynamo.Operations.DeleteItem;
using Goa.Clients.Dynamo.Operations.GetItem;
@@ -220,6 +222,158 @@ public async Task> TransactGetItemsAsync(Transa
return ConvertApiResponse(response);
}
+ ///
+ public async Task>> QueryAsync(QueryRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default)
+ {
+ var content = JsonSerializer.SerializeToUtf8Bytes(request, ResolveJsonTypeInfo());
+ using var requestMessage = CreateRequestMessage(
+ HttpMethod.Post, "/", content,
+ JsonContentType);
+
+ using var response = await SendAsync(requestMessage, "DynamoDB_20120810.Query", cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ return await HandleTypedErrorAsync(response, cancellationToken);
+
+ using var buffer = await ReadResponseBytesAsync(response, cancellationToken);
+ if (buffer.Length == 0)
+ return new QueryResult();
+
+ return DynamoResponseReader.ReadQueryResponse(buffer.Span, itemReader);
+ }
+
+ ///
+ public async Task>> ScanAsync(ScanRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default)
+ {
+ var content = JsonSerializer.SerializeToUtf8Bytes(request, ResolveJsonTypeInfo());
+ using var requestMessage = CreateRequestMessage(
+ HttpMethod.Post, "/", content,
+ JsonContentType);
+
+ using var response = await SendAsync(requestMessage, "DynamoDB_20120810.Scan", cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ return await HandleTypedErrorAsync(response, cancellationToken);
+
+ using var buffer = await ReadResponseBytesAsync(response, cancellationToken);
+ if (buffer.Length == 0)
+ return new ScanResult();
+
+ return DynamoResponseReader.ReadScanResponse(buffer.Span, itemReader);
+ }
+
+ ///
+ public async Task> GetItemAsync(GetItemRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default) where T : class
+ {
+ var content = JsonSerializer.SerializeToUtf8Bytes(request, ResolveJsonTypeInfo());
+ using var requestMessage = CreateRequestMessage(
+ HttpMethod.Post, "/", content,
+ JsonContentType);
+
+ using var response = await SendAsync(requestMessage, "DynamoDB_20120810.GetItem", cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ return await HandleTypedErrorAsync(response, cancellationToken);
+
+ using var buffer = await ReadResponseBytesAsync(response, cancellationToken);
+ if (buffer.Length == 0)
+ return default(T);
+
+ return DynamoResponseReader.ReadGetItemResponse(buffer.Span, itemReader);
+ }
+
+ ///
+ public async Task> PutItemAsync(string tableName, T item, DynamoItemWriter itemWriter, CancellationToken cancellationToken = default)
+ {
+ var bufferWriter = new System.Buffers.ArrayBufferWriter(256);
+ using var writer = new Utf8JsonWriter(bufferWriter);
+ writer.WriteStartObject();
+ writer.WriteString("TableName", tableName);
+ writer.WritePropertyName("Item");
+ itemWriter(writer, item);
+ writer.WriteEndObject();
+ writer.Flush();
+
+ var content = bufferWriter.WrittenSpan.ToArray();
+ using var requestMessage = CreateRequestMessage(
+ HttpMethod.Post, "/", content,
+ JsonContentType);
+
+ using var response = await SendAsync(requestMessage, "DynamoDB_20120810.PutItem", cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ return await HandleTypedErrorAsync(response, cancellationToken);
+
+ using var responseBuffer = await ReadResponseBytesAsync(response, cancellationToken);
+ if (responseBuffer.Length == 0)
+ return new PutItemResponse();
+
+ var jsonReader = new Utf8JsonReader(responseBuffer.Span);
+ return JsonSerializer.Deserialize(ref jsonReader, ResolveJsonTypeInfo())!;
+ }
+
+ ///
+ public async Task>> BatchGetItemAsync(BatchGetItemRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default)
+ {
+ var content = JsonSerializer.SerializeToUtf8Bytes(request, ResolveJsonTypeInfo());
+ using var requestMessage = CreateRequestMessage(
+ HttpMethod.Post, "/", content,
+ JsonContentType);
+
+ using var response = await SendAsync(requestMessage, "DynamoDB_20120810.BatchGetItem", cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ return await HandleTypedErrorAsync(response, cancellationToken);
+
+ using var buffer = await ReadResponseBytesAsync(response, cancellationToken);
+ if (buffer.Length == 0)
+ return new BatchGetResult();
+
+ return DynamoResponseReader.ReadBatchGetItemResponse(buffer.Span, itemReader);
+ }
+
+ ///
+ public async Task>> TransactGetItemsAsync(TransactGetRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default)
+ {
+ var content = JsonSerializer.SerializeToUtf8Bytes(request, ResolveJsonTypeInfo());
+ using var requestMessage = CreateRequestMessage(
+ HttpMethod.Post, "/", content,
+ JsonContentType);
+
+ using var response = await SendAsync(requestMessage, "DynamoDB_20120810.TransactGetItems", cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ return await HandleTypedErrorAsync(response, cancellationToken);
+
+ using var buffer = await ReadResponseBytesAsync(response, cancellationToken);
+ if (buffer.Length == 0)
+ return new TransactGetResult();
+
+ return DynamoResponseReader.ReadTransactGetItemResponse(buffer.Span, itemReader);
+ }
+
+ private async Task HandleTypedErrorAsync(HttpResponseMessage response, CancellationToken cancellationToken)
+ {
+ var errorPayload = await response.Content.ReadAsStringAsync(cancellationToken);
+ if (string.IsNullOrWhiteSpace(errorPayload))
+ return Error.Failure("Goa.DynamoDb.Unknown", "Request not successful.");
+
+ var error = DeserializeJsonError(errorPayload);
+ if (error is not null)
+ error = ProcessAwsErrorHeaders(response, error);
+
+ var errorType = error?.Type ?? error?.Code ?? "Unknown";
+ var metadata = new Dictionary
+ {
+ ["Payload"] = errorPayload,
+ ["StatusCode"] = response.StatusCode
+ };
+ return Error.Failure(
+ code: MapErrorCodeToDynamo(errorType),
+ description: error?.Message ?? "An error occurred while processing the request.",
+ metadata: metadata);
+ }
+
///
/// Converts an ApiResponse to an ErrorOr result, mapping AWS-specific error codes to DynamoDB error codes.
///
diff --git a/src/Clients/Goa.Clients.Dynamo/Goa.Clients.Dynamo.csproj b/src/Clients/Goa.Clients.Dynamo/Goa.Clients.Dynamo.csproj
index 5dbcc214..5baaaaa0 100644
--- a/src/Clients/Goa.Clients.Dynamo/Goa.Clients.Dynamo.csproj
+++ b/src/Clients/Goa.Clients.Dynamo/Goa.Clients.Dynamo.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/Clients/Goa.Clients.Dynamo/IDynamoClient.cs b/src/Clients/Goa.Clients.Dynamo/IDynamoClient.cs
index 1d4f61be..13a1aec7 100644
--- a/src/Clients/Goa.Clients.Dynamo/IDynamoClient.cs
+++ b/src/Clients/Goa.Clients.Dynamo/IDynamoClient.cs
@@ -95,4 +95,34 @@ public interface IDynamoClient
/// A cancellation token to cancel the operation.
/// The transact get response with items in the same order as the requests, or an error if the operation failed.
Task> TransactGetItemsAsync(TransactGetRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Queries items from a DynamoDB table with direct typed deserialization.
+ ///
+ Task>> QueryAsync(QueryRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default);
+
+ ///
+ /// Scans items from a DynamoDB table with direct typed deserialization.
+ ///
+ Task>> ScanAsync(ScanRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets an item from a DynamoDB table with direct typed deserialization.
+ ///
+ Task> GetItemAsync(GetItemRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default) where T : class;
+
+ ///
+ /// Puts a typed item into a DynamoDB table using a direct item writer for zero-copy serialization.
+ ///
+ Task> PutItemAsync(string tableName, T item, DynamoItemWriter itemWriter, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets multiple items from DynamoDB tables with direct typed deserialization.
+ ///
+ Task>> BatchGetItemAsync(BatchGetItemRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default);
+
+ ///
+ /// Executes a transactional get with direct typed deserialization.
+ ///
+ Task>> TransactGetItemsAsync(TransactGetRequest request, DynamoItemReader itemReader, CancellationToken cancellationToken = default);
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Models/CapacityDetail.cs b/src/Clients/Goa.Clients.Dynamo/Models/CapacityDetail.cs
index bf3c17d6..ec82cc43 100644
--- a/src/Clients/Goa.Clients.Dynamo/Models/CapacityDetail.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Models/CapacityDetail.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
namespace Goa.Clients.Dynamo.Models;
///
@@ -8,15 +10,18 @@ public class CapacityDetail
///
/// The number of read capacity units consumed by the operation.
///
+ [JsonPropertyName("ReadCapacityUnits")]
public double? ReadCapacityUnits { get; set; }
-
+
///
/// The number of write capacity units consumed by the operation.
///
+ [JsonPropertyName("WriteCapacityUnits")]
public double? WriteCapacityUnits { get; set; }
-
+
///
/// The total number of capacity units consumed by the operation.
///
+ [JsonPropertyName("CapacityUnits")]
public double? CapacityUnits { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Models/ConsumedCapacity.cs b/src/Clients/Goa.Clients.Dynamo/Models/ConsumedCapacity.cs
index e4f9d56b..5deb7288 100644
--- a/src/Clients/Goa.Clients.Dynamo/Models/ConsumedCapacity.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Models/ConsumedCapacity.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
namespace Goa.Clients.Dynamo.Models;
///
@@ -8,35 +10,42 @@ public class ConsumedCapacity
///
/// The name of the table that was affected by the operation.
///
+ [JsonPropertyName("TableName")]
public string? TableName { get; set; }
-
+
///
/// The total number of capacity units consumed by the operation.
///
+ [JsonPropertyName("CapacityUnits")]
public double? CapacityUnits { get; set; }
-
+
///
/// The total number of read capacity units consumed by the operation.
///
+ [JsonPropertyName("ReadCapacityUnits")]
public double? ReadCapacityUnits { get; set; }
-
+
///
/// The total number of write capacity units consumed by the operation.
///
+ [JsonPropertyName("WriteCapacityUnits")]
public double? WriteCapacityUnits { get; set; }
-
+
///
/// The capacity consumed by the global secondary indexes affected by the operation.
///
+ [JsonPropertyName("GlobalSecondaryIndexes")]
public Dictionary? GlobalSecondaryIndexes { get; set; }
///
/// The capacity consumed by the local secondary indexes affected by the operation.
///
+ [JsonPropertyName("LocalSecondaryIndexes")]
public Dictionary? LocalSecondaryIndexes { get; set; }
-
+
///
/// The capacity consumed by the table itself.
///
+ [JsonPropertyName("Table")]
public CapacityDetail? Table { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Models/ItemCollectionMetrics.cs b/src/Clients/Goa.Clients.Dynamo/Models/ItemCollectionMetrics.cs
index c3766732..bd3eb9c4 100644
--- a/src/Clients/Goa.Clients.Dynamo/Models/ItemCollectionMetrics.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Models/ItemCollectionMetrics.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
namespace Goa.Clients.Dynamo.Models;
///
@@ -8,11 +10,13 @@ public class ItemCollectionMetrics
///
/// The partition key value of the item collection.
///
+ [JsonPropertyName("ItemCollectionKey")]
public Dictionary? ItemCollectionKey { get; set; }
///
/// An estimate of item collection size, in gigabytes. This value is a two-element array
/// containing a lower bound and an upper bound for the estimate.
///
+ [JsonPropertyName("SizeEstimateRangeGB")]
public List? SizeEstimateRangeGB { get; set; }
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemRequest.cs
index 79a611a7..20e38bf6 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -10,10 +11,12 @@ public class BatchGetItemRequest
///
/// A map of one or more table names and, for each table, a map that describes one or more items to retrieve from that table.
///
+ [JsonPropertyName("RequestItems")]
public Dictionary RequestItems { get; set; } = new();
-
+
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemResponse.cs
index d6ee53cf..61232af3 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetItemResponse.cs
@@ -1,5 +1,5 @@
-using Goa.Clients.Dynamo.Models;
using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -11,21 +11,24 @@ public class BatchGetItemResponse
///
/// A map of table name to a list of items. Each item is represented as a set of attributes.
///
+ [JsonPropertyName("Responses")]
public Dictionary> Responses { get; set; } = new();
-
+
///
/// A map of tables and their respective keys that were not processed with the current response.
///
+ [JsonPropertyName("UnprocessedKeys")]
public Dictionary? UnprocessedKeys { get; set; }
-
+
///
/// Gets a value indicating whether there are unprocessed keys that need to be retried.
///
[JsonIgnore]
public bool HasUnprocessedKeys => UnprocessedKeys?.Count > 0;
-
+
///
/// The read capacity units consumed by the BatchGetItem operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public List? ConsumedCapacity { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetRequestItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetRequestItem.cs
index 23855ede..6c1c8876 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetRequestItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetRequestItem.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -10,21 +11,25 @@ public class BatchGetRequestItem
///
/// The primary keys of the items to retrieve. For each key, DynamoDB performs a GetItem operation and returns the entire item.
///
+ [JsonPropertyName("Keys")]
public List> Keys { get; set; } = new();
///
/// A string that identifies one or more attributes to retrieve from the table.
/// These attributes can include scalars, sets, or elements of a JSON document.
///
+ [JsonPropertyName("ProjectionExpression")]
public string? ProjectionExpression { get; set; }
///
/// The consistency of a read operation. If set to true, then a strongly consistent read is used; otherwise, an eventually consistent read is used.
///
+ [JsonPropertyName("ConsistentRead")]
public bool ConsistentRead { get; set; }
///
/// One or more substitution tokens for attribute names in the ProjectionExpression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetResult.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetResult.cs
deleted file mode 100644
index 708a82d9..00000000
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchGetResult.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Goa.Clients.Dynamo.Models;
-
-namespace Goa.Clients.Dynamo.Operations.Batch;
-
-///
-/// Result wrapper for BatchGet operations.
-///
-public class BatchGetResult
-{
- ///
- /// The items retrieved by the BatchGetItem operation.
- ///
- public List Items { get; set; } = new();
-
- ///
- /// Keys that were not processed during the BatchGetItem operation.
- ///
- public List> UnprocessedKeys { get; set; } = new();
-
- ///
- /// Gets a value indicating whether there are unprocessed keys that require additional requests.
- ///
- public bool HasUnprocessedKeys => UnprocessedKeys.Count > 0;
-
- ///
- /// The number of capacity units consumed by the operation.
- ///
- public double ConsumedCapacityUnits { get; set; }
-}
\ No newline at end of file
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemRequest.cs
index 81715e18..97b26738 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemRequest.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Enums;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -10,15 +11,18 @@ public class BatchWriteItemRequest
///
/// A map of one or more table names and, for each table, a list of operations to be performed.
///
+ [JsonPropertyName("RequestItems")]
public Dictionary> RequestItems { get; set; } = new();
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
///
/// Determines whether item collection metrics are returned.
///
+ [JsonPropertyName("ReturnItemCollectionMetrics")]
public ReturnItemCollectionMetrics ReturnItemCollectionMetrics { get; set; } = ReturnItemCollectionMetrics.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemResponse.cs
index af4d2d12..af643513 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteItemResponse.cs
@@ -1,5 +1,5 @@
-using Goa.Clients.Dynamo.Models;
using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -11,21 +11,24 @@ public class BatchWriteItemResponse
///
/// A map of tables and requests against those tables that were not processed.
///
+ [JsonPropertyName("UnprocessedItems")]
public Dictionary>? UnprocessedItems { get; set; }
-
+
///
/// Gets a value indicating whether there are unprocessed items that need to be retried.
///
[JsonIgnore]
public bool HasUnprocessedItems => UnprocessedItems?.Count > 0;
-
+
///
/// The write capacity units consumed by the BatchWriteItem operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public List? ConsumedCapacity { get; set; }
///
/// A list of tables that were processed by BatchWriteItem and, for each table, information about any item collections that were affected by individual DeleteItem or PutItem operations.
///
+ [JsonPropertyName("ItemCollectionMetrics")]
public Dictionary>? ItemCollectionMetrics { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteRequestItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteRequestItem.cs
index 79b131e7..12522fa6 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteRequestItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteRequestItem.cs
@@ -1,4 +1,6 @@
-namespace Goa.Clients.Dynamo.Operations.Batch;
+using System.Text.Json.Serialization;
+
+namespace Goa.Clients.Dynamo.Operations.Batch;
///
/// Individual write request within a BatchWriteItem operation.
@@ -8,10 +10,12 @@ public class BatchWriteRequestItem
///
/// A request to perform a PutItem operation.
///
+ [JsonPropertyName("PutRequest")]
public PutRequest? PutRequest { get; set; }
-
+
///
/// A request to perform a DeleteItem operation.
///
+ [JsonPropertyName("DeleteRequest")]
public DeleteRequest? DeleteRequest { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteResult.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteResult.cs
index 5d31e0ac..8a69683b 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteResult.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/BatchWriteResult.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -10,20 +11,23 @@ public class BatchWriteResult
///
/// Put items that were not processed during the BatchWriteItem operation.
///
+ [JsonPropertyName("UnprocessedPutItems")]
public List> UnprocessedPutItems { get; set; } = new();
-
+
///
/// Delete keys that were not processed during the BatchWriteItem operation.
///
+ [JsonPropertyName("UnprocessedDeleteKeys")]
public List> UnprocessedDeleteKeys { get; set; } = new();
-
+
///
/// Gets a value indicating whether there are unprocessed items that require additional requests.
///
public bool HasUnprocessedItems => UnprocessedPutItems.Count > 0 || UnprocessedDeleteKeys.Count > 0;
-
+
///
/// The number of capacity units consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacityUnits")]
public double ConsumedCapacityUnits { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/DeleteRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/DeleteRequest.cs
index e79b15c5..dcaf8f65 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/DeleteRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/DeleteRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -10,5 +11,6 @@ public class DeleteRequest
///
/// The primary key of the item to be deleted.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/PutRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/PutRequest.cs
index b13f513c..c0d766c8 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Batch/PutRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Batch/PutRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Batch;
@@ -10,5 +11,6 @@ public class PutRequest
///
/// A map of attribute names to AttributeValue objects representing the item to be put.
///
+ [JsonPropertyName("Item")]
public Dictionary Item { get; set; } = new();
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemRequest.cs
index 548e5ca6..d3b30b93 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.DeleteItem;
@@ -11,47 +12,56 @@ public class DeleteItemRequest
///
/// The name of the table from which to delete the item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
///
/// A map of attribute names to AttributeValue objects representing the primary key of the item to delete.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
///
/// A condition that must be satisfied in order for a conditional DeleteItem to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Use ReturnValues if you want to get the item attributes as they appeared before they were deleted.
/// For DeleteItem operations, only NONE and ALL_OLD values are supported.
///
+ [JsonPropertyName("ReturnValues")]
public ReturnValues ReturnValues { get; set; } = ReturnValues.NONE;
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
///
/// Determines whether item collection metrics are returned.
///
+ [JsonPropertyName("ReturnItemCollectionMetrics")]
public ReturnItemCollectionMetrics ReturnItemCollectionMetrics { get; set; } = ReturnItemCollectionMetrics.NONE;
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemResponse.cs
index 072e3393..44575d12 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/DeleteItem/DeleteItemResponse.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.DeleteItem;
@@ -10,15 +11,18 @@ public class DeleteItemResponse
///
/// A map of attribute names to AttributeValue objects representing the item as it appeared before it was deleted.
///
+ [JsonPropertyName("Attributes")]
public DynamoRecord? Attributes { get; set; }
-
+
///
- /// The number of capacity units consumed by the operation.
+ /// The capacity units consumed by the operation.
///
- public double? ConsumedCapacityUnits { get; set; }
+ [JsonPropertyName("ConsumedCapacity")]
+ public ConsumedCapacity? ConsumedCapacity { get; set; }
///
/// Information about item collections, if any, that were affected by the operation.
///
- public Dictionary>? ItemCollectionMetrics { get; set; }
-}
\ No newline at end of file
+ [JsonPropertyName("ItemCollectionMetrics")]
+ public ItemCollectionMetrics? ItemCollectionMetrics { get; set; }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemRequest.cs
index 280fe580..d76357cd 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemRequest.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
@@ -11,32 +12,38 @@ public class GetItemRequest
///
/// The name of the table containing the requested item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// A map of attribute names to AttributeValue objects representing the primary key of the item to retrieve.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
-
+
///
- /// Determines the read consistency model. If set to true, strongly consistent reads are used;
+ /// Determines the read consistency model. If set to true, strongly consistent reads are used;
/// otherwise, eventually consistent reads are used.
///
+ [JsonPropertyName("ConsistentRead")]
public bool ConsistentRead { get; set; } = false;
-
+
///
- /// A string that identifies one or more attributes to retrieve from the table.
+ /// A string that identifies one or more attributes to retrieve from the table.
/// These attributes can include scalars, sets, or elements of a JSON document.
///
+ [JsonPropertyName("ProjectionExpression")]
public string? ProjectionExpression { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
-
+
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemResponse.cs
index 4b274bed..23ebfa8c 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/GetItem/GetItemResponse.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.GetItem;
@@ -10,15 +11,18 @@ public class GetItemResponse
///
/// A map of attribute names to AttributeValue objects, as specified by ProjectionExpression.
///
+ [JsonPropertyName("Item")]
public DynamoRecord? Item { get; set; }
-
+
///
/// The capacity units consumed by the GetItem operation.
///
+ [JsonPropertyName("ConsumedCapacityUnits")]
public double? ConsumedCapacityUnits { get; set; }
-
+
///
/// The capacity units consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public ConsumedCapacity? ConsumedCapacity { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemRequest.cs
index 2ee18891..0412ff60 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemRequest.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
@@ -11,46 +12,55 @@ public class PutItemRequest
///
/// The name of the table to contain the item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
///
/// A map of attribute names to AttributeValue objects representing the item to put.
///
+ [JsonPropertyName("Item")]
public Dictionary Item { get; set; } = new();
///
/// A condition that must be satisfied in order for a conditional PutItem operation to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Use ReturnValues if you want to get the item attributes as they appeared before they were deleted.
///
+ [JsonPropertyName("ReturnValues")]
public ReturnValues ReturnValues { get; set; } = ReturnValues.NONE;
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
///
/// Determines whether item collection metrics are returned.
///
+ [JsonPropertyName("ReturnItemCollectionMetrics")]
public ReturnItemCollectionMetrics ReturnItemCollectionMetrics { get; set; } = ReturnItemCollectionMetrics.NONE;
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemResponse.cs
index cdca227e..264f2e06 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/PutItem/PutItemResponse.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.PutItem;
@@ -8,18 +9,21 @@ namespace Goa.Clients.Dynamo.Operations.PutItem;
public class PutItemResponse
{
///
- /// The attribute values as they appeared before the PutItem operation,
+ /// The attribute values as they appeared before the PutItem operation,
/// but only if ReturnValues is specified as something other than NONE in the request.
///
+ [JsonPropertyName("Attributes")]
public DynamoRecord? Attributes { get; set; }
-
+
///
/// The capacity units consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public ConsumedCapacity? ConsumedCapacity { get; set; }
-
+
///
/// Information about item collections, if any, that were affected by the operation.
///
- public Dictionary>>? ItemCollectionMetrics { get; set; }
-}
\ No newline at end of file
+ [JsonPropertyName("ItemCollectionMetrics")]
+ public ItemCollectionMetrics? ItemCollectionMetrics { get; set; }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryRequest.cs
index fff930d8..01d15151 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryRequest.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
@@ -11,66 +12,79 @@ public class QueryRequest
///
/// The name of the table containing the requested items.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
///
/// The condition that specifies the key values for items to be retrieved by the Query action.
///
+ [JsonPropertyName("KeyConditionExpression")]
public string KeyConditionExpression { get; set; } = string.Empty;
///
/// A string that contains conditions that DynamoDB applies after the Query operation, but before the data is returned to you.
///
+ [JsonPropertyName("FilterExpression")]
public string? FilterExpression { get; set; }
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// The maximum number of items to evaluate (not necessarily the number of matching items).
///
+ [JsonPropertyName("Limit")]
public int? Limit { get; set; }
///
/// The primary key of the first item that this operation will evaluate.
///
+ [JsonPropertyName("ExclusiveStartKey")]
public Dictionary? ExclusiveStartKey { get; set; }
///
/// Specifies the order for index traversal: If true (default), the traversal is performed in ascending order;
/// if false, the traversal is performed in descending order.
///
+ [JsonPropertyName("ScanIndexForward")]
public bool ScanIndexForward { get; set; } = true;
///
/// The name of an index to query. This index can be any local secondary index or global secondary index on the table.
///
+ [JsonPropertyName("IndexName")]
public string? IndexName { get; set; }
///
/// A string that identifies one or more attributes to retrieve from the table.
///
+ [JsonPropertyName("ProjectionExpression")]
public string? ProjectionExpression { get; set; }
///
/// The attributes to be returned in the result.
///
+ [JsonPropertyName("Select")]
public Select Select { get; set; } = Select.ALL_ATTRIBUTES;
///
/// Determines the read consistency model. If set to true, strongly consistent reads are used.
///
+ [JsonPropertyName("ConsistentRead")]
public bool ConsistentRead { get; set; } = false;
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResponse.cs
index c3331e0b..d6606f10 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResponse.cs
@@ -1,5 +1,5 @@
-using Goa.Clients.Dynamo.Models;
using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Query;
@@ -11,12 +11,14 @@ public class QueryResponse
///
/// An array of item attributes that match the query criteria.
///
+ [JsonPropertyName("Items")]
public List Items { get; set; } = new();
///
/// The primary key of the item where the operation stopped, inclusive of the previous result set.
/// Use this value to start a new operation, excluding this value in the new request.
///
+ [JsonPropertyName("LastEvaluatedKey")]
public Dictionary? LastEvaluatedKey { get; set; }
///
@@ -34,10 +36,12 @@ public class QueryResponse
///
/// The number of items evaluated, before any QueryFilter is applied.
///
+ [JsonPropertyName("ScannedCount")]
public int ScannedCount { get; set; }
///
/// The capacity units consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public ConsumedCapacity? ConsumedCapacity { get; set; }
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResult.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResult.cs
index 5455be7c..c3695f8b 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResult.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Query/QueryResult.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Query;
@@ -10,14 +11,16 @@ public class QueryResult
///
/// The items returned by the Query operation.
///
+ [JsonPropertyName("Items")]
public List Items { get; set; } = new();
-
+
///
/// The primary key of the item where the operation stopped, inclusive of the previous result set.
/// Use this value to start a new operation, excluding this value in the new request.
///
+ [JsonPropertyName("LastEvaluatedKey")]
public Dictionary? LastEvaluatedKey { get; set; }
-
+
///
/// Gets the number of items in the response. This is deliberately computed from
/// because DynamoDB's Count always equals the number of items returned (post-filter).
@@ -34,12 +37,14 @@ public class QueryResult
/// The number of items evaluated before any filter expression was applied.
/// If no filter was used, this equals .
///
+ [JsonPropertyName("ScannedCount")]
public int ScannedCount { get; set; }
///
- /// The number of capacity units consumed by the operation.
+ /// The capacity consumed by the operation.
///
- public double ConsumedCapacityUnits { get; set; }
+ [JsonPropertyName("ConsumedCapacity")]
+ public ConsumedCapacity? ConsumedCapacity { get; set; }
}
///
@@ -49,9 +54,11 @@ public class QueryResult
public class QueryResult
{
///
+ [JsonPropertyName("Items")]
public List Items { get; set; } = new();
///
+ [JsonPropertyName("LastEvaluatedKey")]
public Dictionary? LastEvaluatedKey { get; set; }
///
@@ -61,10 +68,12 @@ public class QueryResult
public bool HasMoreResults => LastEvaluatedKey?.Count > 0;
///
+ [JsonPropertyName("ScannedCount")]
public int ScannedCount { get; set; }
///
/// The capacity consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public ConsumedCapacity? ConsumedCapacity { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanRequest.cs
index e4f33fe2..8213917d 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanRequest.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
@@ -11,65 +12,78 @@ public class ScanRequest
///
/// The name of the table containing the requested items.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
///
/// A string that contains conditions that DynamoDB applies after the Scan operation, but before the data is returned to you.
///
+ [JsonPropertyName("FilterExpression")]
public string? FilterExpression { get; set; }
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// The maximum number of items to evaluate (not necessarily the number of matching items).
///
+ [JsonPropertyName("Limit")]
public int? Limit { get; set; }
///
/// The primary key of the first item that this operation will evaluate.
///
+ [JsonPropertyName("ExclusiveStartKey")]
public Dictionary? ExclusiveStartKey { get; set; }
///
/// The name of a secondary index to scan. This index can be any local secondary index or global secondary index.
///
+ [JsonPropertyName("IndexName")]
public string? IndexName { get; set; }
///
/// A string that identifies one or more attributes to retrieve from the specified table or index.
///
+ [JsonPropertyName("ProjectionExpression")]
public string? ProjectionExpression { get; set; }
///
/// The attributes to be returned in the result.
///
+ [JsonPropertyName("Select")]
public Select Select { get; set; } = Select.ALL_ATTRIBUTES;
///
/// The total number of segments for a parallel scan request.
///
+ [JsonPropertyName("TotalSegments")]
public int? TotalSegments { get; set; }
///
/// For a parallel scan request, Segment identifies an individual segment to be scanned by an application worker.
///
+ [JsonPropertyName("Segment")]
public int? Segment { get; set; }
///
/// Determines the read consistency model. If set to true, strongly consistent reads are used.
///
+ [JsonPropertyName("ConsistentRead")]
public bool ConsistentRead { get; set; } = false;
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResponse.cs
index 6c4483c4..b9fb807e 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResponse.cs
@@ -1,5 +1,5 @@
-using Goa.Clients.Dynamo.Models;
using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Scan;
@@ -11,38 +11,43 @@ public class ScanResponse
///
/// An array of item attributes that match the scan criteria.
///
+ [JsonPropertyName("Items")]
public List Items { get; set; } = new();
-
+
///
/// The primary key of the item where the operation stopped, inclusive of the previous result set.
/// Use this value to start a new operation, excluding this value in the new request.
///
+ [JsonPropertyName("LastEvaluatedKey")]
public Dictionary? LastEvaluatedKey { get; set; }
-
+
///
/// The number of items in the response.
///
[JsonIgnore]
public int Count => Items.Count;
-
+
///
/// Gets a value indicating whether there are more results available to retrieve.
///
[JsonIgnore]
public bool HasMoreResults => LastEvaluatedKey?.Count > 0;
-
+
///
/// The number of items evaluated, before any ScanFilter is applied.
///
+ [JsonPropertyName("ScannedCount")]
public int ScannedCount { get; set; }
-
+
///
/// The capacity units consumed by the Scan operation.
///
+ [JsonPropertyName("ConsumedCapacityUnits")]
public double? ConsumedCapacityUnits { get; set; }
-
+
///
/// The capacity units consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public ConsumedCapacity? ConsumedCapacity { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResult.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResult.cs
index c22bd26f..a47eb3ea 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResult.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Scan/ScanResult.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Scan;
@@ -10,14 +11,16 @@ public class ScanResult
///
/// The items returned by the Scan operation.
///
+ [JsonPropertyName("Items")]
public List Items { get; set; } = new();
-
+
///
/// The primary key of the item where the operation stopped, inclusive of the previous result set.
/// Use this value to start a new operation, excluding this value in the new request.
///
+ [JsonPropertyName("LastEvaluatedKey")]
public Dictionary? LastEvaluatedKey { get; set; }
-
+
///
/// Gets the number of items in the response. This is deliberately computed from
/// because DynamoDB's Count always equals the number of items returned (post-filter).
@@ -34,12 +37,14 @@ public class ScanResult
/// The number of items evaluated before any filter expression was applied.
/// If no filter was used, this equals .
///
+ [JsonPropertyName("ScannedCount")]
public int ScannedCount { get; set; }
///
- /// The number of capacity units consumed by the operation.
+ /// The capacity consumed by the operation.
///
- public double ConsumedCapacityUnits { get; set; }
+ [JsonPropertyName("ConsumedCapacity")]
+ public ConsumedCapacity? ConsumedCapacity { get; set; }
}
///
@@ -49,9 +54,11 @@ public class ScanResult
public class ScanResult
{
///
+ [JsonPropertyName("Items")]
public List Items { get; set; } = new();
///
+ [JsonPropertyName("LastEvaluatedKey")]
public Dictionary? LastEvaluatedKey { get; set; }
///
@@ -61,10 +68,12 @@ public class ScanResult
public bool HasMoreResults => LastEvaluatedKey?.Count > 0;
///
+ [JsonPropertyName("ScannedCount")]
public int ScannedCount { get; set; }
///
/// The capacity consumed by the operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public ConsumedCapacity? ConsumedCapacity { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactConditionCheckItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactConditionCheckItem.cs
index e7a31e52..a7e46638 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactConditionCheckItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactConditionCheckItem.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -11,31 +12,37 @@ public class TransactConditionCheckItem
///
/// The name of the table containing the requested item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// The primary key of the item to be checked.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
-
+
///
/// A condition that must be satisfied in order for the ConditionCheck to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string ConditionExpression { get; set; } = string.Empty;
-
+
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactDeleteItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactDeleteItem.cs
index d93eca34..b19bc059 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactDeleteItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactDeleteItem.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -11,31 +12,37 @@ public class TransactDeleteItem
///
/// The name of the table from which to delete the item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// The primary key of the item to be deleted.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
-
+
///
/// A condition that must be satisfied in order for a conditional DeleteItem to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
-
+
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItem.cs
index 45b14bef..e41a7057 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItem.cs
@@ -1,4 +1,6 @@
-namespace Goa.Clients.Dynamo.Operations.Transactions;
+using System.Text.Json.Serialization;
+
+namespace Goa.Clients.Dynamo.Operations.Transactions;
///
/// Individual transact get item.
@@ -8,5 +10,6 @@ public class TransactGetItem
///
/// Contains the primary key that identifies the item to get, together with the name of the table that contains the item.
///
+ [JsonPropertyName("Get")]
public TransactGetItemRequest Get { get; set; } = new();
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemRequest.cs
index 80a371b8..3a37f75c 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,20 +11,24 @@ public class TransactGetItemRequest
///
/// The name of the table containing the requested item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// A map of attribute names to AttributeValue objects representing the primary key of the item to retrieve.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
-
+
///
/// A string that identifies one or more attributes to retrieve from the table.
///
+ [JsonPropertyName("ProjectionExpression")]
public string? ProjectionExpression { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemResponse.cs
index 81281a40..9ca8c401 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetItemResponse.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,10 +11,12 @@ public class TransactGetItemResponse
///
/// An ordered array of up to 100 response items, each of which corresponds to the TransactGetItem request in the same position.
///
+ [JsonPropertyName("Responses")]
public List Responses { get; set; } = new();
-
+
///
/// The capacity units consumed by the entire TransactGetItem operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public List? ConsumedCapacity { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetRequest.cs
index 29249342..692cf43e 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,10 +11,12 @@ public class TransactGetRequest
///
/// An ordered array of up to 100 TransactGetItem objects, each of which contains a Get operation.
///
+ [JsonPropertyName("TransactItems")]
public List TransactItems { get; set; } = new();
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetResult.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetResult.cs
index 2d3129a2..c7960c32 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetResult.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactGetResult.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,5 +11,6 @@ public class TransactGetResult
///
/// Map of attribute names to values for the requested item, or null if the item was not found.
///
+ [JsonPropertyName("Item")]
public DynamoRecord? Item { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactPutItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactPutItem.cs
index 0b9ecdda..5fd27526 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactPutItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactPutItem.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -11,31 +12,37 @@ public class TransactPutItem
///
/// The name of the table to contain the item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// A map of attribute names to AttributeValue objects representing the item.
///
+ [JsonPropertyName("Item")]
public Dictionary Item { get; set; } = new();
-
+
///
/// A condition that must be satisfied in order for a conditional PutItem operation to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
-
+
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactUpdateItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactUpdateItem.cs
index 5b8b0d28..aa9f671f 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactUpdateItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactUpdateItem.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -11,36 +12,43 @@ public class TransactUpdateItem
///
/// The name of the table containing the item to be updated.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// The primary key of the item to be updated.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
-
+
///
/// An expression that defines one or more attributes to be updated.
///
+ [JsonPropertyName("UpdateExpression")]
public string UpdateExpression { get; set; } = string.Empty;
-
+
///
/// A condition that must be satisfied in order for a conditional UpdateItem to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
-
+
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItem.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItem.cs
index 566acaac..20ec5540 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItem.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItem.cs
@@ -1,4 +1,6 @@
-namespace Goa.Clients.Dynamo.Operations.Transactions;
+using System.Text.Json.Serialization;
+
+namespace Goa.Clients.Dynamo.Operations.Transactions;
///
/// Individual transact write item.
@@ -8,20 +10,24 @@ public class TransactWriteItem
///
/// A request to perform a PutItem operation.
///
+ [JsonPropertyName("Put")]
public TransactPutItem? Put { get; set; }
-
+
///
/// A request to perform an UpdateItem operation.
///
+ [JsonPropertyName("Update")]
public TransactUpdateItem? Update { get; set; }
-
+
///
/// A request to perform a DeleteItem operation.
///
+ [JsonPropertyName("Delete")]
public TransactDeleteItem? Delete { get; set; }
-
+
///
/// A request to perform a condition check.
///
+ [JsonPropertyName("ConditionCheck")]
public TransactConditionCheckItem? ConditionCheck { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItemResponse.cs
index ea2867c7..08bf2f0b 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteItemResponse.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,10 +11,12 @@ public class TransactWriteItemResponse
///
/// The capacity units consumed by the entire TransactWriteItem operation.
///
+ [JsonPropertyName("ConsumedCapacity")]
public List? ConsumedCapacity { get; set; }
-
+
///
/// A list of tables that were processed by TransactWriteItem and, for each table, information about any item collections that were affected by individual operations.
///
- public Dictionary? ItemCollectionMetrics { get; set; }
-}
\ No newline at end of file
+ [JsonPropertyName("ItemCollectionMetrics")]
+ public Dictionary>? ItemCollectionMetrics { get; set; }
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteOperation.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteOperation.cs
index 2f52344a..85f84da6 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteOperation.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteOperation.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Models;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,40 +11,48 @@ public class TransactWriteOperation
///
/// The type of operation to perform (Put, Update, Delete, or ConditionCheck).
///
+ [JsonPropertyName("OperationType")]
public TransactOperationType OperationType { get; set; }
-
+
///
/// The name of the table containing the item.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
-
+
///
/// The primary key of the item to operate on.
///
+ [JsonPropertyName("Key")]
public Dictionary? Key { get; set; }
-
+
///
/// A map of attribute names to AttributeValue objects (for Put operations).
///
+ [JsonPropertyName("Item")]
public Dictionary? Item { get; set; }
-
+
///
/// A condition that must be satisfied in order for the operation to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
-
+
///
/// An expression that defines one or more attributes to be updated (for Update operations).
///
+ [JsonPropertyName("UpdateExpression")]
public string? UpdateExpression { get; set; }
-
+
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
-
+
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteRequest.cs
index e752a36e..6a1cec6e 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/Transactions/TransactWriteRequest.cs
@@ -1,4 +1,5 @@
-using Goa.Clients.Dynamo.Enums;
+using System.Text.Json.Serialization;
+using Goa.Clients.Dynamo.Enums;
namespace Goa.Clients.Dynamo.Operations.Transactions;
@@ -10,21 +11,25 @@ public class TransactWriteRequest
///
/// An ordered array of up to 100 TransactWriteItem objects, each of which contains a ConditionCheck, Put, Update, or Delete operation.
///
+ [JsonPropertyName("TransactItems")]
public List TransactItems { get; set; } = new();
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
///
/// Determines whether item collection metrics are returned.
///
+ [JsonPropertyName("ReturnItemCollectionMetrics")]
public ReturnItemCollectionMetrics ReturnItemCollectionMetrics { get; set; } = ReturnItemCollectionMetrics.NONE;
///
/// Providing a ClientRequestToken makes the call to TransactWriteItems idempotent,
/// meaning that multiple identical calls have the same effect as one single call.
///
+ [JsonPropertyName("ClientRequestToken")]
public string? ClientRequestToken { get; set; }
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemRequest.cs b/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemRequest.cs
index 2d009143..7094d476 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemRequest.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemRequest.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Enums;
using Goa.Clients.Dynamo.Models;
@@ -11,51 +12,61 @@ public class UpdateItemRequest
///
/// The name of the table containing the item to update.
///
+ [JsonPropertyName("TableName")]
public string TableName { get; set; } = string.Empty;
///
/// The primary key of the item to be updated.
///
+ [JsonPropertyName("Key")]
public Dictionary Key { get; set; } = new();
///
/// An expression that defines one or more attributes to be updated, the action to be performed on them, and new values for them.
///
+ [JsonPropertyName("UpdateExpression")]
public string UpdateExpression { get; set; } = string.Empty;
///
/// A condition that must be satisfied in order for a conditional update to succeed.
///
+ [JsonPropertyName("ConditionExpression")]
public string? ConditionExpression { get; set; }
///
/// One or more values that can be substituted in an expression.
///
+ [JsonPropertyName("ExpressionAttributeValues")]
public Dictionary? ExpressionAttributeValues { get; set; }
///
/// One or more substitution tokens for attribute names in an expression.
///
+ [JsonPropertyName("ExpressionAttributeNames")]
public Dictionary? ExpressionAttributeNames { get; set; }
///
/// Use ReturnValues if you want to get the item attributes as they appeared before they were updated.
///
+ [JsonPropertyName("ReturnValues")]
public ReturnValues ReturnValues { get; set; } = ReturnValues.NONE;
///
/// Determines the level of detail about provisioned throughput consumption that is returned in the response.
///
+ [JsonPropertyName("ReturnConsumedCapacity")]
public ReturnConsumedCapacity ReturnConsumedCapacity { get; set; } = ReturnConsumedCapacity.NONE;
///
/// Determines whether item collection metrics are returned.
///
+ [JsonPropertyName("ReturnItemCollectionMetrics")]
public ReturnItemCollectionMetrics ReturnItemCollectionMetrics { get; set; } = ReturnItemCollectionMetrics.NONE;
///
/// Specifies how to return attribute values when a conditional check fails.
/// Use ALL_OLD to return all attributes of the item as they appeared before the operation.
///
+ [JsonPropertyName("ReturnValuesOnConditionCheckFailure")]
public ReturnValuesOnConditionCheckFailure ReturnValuesOnConditionCheckFailure { get; set; } = ReturnValuesOnConditionCheckFailure.NONE;
}
diff --git a/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemResponse.cs b/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemResponse.cs
index d7ba2e10..330778fc 100644
--- a/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemResponse.cs
+++ b/src/Clients/Goa.Clients.Dynamo/Operations/UpdateItem/UpdateItemResponse.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using Goa.Clients.Dynamo.Models;
namespace Goa.Clients.Dynamo.Operations.UpdateItem;
@@ -10,15 +11,18 @@ public class UpdateItemResponse
///
/// A map of attribute names to AttributeValue objects representing the item as it appeared before it was updated.
///
+ [JsonPropertyName("Attributes")]
public DynamoRecord? Attributes { get; set; }
-
+
///
- /// The number of capacity units consumed by the operation.
+ /// The capacity units consumed by the operation.
///
- public double? ConsumedCapacityUnits { get; set; }
+ [JsonPropertyName("ConsumedCapacity")]
+ public ConsumedCapacity? ConsumedCapacity { get; set; }
///
/// Information about item collections, if any, that were affected by the operation.
///
- public Dictionary>? ItemCollectionMetrics { get; set; }
-}
\ No newline at end of file
+ [JsonPropertyName("ItemCollectionMetrics")]
+ public ItemCollectionMetrics? ItemCollectionMetrics { get; set; }
+}
diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs
index 827b094c..194ca5e8 100644
--- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs
+++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Benchmarks/QueryAsyncBenchmarks.cs
@@ -3,6 +3,7 @@
using EfficientDynamoDb;
using EfficientDynamoDb.Attributes;
using Goa.Clients.Dynamo.Benchmarks.Infrastructure;
+using Goa.Clients.Dynamo;
using EfficientAttributeValue = EfficientDynamoDb.DocumentModel.AttributeValue;
using GoaModels = Goa.Clients.Dynamo.Models;
using GoaQueryRequest = Goa.Clients.Dynamo.Operations.Query.QueryRequest;
@@ -11,6 +12,7 @@
namespace Goa.Clients.Dynamo.Benchmarks.Benchmarks;
[DynamoDbTable("benchmark-table")]
+[DynamoModel(PK = "", SK = "", PKName = "pk", SKName = "sk")]
public class BenchmarkEntity
{
[DynamoDbProperty("pk", DynamoDbAttributeType.PartitionKey)]
@@ -19,13 +21,13 @@ public class BenchmarkEntity
[DynamoDbProperty("sk", DynamoDbAttributeType.SortKey)]
public string Sk { get; set; } = "";
- [DynamoDbProperty("data")]
+ [DynamoDbProperty("data"), SerializedName("data")]
public string Data { get; set; } = "";
- [DynamoDbProperty("number")]
+ [DynamoDbProperty("number"), SerializedName("number")]
public int Number { get; set; }
- [DynamoDbProperty("status")]
+ [DynamoDbProperty("status"), SerializedName("status")]
public string Status { get; set; } = "";
}
@@ -190,7 +192,7 @@ public void Cleanup()
// Dictionary? lastKey = null;
// do
// {
- // var result = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest
+ // var result = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest
// {
// TableName = _fixture.TableName,
// KeyConditionExpression = "pk = :pk",
@@ -200,7 +202,7 @@ public void Cleanup()
// },
// Limit = 5,
// ExclusiveStartKey = lastKey
- // }, DynamoItemReaderRegistry.Get());
+ // }, DynamoItemReaderRegistry.Get());
// count += result.Value.Items.Count;
// lastKey = result.Value.HasMoreResults ? result.Value.LastEvaluatedKey : null;
// } while (lastKey != null);
@@ -299,29 +301,29 @@ public async Task Goa_Query_100Items_DynamoRecord()
return count;
}
- // [Benchmark, BenchmarkCategory("100 Items")]
- // public async Task Goa_Query_100Items_Typed()
- // {
- // var count = 0;
- // Dictionary? lastKey = null;
- // do
- // {
- // var result = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest
- // {
- // TableName = _fixture.TableName,
- // KeyConditionExpression = "pk = :pk",
- // ExpressionAttributeValues = new Dictionary
- // {
- // [":pk"] = GoaModels.AttributeValue.String("query-100")
- // },
- // Limit = 25,
- // ExclusiveStartKey = lastKey
- // }, DynamoItemReaderRegistry.Get());
- // count += result.Value.Items.Count;
- // lastKey = result.Value.HasMoreResults ? result.Value.LastEvaluatedKey : null;
- // } while (lastKey != null);
- // return count;
- // }
+ [Benchmark, BenchmarkCategory("100 Items")]
+ public async Task Goa_Query_100Items_Typed()
+ {
+ var count = 0;
+ Dictionary? lastKey = null;
+ do
+ {
+ var result = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest
+ {
+ TableName = _fixture.TableName,
+ KeyConditionExpression = "pk = :pk",
+ ExpressionAttributeValues = new Dictionary
+ {
+ [":pk"] = GoaModels.AttributeValue.String("query-100")
+ },
+ Limit = 25,
+ ExclusiveStartKey = lastKey
+ }, DynamoItemReaderRegistry.Get());
+ count += result.Value.Items.Count;
+ lastKey = result.Value.HasMoreResults ? result.Value.LastEvaluatedKey : null;
+ } while (lastKey != null);
+ return count;
+ }
[Benchmark, BenchmarkCategory("100 Items")]
public async Task Efficient_Query_100Items()
@@ -420,7 +422,7 @@ public async Task Efficient_Query_100Items_Typed()
// Dictionary? lastKey = null;
// do
// {
- // var result = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest
+ // var result = await _fixture.GoaClient.QueryAsync(new GoaQueryRequest
// {
// TableName = _fixture.TableName,
// KeyConditionExpression = "pk = :pk",
@@ -429,7 +431,7 @@ public async Task Efficient_Query_100Items_Typed()
// [":pk"] = GoaModels.AttributeValue.String("nonexistent-pk")
// },
// ExclusiveStartKey = lastKey
- // }, DynamoItemReaderRegistry.Get());
+ // }, DynamoItemReaderRegistry.Get());
// count += result.Value.Items.Count;
// lastKey = result.Value.HasMoreResults ? result.Value.LastEvaluatedKey : null;
// } while (lastKey != null);
diff --git a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Goa.Clients.Dynamo.Benchmarks.csproj b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Goa.Clients.Dynamo.Benchmarks.csproj
index f741877a..25a78b30 100644
--- a/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Goa.Clients.Dynamo.Benchmarks.csproj
+++ b/tests/Clients/Goa.Clients.Dynamo.Benchmarks/Goa.Clients.Dynamo.Benchmarks.csproj
@@ -20,6 +20,7 @@
+
diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs
index 23c91b7b..dea59a87 100644
--- a/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs
+++ b/tests/Clients/Goa.Clients.Dynamo.Tests/BuilderChainingTests.cs
@@ -304,7 +304,7 @@ public async Task DeleteItemBuilder_WithReturnValuesOnConditionCheckFailure_AllO
.WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD);
var request = builder.Build();
- var json = JsonSerializer.Serialize(request, DynamoJsonContext.Default.DeleteItemRequest);
+ var json = SerializeWithGeneratedWriter(request);
await Assert.That(json).Contains("\"ReturnValuesOnConditionCheckFailure\":\"ALL_OLD\"");
}
@@ -317,7 +317,7 @@ public async Task PutItemBuilder_WithReturnValuesOnConditionCheckFailure_AllOld_
.WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD);
var request = builder.Build();
- var json = JsonSerializer.Serialize(request, DynamoJsonContext.Default.PutItemRequest);
+ var json = SerializeWithGeneratedWriter(request);
await Assert.That(json).Contains("\"ReturnValuesOnConditionCheckFailure\":\"ALL_OLD\"");
}
@@ -331,8 +331,20 @@ public async Task UpdateItemBuilder_WithReturnValuesOnConditionCheckFailure_AllO
.WithReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD);
var request = builder.Build();
- var json = JsonSerializer.Serialize(request, DynamoJsonContext.Default.UpdateItemRequest);
+ var json = SerializeWithGeneratedWriter(request);
await Assert.That(json).Contains("\"ReturnValuesOnConditionCheckFailure\":\"ALL_OLD\"");
}
+
+ private static string SerializeWithGeneratedWriter(T value)
+ {
+ var typeInfo = DynamoJsonContext.Default.GetTypeInfo(typeof(T))
+ as System.Text.Json.Serialization.Metadata.JsonTypeInfo
+ ?? throw new System.InvalidOperationException($"Cannot find type {typeof(T).Name} in serialization context");
+ using var buffer = new System.IO.MemoryStream();
+ using var jsonWriter = new System.Text.Json.Utf8JsonWriter(buffer);
+ System.Text.Json.JsonSerializer.Serialize(jsonWriter, value, typeInfo);
+ jsonWriter.Flush();
+ return System.Text.Encoding.UTF8.GetString(buffer.ToArray());
+ }
}
diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoItemWriterRegistryTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoItemWriterRegistryTests.cs
new file mode 100644
index 00000000..4a8ebcad
--- /dev/null
+++ b/tests/Clients/Goa.Clients.Dynamo.Tests/DynamoItemWriterRegistryTests.cs
@@ -0,0 +1,67 @@
+using System.Text.Json;
+
+namespace Goa.Clients.Dynamo.Tests;
+
+public class DynamoItemWriterRegistryTests
+{
+ private sealed class TestItem
+ {
+ public string? Name { get; set; }
+ public int Value { get; set; }
+ }
+
+ private sealed class UnregisteredItem
+ {
+ public string? Id { get; set; }
+ }
+
+ private static void WriteTestItem(Utf8JsonWriter writer, TestItem item)
+ {
+ writer.WriteStartObject();
+ if (item.Name is not null)
+ {
+ writer.WritePropertyName("name");
+ writer.WriteStartObject();
+ writer.WriteString("S", item.Name);
+ writer.WriteEndObject();
+ }
+ writer.WritePropertyName("value");
+ writer.WriteStartObject();
+ writer.WriteNumber("N", item.Value);
+ writer.WriteEndObject();
+ writer.WriteEndObject();
+ }
+
+ [Test]
+ public async Task Register_and_Get_roundtrip_returns_registered_writer()
+ {
+ DynamoItemWriterRegistry.Register(WriteTestItem);
+
+ var writer = DynamoItemWriterRegistry.Get();
+
+ await Assert.That(writer).IsEqualTo((DynamoItemWriter)WriteTestItem);
+ }
+
+ [Test]
+ public void Get_without_registration_throws_InvalidOperationException()
+ {
+ Assert.Throws(() => DynamoItemWriterRegistry.Get());
+ }
+
+ [Test]
+ public async Task Re_registering_overwrites_previous_writer()
+ {
+ static void originalWriter(Utf8JsonWriter writer, TestItem item)
+ {
+ writer.WriteStartObject();
+ writer.WriteEndObject();
+ }
+
+ DynamoItemWriterRegistry.Register(originalWriter);
+ DynamoItemWriterRegistry.Register(WriteTestItem);
+
+ var result = DynamoItemWriterRegistry.Get();
+
+ await Assert.That(result).IsEqualTo((DynamoItemWriter)WriteTestItem);
+ }
+}
diff --git a/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs b/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs
index f362a66a..63c6c46e 100644
--- a/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs
+++ b/tests/Clients/Goa.Clients.Dynamo.Tests/TypedExtensionTests.cs
@@ -1,14 +1,110 @@
+using ErrorOr;
using Goa.Clients.Dynamo.Exceptions;
using Goa.Clients.Dynamo.Models;
using Goa.Clients.Dynamo.Operations.Query;
using Goa.Clients.Dynamo.Operations.Scan;
-using ErrorOr;
using Moq;
namespace Goa.Clients.Dynamo.Tests;
public class TypedExtensionTests
{
+ private record TestModel(string Id, string Name);
+
+ private static readonly DynamoItemReader TestReader = (ref System.Text.Json.Utf8JsonReader _) => new TestModel("", "");
+
+ // Tests for QueryAllAsync auto-paginating
+ [Test]
+ public async Task QueryAllAsync_T_PaginatesAutomatically()
+ {
+ var mock = new Mock();
+ var lastKey = new Dictionary
+ {
+ ["pk"] = AttributeValue.String("lastPk")
+ };
+
+ ErrorOr> firstPage = new QueryResult
+ {
+ Items = [new TestModel("1", "Alice")],
+ LastEvaluatedKey = lastKey
+ };
+ ErrorOr> secondPage = new QueryResult
+ {
+ Items = [new TestModel("2", "Bob")],
+ LastEvaluatedKey = null
+ };
+
+ mock.Setup(c => c.QueryAsync(It.Is(r => r.ExclusiveStartKey == null), It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(firstPage);
+ mock.Setup(c => c.QueryAsync(It.Is(r => r.ExclusiveStartKey != null), It.IsAny